From c9801fc66ec0dff17228da702c7c545cfcb44b16 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 20 Nov 2025 16:33:44 +0300 Subject: [PATCH 001/423] FeatureFlags: Remove unused settings for admin UI (#114137) --- .../dashboardversion/dashverimpl/dashver.go | 4 +- .../featuremgmt/feature_toggle_api/types.go | 6 - pkg/services/featuremgmt/models.go | 4 - pkg/services/featuremgmt/openfeature_test.go | 10 +- pkg/services/featuremgmt/registry.go | 1168 ++++++++--------- pkg/services/featuremgmt/toggles-gitlog.csv | 152 ++- pkg/services/featuremgmt/toggles_gen.csv | 14 +- pkg/services/featuremgmt/toggles_gen.go | 304 ----- pkg/services/featuremgmt/toggles_gen.json | 186 +-- pkg/services/featuremgmt/toggles_gen_test.go | 26 +- 10 files changed, 703 insertions(+), 1171 deletions(-) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index ef39b815fad..2a4d31f2b4d 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -170,8 +170,8 @@ func (s *Service) RestoreVersion(ctx context.Context, cmd *dashver.RestoreVersio cmd.DashboardUID = u } //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) || - s.features.IsEnabledGlobally(featuremgmt.FlagDashboardNewLayouts) { + if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesDashboards) || + s.features.IsEnabled(ctx, featuremgmt.FlagDashboardNewLayouts) { s.log.Debug("restoring dashboard version through k8s") res, err := s.restoreVersionThroughK8s(ctx, cmd) if err != nil { diff --git a/pkg/services/featuremgmt/feature_toggle_api/types.go b/pkg/services/featuremgmt/feature_toggle_api/types.go index 6b62447fa33..046f59c28ab 100644 --- a/pkg/services/featuremgmt/feature_toggle_api/types.go +++ b/pkg/services/featuremgmt/feature_toggle_api/types.go @@ -44,12 +44,6 @@ type FeatureSpec struct { // The flag is used at startup, so any change requires a restart RequiresRestart bool `json:"requiresRestart,omitempty"` - // Allow cloud users to set the values in UI - AllowSelfServe bool `json:"allowSelfServe,omitempty"` - - // Do not show the value in the UI - HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` - // Do not show the value in docs HideFromDocs bool `json:"hideFromDocs,omitempty"` diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 557ca7ef8a2..45d3dc810f4 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -129,10 +129,6 @@ type FeatureFlag struct { Stage FeatureFlagStage `json:"stage,omitempty"` Owner codeowner `json:"-"` // Owner person or team that owns this feature flag - // Recommended properties - control behavior of the feature toggle management page in the UI - AllowSelfServe bool `json:"allowSelfServe,omitempty"` // allow users with the right privileges to toggle this from the UI (GeneralAvailability, PublicPreview, and Deprecated toggles only) - HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` // GA, Deprecated, and PublicPreview toggles only: don't display this feature in the UI; if this is a GA toggle, add a comment with the reasoning - // CEL-GO expression. Using the value "true" will mean this is on by default Expression string `json:"expression,omitempty"` diff --git a/pkg/services/featuremgmt/openfeature_test.go b/pkg/services/featuremgmt/openfeature_test.go index 2cb5c3542d0..788b53531d4 100644 --- a/pkg/services/featuremgmt/openfeature_test.go +++ b/pkg/services/featuremgmt/openfeature_test.go @@ -6,16 +6,16 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/clientauth/middleware" - "github.com/grafana/grafana/pkg/setting" - - authlib "github.com/grafana/authlib/authn" gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + authlib "github.com/grafana/authlib/authn" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/clientauth/middleware" + "github.com/grafana/grafana/pkg/setting" ) func TestCreateProvider(t *testing.T) { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bc4d4c282ee..e6587428f18 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -18,28 +18,24 @@ var ( // Register each toggle here standardFeatureFlags = []FeatureFlag{ { - Name: "disableEnvelopeEncryption", - Description: "Disable envelope encryption (emergency only)", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - Expression: "false", + Name: "disableEnvelopeEncryption", + Description: "Disable envelope encryption (emergency only)", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", }, { - Name: "panelTitleSearch", - Description: "Search for dashboards using panel title", - Stage: FeatureStagePublicPreview, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, + Name: "panelTitleSearch", + Description: "Search for dashboards using panel title", + Stage: FeatureStagePublicPreview, + Owner: grafanaSearchAndStorageSquad, }, { - Name: "publicDashboardsEmailSharing", - Description: "Enables public dashboard sharing to be restricted to only allowed emails", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "publicDashboardsEmailSharing", + Description: "Enables public dashboard sharing to be restricted to only allowed emails", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { Name: "publicDashboardsScene", @@ -56,12 +52,11 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "featureHighlights", - Description: "Highlight Grafana Enterprise features", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - AllowSelfServe: true, - Expression: "false", + Name: "featureHighlights", + Description: "Highlight Grafana Enterprise features", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", }, { Name: "storage", @@ -70,12 +65,11 @@ var ( Owner: grafanaSearchAndStorageSquad, }, { - Name: "canvasPanelNesting", - Description: "Allow elements nesting", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - HideFromAdminPage: true, + Name: "canvasPanelNesting", + Description: "Allow elements nesting", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "logRequestsInstrumentedAsUnknown", @@ -84,19 +78,17 @@ var ( Owner: grafanaBackendGroup, }, { - Name: "grpcServer", - Description: "Run the GRPC server", - Stage: FeatureStagePublicPreview, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, + Name: "grpcServer", + Description: "Run the GRPC server", + Stage: FeatureStagePublicPreview, + Owner: grafanaSearchAndStorageSquad, }, { - Name: "cloudWatchCrossAccountQuerying", - Description: "Enables cross-account querying in CloudWatch datasources", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, - AllowSelfServe: true, + Name: "cloudWatchCrossAccountQuerying", + Description: "Enables cross-account querying in CloudWatch datasources", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, }, { Name: "showDashboardValidationWarnings", @@ -117,13 +109,12 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "logsContextDatasourceUi", - Description: "Allow datasource to provide custom UI for context view", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default - AllowSelfServe: true, + Name: "logsContextDatasourceUi", + Description: "Allow datasource to provide custom UI for context view", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default }, { Name: "lokiShardSplitting", @@ -133,13 +124,12 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "lokiQuerySplitting", - Description: "Split large interval queries into subqueries with smaller time intervals", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default - AllowSelfServe: true, + Name: "lokiQuerySplitting", + Description: "Split large interval queries into subqueries with smaller time intervals", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default }, { Name: "individualCookiePreferences", @@ -148,22 +138,20 @@ var ( Owner: grafanaBackendGroup, }, { - Name: "influxdbBackendMigration", - Description: "Query InfluxDB InfluxQL without the proxy", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaPartnerPluginsSquad, - Expression: "true", // enabled by default - AllowSelfServe: false, + Name: "influxdbBackendMigration", + Description: "Query InfluxDB InfluxQL without the proxy", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaPartnerPluginsSquad, + Expression: "true", // enabled by default }, { - Name: "starsFromAPIServer", - Description: "populate star status from apiserver", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaFrontendSearchNavOrganise, - AllowSelfServe: false, - HideFromDocs: true, + Name: "starsFromAPIServer", + Description: "populate star status from apiserver", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaFrontendSearchNavOrganise, + HideFromDocs: true, }, { Name: "kubernetesStars", @@ -198,26 +186,23 @@ var ( Owner: grafanaDatasourcesCoreServicesSquad, }, { - Name: "unifiedRequestLog", - Description: "Writes error logs to the request logger", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaBackendGroup, - Expression: "true", - HideFromAdminPage: true, + Name: "unifiedRequestLog", + Description: "Writes error logs to the request logger", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaBackendGroup, + Expression: "true", }, { - Name: "renderAuthJWT", - Description: "Uses JWT-based auth for rendering instead of relying on remote cache", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, + Name: "renderAuthJWT", + Description: "Uses JWT-based auth for rendering instead of relying on remote cache", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, }, { - Name: "refactorVariablesTimeRange", - Description: "Refactor time range variables flow to reduce number of API calls made when query variables are chained", - Stage: FeatureStagePublicPreview, - Owner: grafanaDashboardsSquad, - HideFromAdminPage: true, // Non-feature, used to test out a bug fix that impacts the performance of template variables. + Name: "refactorVariablesTimeRange", + Description: "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + Stage: FeatureStagePublicPreview, + Owner: grafanaDashboardsSquad, }, { Name: "faroDatasourceSelector", @@ -229,7 +214,7 @@ var ( { Name: "enableDatagridEditing", Description: "Enables the edit functionality in the datagrid panel", - FrontendOnly: true, + FrontendOnly: false, // The set of plugins returned in frontend settings changes based on this flag Stage: FeatureStagePublicPreview, Owner: grafanaDatavizSquad, }, @@ -358,31 +343,26 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "externalServiceAccounts", - Description: "Automatic service account and token setup for plugins", - HideFromAdminPage: true, - Stage: FeatureStagePublicPreview, - Owner: identityAccessTeam, + Name: "externalServiceAccounts", + Description: "Automatic service account and token setup for plugins", + Stage: FeatureStagePublicPreview, + Owner: identityAccessTeam, }, { - Name: "enableNativeHTTPHistogram", - Description: "Enables native HTTP Histograms", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaBackendServicesSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - RequiresRestart: true, + Name: "enableNativeHTTPHistogram", + Description: "Enables native HTTP Histograms", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + RequiresRestart: true, }, { - Name: "disableClassicHTTPHistogram", - Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaBackendServicesSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - RequiresRestart: true, + Name: "disableClassicHTTPHistogram", + Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + RequiresRestart: true, }, { Name: "kubernetesSnapshots", @@ -403,7 +383,7 @@ var ( Description: "Use the kubernetes API in the frontend for dashboards", Stage: FeatureStageGeneralAvailability, Owner: grafanaDashboardsSquad, - FrontendOnly: true, + FrontendOnly: false, // The backend changes permission behavior based on this flag Expression: "true", // enabled by default }, { @@ -534,20 +514,18 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "alertingProvenanceLockWrites", - Description: "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertingProvenanceLockWrites", + Description: "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "alertingUIUseBackendFilters", - Description: "Enables the UI to use certain backend-side filters", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertingUIUseBackendFilters", + Description: "Enables the UI to use certain backend-side filters", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { Name: "alertmanagerRemotePrimary", @@ -591,7 +569,7 @@ var ( Name: "dashboardNewLayouts", Description: "Enables experimental new dashboard layouts", Stage: FeatureStageExperimental, - FrontendOnly: true, + FrontendOnly: false, // The restore backend feature changes behavior based on this flag Owner: grafanaDashboardsSquad, }, { @@ -653,42 +631,35 @@ var ( Owner: grafanaDatavizSquad, }, { - Name: "kubernetesFeatureToggles", - Description: "Use the kubernetes API for feature toggle management in the frontend", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaOperatorExperienceSquad, - AllowSelfServe: false, - HideFromAdminPage: true, + Name: "kubernetesFeatureToggles", + Description: "Use the kubernetes API for feature toggle management in the frontend", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaOperatorExperienceSquad, }, { - Name: "cloudRBACRoles", - Description: "Enabled grafana cloud specific RBAC roles", - Stage: FeatureStagePublicPreview, - Owner: identityAccessTeam, - HideFromDocs: true, - AllowSelfServe: true, - HideFromAdminPage: true, - RequiresRestart: true, + Name: "cloudRBACRoles", + Description: "Enabled grafana cloud specific RBAC roles", + Stage: FeatureStagePublicPreview, + Owner: identityAccessTeam, + HideFromDocs: true, + RequiresRestart: true, }, { - Name: "alertingQueryOptimization", - Description: "Optimizes eligible queries in order to reduce load on datasources", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - Expression: "false", + Name: "alertingQueryOptimization", + Description: "Optimizes eligible queries in order to reduce load on datasources", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + Expression: "false", }, { - Name: "jitterAlertRulesWithinGroups", - Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.", - FrontendOnly: false, - Stage: FeatureStagePublicPreview, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: false, - RequiresRestart: true, + Name: "jitterAlertRulesWithinGroups", + Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.", + FrontendOnly: false, + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + RequiresRestart: true, }, { Name: "onPremToCloudMigrations", @@ -725,41 +696,37 @@ var ( Expression: "true", }, { - Name: "scopeApi", - Description: "In-development feature flag for the scope api using the app platform.", - Stage: FeatureStageExperimental, - Owner: grafanaAppPlatformSquad, - HideFromAdminPage: true, - Expression: "false", + Name: "scopeApi", + Description: "In-development feature flag for the scope api using the app platform.", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + Expression: "false", }, { - Name: "useScopeSingleNodeEndpoint", - Description: "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - Expression: "false", - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useScopeSingleNodeEndpoint", + Description: "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "useMultipleScopeNodesEndpoint", - Description: "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - Expression: "false", - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useMultipleScopeNodesEndpoint", + Description: "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "logQLScope", - Description: "In-development feature that will allow injection of labels into loki queries.", - Stage: FeatureStagePrivatePreview, - Owner: grafanaObservabilityLogsSquad, - Expression: "false", - HideFromDocs: true, - HideFromAdminPage: true, + Name: "logQLScope", + Description: "In-development feature that will allow injection of labels into loki queries.", + Stage: FeatureStagePrivatePreview, + Owner: grafanaObservabilityLogsSquad, + Expression: "false", + HideFromDocs: true, }, { Name: "sqlExpressions", @@ -790,58 +757,49 @@ var ( RequiresRestart: true, }, { - Name: "groupByVariable", - Description: "Enable groupBy variable support in scenes dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "groupByVariable", + Description: "Enable groupBy variable support in scenes dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { - Name: "scopeFilters", - Description: "Enables the use of scope filters in Grafana", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "scopeFilters", + Description: "Enables the use of scope filters in Grafana", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { - Name: "oauthRequireSubClaim", - Description: "Require that sub claims is present in oauth tokens.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "oauthRequireSubClaim", + Description: "Require that sub claims is present in oauth tokens.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "refreshTokenRequired", - Description: "Require that refresh tokens are present in oauth tokens.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "refreshTokenRequired", + Description: "Require that refresh tokens are present in oauth tokens.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "newDashboardWithFiltersAndGroupBy", - Description: "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "newDashboardWithFiltersAndGroupBy", + Description: "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { - Name: "cloudWatchNewLabelParsing", - Description: "Updates CloudWatch label parsing to be more accurate", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, - FrontendOnly: false, - AllowSelfServe: false, + Name: "cloudWatchNewLabelParsing", + Description: "Updates CloudWatch label parsing to be more accurate", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + FrontendOnly: false, }, { Name: "disableNumericMetricsSortingInExpressions", @@ -852,21 +810,18 @@ var ( RequiresRestart: true, }, { - Name: "grafanaManagedRecordingRules", - Description: "Enables Grafana-managed recording rules.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "grafanaManagedRecordingRules", + Description: "Enables Grafana-managed recording rules.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "queryLibrary", - Description: "Enables Saved queries (query library) feature", - Stage: FeatureStagePublicPreview, - Owner: grafanaSharingSquad, - FrontendOnly: false, - AllowSelfServe: false, + Name: "queryLibrary", + Description: "Enables Saved queries (query library) feature", + Stage: FeatureStagePublicPreview, + Owner: grafanaSharingSquad, + FrontendOnly: false, }, { Name: "dashboardLibrary", @@ -897,28 +852,25 @@ var ( FrontendOnly: true, }, { - Name: "alertingDisableSendAlertsExternal", - Description: "Disables the ability to send alerts to an external Alertmanager datasource.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "alertingDisableSendAlertsExternal", + Description: "Disables the ability to send alerts to an external Alertmanager datasource.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "preserveDashboardStateWhenNavigating", - Description: "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "preserveDashboardStateWhenNavigating", + Description: "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { Name: "alertingCentralAlertHistory", Description: "Enables the new central alert history.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - FrontendOnly: true, + FrontendOnly: false, // changes navtree from backend }, { Name: "pluginProxyPreserveTrailingSlash", @@ -935,59 +887,51 @@ var ( Expression: "true", // enabled by default }, { - Name: "authZGRPCServer", - Description: "Enables the gRPC server for authorization", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "authZGRPCServer", + Description: "Enables the gRPC server for authorization", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "ssoSettingsLDAP", Description: "Use the new SSO Settings API to configure LDAP", Stage: FeatureStageGeneralAvailability, Owner: identityAccessTeam, - AllowSelfServe: true, RequiresRestart: true, Expression: "true", // enabled by default }, { - Name: "zanzana", - Description: "Use openFGA as authorization engine.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "zanzana", + Description: "Use openFGA as authorization engine.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "zanzanaNoLegacyClient", - Description: "Use openFGA as main authorization engine and disable legacy RBAC clietn.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "zanzanaNoLegacyClient", + Description: "Use openFGA as main authorization engine and disable legacy RBAC clietn.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "reloadDashboardsOnParamsChange", - Description: "Enables reload of dashboards on scopes, time range and variables changes", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "reloadDashboardsOnParamsChange", + Description: "Enables reload of dashboards on scopes, time range and variables changes", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { - Name: "enableScopesInMetricsExplore", - Description: "Enables the scopes usage in Metrics Explore", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "enableScopesInMetricsExplore", + Description: "Enables the scopes usage in Metrics Explore", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { Name: "cloudWatchRoundUpEndTime", @@ -1024,13 +968,12 @@ var ( Expression: "true", // enabled by default }, { - Name: "vizActionsAuth", - Description: "Allows authenticated API calls in actions", - Stage: FeatureStagePublicPreview, - Owner: grafanaDatavizSquad, - FrontendOnly: true, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "vizActionsAuth", + Description: "Allows authenticated API calls in actions", + Stage: FeatureStagePublicPreview, + Owner: grafanaDatavizSquad, + FrontendOnly: true, + HideFromDocs: true, }, { Name: "alertingPrometheusRulesPrimary", @@ -1061,12 +1004,11 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "appPlatformGrpcClientAuth", - Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "appPlatformGrpcClientAuth", + Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "groupAttributeSync", @@ -1084,12 +1026,11 @@ var ( Expression: "true", }, { - Name: "improvedExternalSessionHandling", - Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: identityAccessTeam, - AllowSelfServe: true, + Name: "improvedExternalSessionHandling", + Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: identityAccessTeam, }, { Name: "useSessionStorageForRedirection", @@ -1105,28 +1046,25 @@ var ( Owner: identityAccessTeam, }, { - Name: "unifiedStorageSearch", - Description: "Enable unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearch", + Description: "Enable unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "unifiedStorageSearchSprinkles", - Description: "Enable sprinkles on unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearchSprinkles", + Description: "Enable sprinkles on unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "managedDualWriter", - Description: "Pick the dual write mode from database configs", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "managedDualWriter", + Description: "Pick the dual write mode from database configs", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "pluginsSriChecks", @@ -1169,13 +1107,11 @@ var ( RequiresRestart: true, }, { - Name: "passwordlessMagicLinkAuthentication", - Description: "Enable passwordless login via magic link authentication", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, - AllowSelfServe: false, + Name: "passwordlessMagicLinkAuthentication", + Description: "Enable passwordless login via magic link authentication", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "exploreMetricsRelatedLogs", @@ -1228,67 +1164,60 @@ var ( Expression: "true", // Enabled by default for now }, { - Name: "alertingAIGenAlertRules", - Description: "Enable AI-generated alert rules.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIGenAlertRules", + Description: "Enable AI-generated alert rules.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIFeedback", - Description: "Enable AI-generated feedback from the Grafana UI.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIFeedback", + Description: "Enable AI-generated feedback from the Grafana UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIImproveAlertRules", - Description: "Enable AI-improve alert rules labels and annotations.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIImproveAlertRules", + Description: "Enable AI-improve alert rules labels and annotations.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIGenTemplates", - Description: "Enable AI-generated alerting templates.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIGenTemplates", + Description: "Enable AI-generated alerting templates.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingEnrichmentPerRule", - Description: "Enable enrichment per rule in the alerting UI.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingEnrichmentPerRule", + Description: "Enable enrichment per rule in the alerting UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingEnrichmentAssistantInvestigations", - Description: "Enable Assistant Investigations enrichment type.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingEnrichmentAssistantInvestigations", + Description: "Enable Assistant Investigations enrichment type.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIAnalyzeCentralStateHistory", - Description: "Enable AI-analyze central state history.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIAnalyzeCentralStateHistory", + Description: "Enable AI-analyze central state history.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "alertingNotificationsStepMode", @@ -1299,12 +1228,11 @@ var ( Expression: "true", }, { - Name: "unifiedStorageSearchUI", - Description: "Enable unified storage search UI", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearchUI", + Description: "Enable unified storage search UI", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "elasticsearchCrossClusterSearch", @@ -1351,12 +1279,11 @@ var ( Expression: "false", }, { - Name: "improvedExternalSessionHandlingSAML", - Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: identityAccessTeam, - AllowSelfServe: true, + Name: "improvedExternalSessionHandlingSAML", + Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: identityAccessTeam, }, { Name: "teamHttpHeadersTempo", @@ -1386,12 +1313,11 @@ var ( FrontendOnly: true, }, { - Name: "fetchRulesUsingPost", - Description: "Use a POST request to list rules by passing down the namespaces user has access to", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "fetchRulesUsingPost", + Description: "Use a POST request to list rules by passing down the namespaces user has access to", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { Name: "newLogsPanel", @@ -1402,14 +1328,13 @@ var ( Expression: "true", }, { - Name: "grafanaconThemes", - Description: "Enables the temporary themes for GrafanaCon", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaFrontendPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - RequiresRestart: true, - Expression: "true", + Name: "grafanaconThemes", + Description: "Enables the temporary themes for GrafanaCon", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaFrontendPlatformSquad, + HideFromDocs: true, + RequiresRestart: true, + Expression: "true", }, { Name: "alertingJiraIntegration", @@ -1420,58 +1345,52 @@ var ( HideFromDocs: true, }, { - Name: "alertingUseNewSimplifiedRoutingHashAlgorithm", - Description: "", - Stage: FeatureStagePublicPreview, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - RequiresRestart: true, - Expression: "true", + Name: "alertingUseNewSimplifiedRoutingHashAlgorithm", + Description: "", + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + RequiresRestart: true, + Expression: "true", }, { - Name: "useScopesNavigationEndpoint", - Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useScopesNavigationEndpoint", + Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "scopeSearchAllLevels", - Description: "Enable scope search to include all levels of the scope node tree", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "scopeSearchAllLevels", + Description: "Enable scope search to include all levels of the scope node tree", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { - Name: "alertingRuleVersionHistoryRestore", - Description: "Enables the alert rule version history restore feature", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRuleVersionHistoryRestore", + Description: "Enables the alert rule version history restore feature", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "newShareReportDrawer", - Description: "Enables the report creation drawer in a dashboard", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "newShareReportDrawer", + Description: "Enables the report creation drawer in a dashboard", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { - Name: "rendererDisableAppPluginsPreload", - Description: "Disable pre-loading app plugins when the request is coming from the renderer", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, + Name: "rendererDisableAppPluginsPreload", + Description: "Disable pre-loading app plugins when the request is coming from the renderer", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, + FrontendOnly: true, }, { Name: "assetSriChecks", @@ -1524,32 +1443,29 @@ var ( Owner: grafanaFrontendPlatformSquad, }, { - Name: "unifiedStorageGrpcConnectionPool", - Description: "Enables the unified storage grpc connection pool", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "unifiedStorageGrpcConnectionPool", + Description: "Enables the unified storage grpc connection pool", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "alertingRulePermanentlyDelete", - Description: "Enables UI functionality to permanently delete alert rules", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRulePermanentlyDelete", + Description: "Enables UI functionality to permanently delete alert rules", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "alertingRuleRecoverDeleted", - Description: "Enables the UI functionality to recover and view deleted alert rules", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRuleRecoverDeleted", + Description: "Enables the UI functionality to recover and view deleted alert rules", + FrontendOnly: false, // changes navtree from the backend + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { Name: "multiTenantTempCredentials", @@ -1596,11 +1512,10 @@ var ( Owner: grafanaOSSBigTent, }, { - Name: "tempoAlerting", - Description: "Enables creating alerts from Tempo data source", - Stage: FeatureStageExperimental, - Owner: grafanaObservabilityTracesAndProfilingSquad, - FrontendOnly: true, + Name: "tempoAlerting", + Description: "Enables creating alerts from Tempo data source", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityTracesAndProfilingSquad, }, { Name: "pluginsAutoUpdate", @@ -1624,107 +1539,95 @@ var ( Expression: "false", }, { - Name: "alertingBulkActionsInUI", - Description: "Enables the alerting bulk actions in the UI", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingBulkActionsInUI", + Description: "Enables the alerting bulk actions in the UI", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "kubernetesAuthzApis", - Description: "Registers AuthZ /apis endpoint", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzApis", + Description: "Registers AuthZ /apis endpoint", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthZHandlerRedirect", - Description: "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthZHandlerRedirect", + Description: "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthzResourcePermissionApis", - Description: "Registers AuthZ resource permission /apis endpoints", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzResourcePermissionApis", + Description: "Registers AuthZ resource permission /apis endpoints", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthzZanzanaSync", - Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzZanzanaSync", + Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthnMutation", - Description: "Enables create, delete, and update mutations for resources owned by IAM identity", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthnMutation", + Description: "Enables create, delete, and update mutations for resources owned by IAM identity", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "restoreDashboards", - Description: "Enables restore deleted dashboards feature", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendSearchNavOrganise, - HideFromAdminPage: true, - Expression: "false", + Name: "restoreDashboards", + Description: "Enables restore deleted dashboards feature", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + Expression: "false", }, { - Name: "alertEnrichment", - Description: "Enable configuration of alert enrichments in Grafana Cloud.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichment", + Description: "Enable configuration of alert enrichments in Grafana Cloud.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertEnrichmentMultiStep", - Description: "Allow multiple steps per enrichment.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichmentMultiStep", + Description: "Allow multiple steps per enrichment.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertEnrichmentConditional", - Description: "Enable conditional alert enrichment steps.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichmentConditional", + Description: "Enable conditional alert enrichment steps.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingImportAlertmanagerAPI", - Description: "Enables the API to import Alertmanager configuration", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingImportAlertmanagerAPI", + Description: "Enables the API to import Alertmanager configuration", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingImportAlertmanagerUI", - Description: "Enables the UI to see imported Alertmanager configuration", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingImportAlertmanagerUI", + Description: "Enables the UI to see imported Alertmanager configuration", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "sharingDashboardImage", @@ -1757,34 +1660,31 @@ var ( Expression: "false", }, { - Name: "enableAppChromeExtensions", - Description: "Set this to true to enable all app chrome extensions registered by plugins.", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", // extensions will be disabled by default + Name: "enableAppChromeExtensions", + Description: "Set this to true to enable all app chrome extensions registered by plugins.", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", // extensions will be disabled by default }, { - Name: "enableDashboardEmptyExtensions", - Description: "Set this to true to enable all dashboard empty state extensions registered by plugins.", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", // extensions will be disabled by default + Name: "enableDashboardEmptyExtensions", + Description: "Set this to true to enable all dashboard empty state extensions registered by plugins.", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", // extensions will be disabled by default }, { - Name: "foldersAppPlatformAPI", - Description: "Enables use of app platform API for folders", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendSearchNavOrganise, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", + Name: "foldersAppPlatformAPI", + Description: "Enables use of app platform API for folders", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", }, { Name: "otelLogsFormatting", @@ -1794,21 +1694,19 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "alertingNotificationHistory", - Description: "Enables the notification history feature", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingNotificationHistory", + Description: "Enables the notification history feature", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "unifiedStorageSearchDualReaderEnabled", - Description: "Enable dual reader for unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "unifiedStorageSearchDualReaderEnabled", + Description: "Enable dual reader for unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "dashboardLevelTimeMacros", @@ -1818,22 +1716,20 @@ var ( FrontendOnly: true, }, { - Name: "alertmanagerRemoteSecondaryWithRemoteState", - Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertmanagerRemoteSecondaryWithRemoteState", + Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "restrictedPluginApis", - Description: "Enables sharing a list of APIs with a list of plugins", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", + Name: "restrictedPluginApis", + Description: "Enables sharing a list of APIs with a list of plugins", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", }, { Name: "favoriteDatasources", @@ -1872,14 +1768,13 @@ var ( Owner: grafanaPathfinderSquad, }, { - Name: "alertingTriage", - Description: "Enables the alerting triage feature", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaAlertingSquad, - HideFromDocs: true, - HideFromAdminPage: true, - Expression: "false", + Name: "alertingTriage", + Description: "Enables the alerting triage feature", + Stage: FeatureStageExperimental, + FrontendOnly: false, // changes navtree in backend + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "graphiteBackendMode", @@ -1992,25 +1887,22 @@ var ( Expression: "false", }, { - Name: "onlyStoreActionSets", - Description: "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: false, - HideFromDocs: true, - HideFromAdminPage: true, // this should not be a user facing change - Owner: identityAccessTeam, - Expression: "true", + Name: "onlyStoreActionSets", + Description: "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: false, + HideFromDocs: true, + Owner: identityAccessTeam, + Expression: "true", }, { - Name: "panelTimeSettings", - Description: "Enables a new panel time settings drawer", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: false, - HideFromAdminPage: false, + Name: "panelTimeSettings", + Description: "Enables a new panel time settings drawer", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: false, }, { Name: "dashboardTemplates", @@ -2048,16 +1940,14 @@ var ( Owner: grafanaPluginsPlatformSquad, }, { - Name: "rudderstackUpgrade", - Description: "Enables the new version of rudderstack", - FrontendOnly: true, - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - Expression: "false", - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: false, - HideFromAdminPage: false, + Name: "rudderstackUpgrade", + Description: "Enables the new version of rudderstack", + FrontendOnly: true, + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + Expression: "false", + RequiresRestart: false, + HideFromDocs: false, }, } ) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index d8fbbaf3924..3f34ca9edc9 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -75,7 +75,7 @@ traceqlEditor,2022-08-24T16:57:59Z,2022-12-13T13:27:45Z,c8f2148f7504b393b8f9b32f athenaAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu redshiftAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu increaseInMemDatabaseQueryCache,2022-09-12T07:50:54Z,2023-02-06T21:01:04Z,72ae4a5aa368d18793f778683e92ae4a3ac7a1db,Carl Bergquist -correlations,2022-09-16T13:14:27Z,,9b4cdfe6526a290e5708a8c0760d50efda607d5c,Piotr Jamróz +correlations,2022-09-16T13:14:27Z,2025-11-13T09:21:46Z,9b4cdfe6526a290e5708a8c0760d50efda607d5c,Piotr Jamróz grpcServer,2022-09-26T20:25:34Z,,55aae797441d2beadc911e390f14c04bb3f101da,Alexander Emelin alertingBigTransactions,2022-10-06T06:22:58Z,2023-04-06T16:06:25Z,b476ae62fba9b94dd272409c521812108750bf50,Joe Blubaugh lokiMonacoEditor,2022-10-06T14:35:30Z,2023-02-06T10:18:01Z,729ce8bb72a5db53fbeb1b5856557652bb175c7a,Matias Chomicki @@ -88,7 +88,7 @@ showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031fe interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard accessControlOnCall,2022-10-19T16:10:09Z,2025-02-25T12:44:40Z,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley -nestedFolders,2022-10-26T14:15:14Z,2025-08-06T07:07:23Z,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert +nestedFolders,2022-10-26T14:15:14Z,2025-07-31T22:56:50Z,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist promQueryBuilder,2022-11-03T17:34:01Z,2022-12-19T13:52:06Z,857e545c5ac71722c8b2d3d27621dfb453dbb8ff,Ryan McKinley elasticsearchBackendMigration,2022-11-10T15:35:15Z,2023-04-12T12:20:43Z,261d620f1c46eb43282cc444ffb4633b77af4283,Ivana Huckova @@ -108,7 +108,7 @@ supportBundles,2022-12-20T10:13:37Z,2023-02-10T09:12:04Z,2c7410c87d81b8a644e738f publicDashboardsEmailSharing,2023-01-03T19:45:15Z,,f0ee3ac80ae2af695c56e709af435c155a2fb33d,owensmallwood alertingNoNormalState,2023-01-13T23:29:29Z,2025-02-03T15:32:50Z,9d57b1c72eb817c5fe1b43ad7bc9706a264b2da0,Yuri Tseretyan azureMultipleResourcePicker,2023-01-20T15:29:23Z,2023-01-30T16:19:03Z,b1efd911c1f7bfabe1b8231b059470c72dce4712,Andres Martinez Gotor -editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi +editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,2025-11-12T14:47:44Z,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi topNavCommandPalette,2023-01-24T12:41:09Z,2023-02-24T12:14:53Z,88347caf5fcc99c21c8a07367b9e846947367636,Josh Hunt k8sDashboards,2023-01-25T19:10:16Z,2023-02-09T17:54:00Z,4965cf2edae6eae8e341e6c93c3a1aa56417efb3,Ryan McKinley logsContextDatasourceUi,2023-01-27T14:12:01Z,,7c02d9bb8a864aab35c676f1a44c6c5be9eb6c09,Sven Grossmann @@ -155,11 +155,11 @@ extraThemes,2023-05-10T13:37:04Z,2025-05-20T08:18:08Z,f8cf67347f069310b8cc6a2d9f dataSourcePageHeader,2023-05-23T13:18:00Z,2023-11-06T20:21:15Z,7f84e83ffee53446b4c458c29f7c1385dac09ce3,Taewoo K alertingNotificationsPoliciesMatchingInstances,2023-05-30T13:15:22Z,2023-11-09T17:35:03Z,2f0728ac677c26b291947354058253d2e8a50bb7,Konrad Lalik lokiPredefinedOperations,2023-06-02T10:52:36Z,2025-06-30T14:08:36Z,06003c98c855fcb7c1c718c3f0e5f190b644656a,Ivana Huckova -pluginsFrontendSandbox,2023-06-05T08:51:36Z,,1ed4c0382b3812eecf9722596d8a3ac922f3543d,Esteban Beltran +pluginsFrontendSandbox,2023-06-05T08:51:36Z,2025-11-06T10:06:53Z,1ed4c0382b3812eecf9722596d8a3ac922f3543d,Esteban Beltran refactorVariablesTimeRange,2023-06-06T13:12:09Z,,07dd90b5a8866273813c6b4a5e7087cea529f908,Alexa V -sqlDatasourceDatabaseSelection,2023-06-06T16:28:52Z,2025-08-12T13:22:30Z,c0a1fc2cbdc1ceab4fdcb43270fd3720efe4494a,Jev Forsberg +sqlDatasourceDatabaseSelection,2023-06-06T16:28:52Z,2025-07-31T22:56:50Z,c0a1fc2cbdc1ceab4fdcb43270fd3720efe4494a,Jev Forsberg cloudWatchLogsMonacoEditor,2023-06-12T13:49:52Z,2024-03-18T12:56:57Z,5a831d877ac3305e7dfdb54057069b41b36b40fa,Isabella Siu -recordedQueriesMulti,2023-06-14T12:34:22Z,,db75f20e53e717503fe3bf27217fedc40f4a12e1,Kyle Brandt +recordedQueriesMulti,2023-06-14T12:34:22Z,2025-11-10T20:31:43Z,db75f20e53e717503fe3bf27217fedc40f4a12e1,Kyle Brandt exploreScrollableLogsContainer,2023-06-15T11:25:34Z,2024-03-12T14:53:13Z,cda10fae525d5cf784368791e1aaa1de2ce88f4b,Gareth Dawson alertingLokiRangeToInstant,2023-06-16T17:55:49Z,2023-09-13T11:52:40Z,934ba1aaa1fa4c1b9f1dcc0d6753c67a23c4f466,Jean-Philippe Quéméner lokiExperimentalStreaming,2023-06-19T10:03:51Z,,271cdb4baa6d5b021e013b463350f227173c4e95,Gábor Farkas @@ -178,7 +178,7 @@ lokiLogsDataplane,2023-07-13T07:58:00Z,,e045860fbf449feb101d187730a46bb1cdab787a mlExpressions,2023-07-13T17:37:50Z,,541bfe636daa55705a130df4004aa417c99adc09,Yuri Tseretyan disableTraceQLStreaming,2023-07-14T14:10:46Z,2023-07-26T13:33:16Z,c1709c93013a3fa5797898e81c046a2a53ae7ed4,Andre Pereira grafanaAPIServer,2023-07-14T19:22:10Z,2024-01-23T16:27:28Z,52121b7165b6ffcdf10dd7a62fdf04043ef0c00a,Todd Treece -featureToggleAdminPage,2023-07-18T20:43:32Z,,600f623610b00547400e00fd0b2ee7dc4bab5849,Ibrahim +featureToggleAdminPage,2023-07-18T20:43:32Z,2025-08-29T14:46:39Z,600f623610b00547400e00fd0b2ee7dc4bab5849,Ibrahim lokiFormatQuery,2023-07-21T12:03:56Z,2024-06-05T09:46:28Z,4e42f9b6198d39ffca82c3c3d32a16d54c66090b,Gareth Dawson splitScopes,2023-07-21T14:23:01Z,2024-02-23T15:53:37Z,cfa1a2c55feea58bd7f82e4e03df2ea712ef34a7,Ieva awsAsyncQueryCaching,2023-07-21T15:34:07Z,,56913fbd9578c4666599b2ab46423361d6c9261f,Isabella Siu @@ -215,7 +215,7 @@ enableNativeHTTPHistogram,2023-10-03T18:23:55Z,,0fc403d116b9cbd0b93e497cff0ddded transformationsVariableSupport,2023-10-04T14:28:46Z,2025-02-17T12:07:11Z,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed kubernetesPlaylists,2023-10-05T19:00:36Z,2024-08-13T08:03:28Z,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c184bfcddf2410eed9d908,Ryan McKinley -panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin +panelMonitoring,2023-10-09T05:19:08Z,2025-11-07T19:04:42Z,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin navAdminSubsections,2023-10-10T10:50:44Z,2023-11-17T10:04:34Z,f56cc6fdc01dbf2efb802f22650cb2bef0c40179,Ashley Harrison recoveryThreshold,2023-10-10T14:51:50Z,2025-04-24T15:58:17Z,810fbc3327841da6d21f945e75cad9daf040e625,Yuri Tseretyan libraryPanelRBAC,2023-10-11T23:30:50Z,2025-06-27T08:09:02Z,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,kay delaney @@ -223,7 +223,7 @@ awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,2024-07-22T12:48:17Z,2771fb940 cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus panelTitleSearchInV1,2023-10-13T12:04:24Z,2025-01-21T09:59:32Z,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R exploreContentOutline,2023-10-13T16:57:13Z,2024-06-24T15:45:42Z,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac -formatString,2023-10-13T18:17:12Z,2025-11-12T00:00:00Z,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol +formatString,2023-10-13T18:17:12Z,2025-11-17T13:06:30Z,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol pluginsInstrumentationStatusSource,2023-10-17T08:27:45Z,2024-02-21T11:57:40Z,f5076d1868caa14ce44a70e812315541b4199d9f,Giuseppe Guerra teamHttpHeaders,2023-10-17T10:23:54Z,2025-02-20T10:26:46Z,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck costManagementUi,2023-10-17T16:15:51Z,2024-01-08T14:25:11Z,de1ed216f4bbf6f341aa22b144fa66d583a63981,Adam Bannach @@ -237,10 +237,10 @@ alertmanagerRemoteSecondary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba4 annotationPermissionUpdate,2023-10-31T13:30:13Z,,c51c51458e4dd103aa0c099aa48b0d41f9375f86,Ieva kubernetesPlaylistsAPI,2023-10-31T17:26:39Z,2023-11-08T19:14:05Z,dd773e74f120ba908cafd596a6875c2d7fa199bf,Ryan McKinley traceToProfiles,2023-11-01T10:14:24Z,2024-01-22T14:21:14Z,c39e9a8f527b79881b95f64fd1c413b4bff42983,Joey -extractFieldsNameDeduplication,2023-11-02T15:47:42Z,,0eda368d32dbd7f57635cabd6b7d6870ca7b58ee,Oscar Kilhed +extractFieldsNameDeduplication,2023-11-02T15:47:42Z,2025-11-12T10:08:13Z,0eda368d32dbd7f57635cabd6b7d6870ca7b58ee,Oscar Kilhed dashboardSceneForViewers,2023-11-02T19:02:25Z,,6e80a3d59b14d9af235431d6acdce95835e2a792,Dominik Prokop panelFilterVariable,2023-11-03T12:15:54Z,,6bf4d0cbc6756ec8e8fb64b3a9d7b76f4c4194f5,Jacob Zelek -addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,,61d63d3034d7eed4837c63e1f5cb2f6b7114758d,Victor Marin +addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,61d63d3034d7eed4837c63e1f5cb2f6b7114758d,Victor Marin 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 @@ -250,10 +250,11 @@ alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8 alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar dashboardScene,2023-11-13T08:51:21Z,,4bc322ca1d6ed63d7e79eecb1ed09f3043f9aedb,Torkel Ödegaard datatrails,2023-11-15T11:28:29Z,2024-04-09T18:15:18Z,1f1d348e1700735683dc9b80fce106b8a5bc4cee,Torkel Ödegaard -pluginsSkipHostEnvVars,2023-11-15T17:09:14Z,,cb0a88a02770eaee2561fd434d8339cab268f02e,Giuseppe Guerra +pluginsSkipHostEnvVars,2023-11-15T17:09:14Z,2025-11-13T15:31:57Z,cb0a88a02770eaee2561fd434d8339cab268f02e,Giuseppe Guerra logRowsPopoverMenu,2023-11-16T09:48:10Z,,9cb303c3f701169e8651267744a4b5fe1695a066,Matias Chomicki lokiStructuredMetadata,2023-11-16T16:06:14Z,2025-06-30T14:09:44Z,a01f8c5b42bb7937139b8d52e9723cc37e5f7ae6,Sven Grossmann tracesEmbeddedFlameGraph,2023-11-23T13:36:53Z,2024-01-22T14:21:14Z,4f46fb412ca4e96b0b0ef3f462aead3fc146389e,Joey +regressionTransformation,2023-11-24T14:49:16Z,2025-07-01T13:59:22Z,ab982e7bd36b86c94093bddcc31008f5dc49660e,Oscar Kilhed displayAnonymousStats,2023-11-29T16:58:41Z,2024-02-23T15:53:37Z,59bdff0280d52ca5d8918157d7697b9279b25501,Eric Leijonmarck influxqlStreamingParser,2023-11-29T17:29:35Z,,5845f140758473ab5ffe789bec4077032fd22839,ismail simsek kubernetesSnapshots,2023-12-05T22:31:49Z,,439edebcd605a1b63bf3a9b0ab5c2b83341cd5cd,Ryan McKinley @@ -274,12 +275,12 @@ jitterAlertRules,2024-01-18T18:48:11Z,2024-02-09T21:53:58Z,00a260effab802edc8f72 jitterAlertRulesWithinGroups,2024-01-18T18:48:11Z,,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver onPremToCloudMigrations,2024-01-22T16:09:08Z,,cf13cb9f70c2230f17450667ce59440304fb023c,Michael Mandrus alertingSaveStatePeriodic,2024-01-23T16:03:30Z,,aa25776f813926cb4f1947d4ae5a014f4e7728ff,Jean-Philippe Quéméner -promQLScope,2024-01-29T20:22:17Z,,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt +promQLScope,2024-01-29T20:22:17Z,2025-10-10T14:53:18Z,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt slateAutocomplete,2024-01-31T10:01:20Z,2024-02-01T13:03:11Z,39057552dc2ddfe8ee770c67a71cf55159aca741,Ashley Harrison nodeGraphDotLayout,2024-01-31T16:26:12Z,2025-04-08T14:37:17Z,cb945aa5df3453ab2a851ef7ec8bee244e3eeac4,Andrej Ocenas kubernetesQueryServiceRewrite,2024-01-31T18:36:51Z,2024-04-19T09:26:21Z,e013cd427cb0457177e11f19ebd30bc523b36c76,Ryan McKinley influxdbRunQueriesInParallel,2024-02-01T10:58:24Z,,536153c3362782ae7f79e06755e8c7e0cd3b3acd,ismail simsek -groupToNestedTableTransformation,2024-02-07T14:28:26Z,,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham +groupToNestedTableTransformation,2024-02-07T14:28:26Z,2025-11-17T11:57:22Z,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham newPDFRendering,2024-02-08T12:09:34Z,,28e66b4ad82ecebb551374325f0be4332412341c,Agnès Toulet autoMigrateGraphPanel,2024-02-08T22:00:48Z,2025-04-04T09:31:35Z,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs dashboardSceneSolo,2024-02-11T08:08:47Z,,fe6d1460b09b403fc74fd20e95f61513eede2555,Torkel Ödegaard @@ -290,7 +291,7 @@ autoMigrateTablePanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs groupByVariable,2024-02-14T17:18:04Z,,f016f95298fe490a865612864520f3622f8e804a,Dominik Prokop alertingUpgradeDryrunOnStart,2024-02-16T16:29:54Z,2024-03-14T14:36:35Z,dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9,Matthew Jacobson -expressionParser,2024-02-17T00:59:11Z,2025-08-26T13:21:24Z,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley +expressionParser,2024-02-17T00:59:11Z,2025-07-31T22:56:50Z,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley sqlExpressions,2024-02-27T21:16:00Z,,70009201d44c2d0ab39cc77081808a69a6c4fd63,Scott Lepper aiGeneratedDashboardChanges,2024-03-05T12:01:31Z,,a7c06d26f14b2a9fa8faa929a6c9a0c355018429,Ivan Ortega Alba scopeFilters,2024-03-05T15:41:19Z,,b3efb4217e48656f24aacdffd5737595d7361afe,Carl Bergquist @@ -303,7 +304,7 @@ usePrometheusFrontendPackage,2024-03-23T00:47:53Z,2024-04-15T21:45:23Z,d08459521 oauthRequireSubClaim,2024-03-25T13:22:24Z,,2f3a01f79fda7f6c465dd64adf5cf5c7227f9d19,Karl Persson authAPIAccessTokenAuth,2024-04-02T15:45:15Z,2025-02-04T15:31:24Z,5340a6e548b1e5fcd3af66695ca34d7e943fd068,Jo newDashboardWithFiltersAndGroupBy,2024-04-04T11:25:21Z,,32b6ef9d153dc7def2c17b81b02cfb1c412eb315,Dominik Prokop -prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,2025-08-27T13:11:58Z,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond +prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,2025-07-31T22:56:50Z,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond cloudWatchNewLabelParsing,2024-04-05T15:57:56Z,,58f32150c262605d188874b88f44a7dece4b9388,Isabella Siu exploreMetrics,2024-04-09T18:15:18Z,2025-04-11T20:45:14Z,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek accessActionSets,2024-04-12T16:19:25Z,2025-03-13T15:18:23Z,56f4664875047d6861ea3facbc94cd921e263950,Ieva @@ -314,8 +315,8 @@ queryServiceRewrite,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a8369 grafanaManagedRecordingRules,2024-04-22T17:53:16Z,2025-05-19T10:15:49Z,c32953e52cf9fd46a462711bc23fde15a5c3b6bf,Alexander Weaver logsExploreTableDefaultVisualization,2024-05-02T15:28:15Z,,840aeddbd1957117d9b62074c155e87bb4afd4b4,Galen Kistler autofixDSUID,2024-05-03T11:32:07Z,2024-06-20T10:56:39Z,b6f899d953a0924760dc5fd5a3d40669c86d475d,Andres Martinez Gotor -newDashboardSharingComponent,2024-05-03T15:02:18Z,,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas -tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev +newDashboardSharingComponent,2024-05-03T15:02:18Z,2025-08-29T14:46:39Z,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas +tlsMemcached,2024-05-09T19:12:08Z,2025-11-12T15:49:28Z,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev notificationBanner,2024-05-13T09:32:34Z,2025-01-10T10:18:43Z,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R @@ -329,7 +330,7 @@ alertingCentralAlertHistory,2024-05-29T15:01:38Z,,289ce6185574df99acea37b86c2e08 pluginProxyPreserveTrailingSlash,2024-06-05T11:36:14Z,,fe3e5917f1bc83ab29b6a57578316e054718aa4a,Marcus Efraimsson kubernetesDashboards,2024-06-05T14:34:23Z,,41e0430f83bf7db50c4caaa1472afa3bf3d5c2dc,Ryan McKinley azureMonitorPrometheusExemplars,2024-06-06T16:53:17Z,,c9778c3332aa93e5dd8bc3b894264e1955f0d593,Andreas Christou -pinNavItems,2024-06-10T11:40:03Z,,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández +pinNavItems,2024-06-10T11:40:03Z,2025-11-17T12:12:47Z,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández authZGRPCServer,2024-06-13T09:41:35Z,,afcb5a855c26e985e43861bff6fab36b1b008109,Gabriel MABILLE openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,2025-02-03T13:24:49Z,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk ssoSettingsLDAP,2024-06-18T11:31:27Z,,d074cc7892b96a1333bd07011baff146ea71e21d,Mihai Doarna @@ -388,7 +389,7 @@ enableExtensionsAdminPage,2024-11-05T15:55:10Z,,c7a7f7dce5d93dc5044c4be140433ee2 exploreMetricsRelatedLogs,2024-11-05T16:28:43Z,,82ac9e2bb67a665c3b56e3a80c47d8aea7f7d044,ismail simsek enableScopesInMetricsExplore,2024-11-06T13:11:33Z,,a80517d6fecf245cb9366b5662ab6d8760200389,Bogdan Matei zipkinBackendMigration,2024-11-07T09:35:53Z,2025-01-28T14:11:10Z,c151021b162d99da2cb0f09485f90a2b5d38c11c,Ivana Huckova -preinstallAutoUpdate,2024-11-07T12:14:25Z,,a415c0b83184ad473b3f8e10c0319c46d9e5c515,Andres Martinez Gotor +preinstallAutoUpdate,2024-11-07T12:14:25Z,2025-11-10T14:06:30Z,a415c0b83184ad473b3f8e10c0319c46d9e5c515,Andres Martinez Gotor enableSCIM,2024-11-07T14:38:46Z,,3d51ca7377eff46b39750a8e0311ead7b75fd359,linoman logQLScope,2024-11-11T11:53:24Z,,c7b6822a5e9cba703e521536947682aaf801203e,Carl Bergquist userStorageAPI,2024-11-12T11:56:41Z,2025-03-27T12:40:00Z,c3494614e39638f6d78b79397cf6032cbcde4b9f,Andres Martinez Gotor @@ -417,7 +418,7 @@ k8SFolderCounts,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,L k8SFolderMove,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira kubernetesRestore,2025-01-03T14:48:47Z,2025-03-20T21:38:32Z,5429512779bd5f25b88ff728ea91efdef7dfafa0,Stephanie Hingtgen improvedExternalSessionHandlingSAML,2025-01-09T17:02:49Z,,c52ec21c75ab72c2f7d28259bac0364edae560d0,Misi -teamHttpHeadersMimir,2025-01-13T10:42:47Z,2025-08-07T09:04:46Z,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck +teamHttpHeadersMimir,2025-01-13T10:42:47Z,2025-07-31T22:56:50Z,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck ABTestFeatureToggleA,2025-01-13T21:13:13Z,2025-05-27T19:18:23Z,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs ABTestFeatureToggleB,2025-01-13T21:13:13Z,2025-05-27T19:18:23Z,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs kubernetesFoldersServiceV2,2025-01-13T21:15:35Z,2025-02-18T23:11:26Z,766d645d827f5e6e0872ae30e5fe23226ae85785,maicon @@ -425,10 +426,10 @@ queryLibraryDashboards,2025-01-14T11:01:15Z,2025-02-14T16:39:22Z,740cd22fe51a354 elasticsearchImprovedParsing,2025-01-15T17:05:54Z,,bab55a4cb84f2ba57838f96a492ab9aa7f307957,Adam Yeats grafanaAdvisor,2025-01-20T10:08:00Z,,c1364d6be6f552203ba786f17a89664304b89247,Andres Martinez Gotor datasourceConnectionsTab,2025-01-21T17:39:48Z,,97d8f68b705f9949493079d1833abfe80e7b48f3,Syerikjan Kh -unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,2025-08-06T08:04:32Z,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood +unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,2025-07-31T22:56:50Z,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood alertingSaveStateCompressed,2025-01-27T17:47:33Z,,cb43f4b6962fca18655b3ba634adeb4d59dc89df,Alexander Akhmetov fetchRulesUsingPost,2025-01-29T12:17:44Z,,1444051b65af0de6c412a12132083135c7730414,Fayzal Ghantiwala -templateVariablesUsesCombobox,2025-01-31T09:53:13Z,,7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7,Tobias Skarhed +templateVariablesUsesCombobox,2025-01-31T09:53:13Z,2025-11-13T03:31:18Z,7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7,Tobias Skarhed alertingAlertmanagerExtraDedupStage,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan alertingAlertmanagerExtraDedupStageStopPipeline,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan exploreMetricsUseExternalAppPlugin,2025-02-03T20:46:54Z,2025-04-11T20:45:14Z,29fa6dfc8de0a758f9f37f000c923f0806c3b629,Nick Richmond @@ -445,24 +446,24 @@ rendererDisableAppPluginsPreload,2025-02-24T14:43:06Z,,608d974585c696253ac629f3c assetSriChecks,2025-03-04T10:56:35Z,,bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325,Jack Westbrook alertRuleRestore,2025-03-05T14:15:26Z,,374380d1f6ab05e98bc00b0b0fdaa076d1d30ffa,Yuri Tseretyan grafanaManagedRecordingRulesDatasources,2025-03-07T13:30:40Z,2025-06-02T08:56:05Z,14ebec527ccf22c15321ac8394a4f9c3909dd4e1,Steve Simpson -inviteUserExperimental,2025-03-07T19:09:59Z,,5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45,Juan Cabanas +inviteUserExperimental,2025-03-07T19:09:59Z,2025-11-14T15:33:26Z,5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45,Juan Cabanas extraLanguages,2025-03-11T10:07:16Z,2025-03-27T12:58:52Z,210c886bb7b913e4ca0b77ff142a72185a82a385,Josh Hunt infinityRunQueriesInParallel,2025-03-14T12:54:04Z,,d72b6649810ffacebe73ef67c6b5bd6e46dfab47,ismail simsek noBackdropBlur,2025-03-14T15:21:35Z,2025-04-08T10:58:19Z,bf172dfd296d15c02473904646c19d2bb663cc3f,Josh Hunt alertingMigrationUI,2025-03-14T16:40:05Z,,ef9dca9ea369997116cd7880c238597fc18ca682,Tom Ratcliffe -unifiedStorageHistoryPruner,2025-03-17T10:36:38Z,,1700a8aa9f2a103f953a385544c2ce200e6fcf9d,Jean-Philippe Quéméner +unifiedStorageHistoryPruner,2025-03-17T10:36:38Z,2025-11-17T19:47:37Z,1700a8aa9f2a103f953a385544c2ce200e6fcf9d,Jean-Philippe Quéméner secretsManagementAppPlatform,2025-03-19T09:25:14Z,,ac4b2a320071323f0d2849b844f7e8c3afbff21d,Matheus Macabu unifiedStorageGrpcConnectionPool,2025-03-21T13:24:54Z,,ba3e8014b3363762369c434c0c67d871a311a033,Jean-Philippe Quéméner -tableNextGen,2025-03-26T03:57:57Z,2025-08-26T21:25:16Z,03d6d8f854ac06701bba2d5d10bedd8a227bea4e,Drew Slobodnjak +tableNextGen,2025-03-26T03:57:57Z,2025-07-31T22:56:50Z,03d6d8f854ac06701bba2d5d10bedd8a227bea4e,Drew Slobodnjak alertingRuleRecoverDeleted,2025-03-27T14:39:26Z,,f9471ac10b65e08343b08d82e1209f1ae7006955,Sonia Aguilar -localizationForPlugins,2025-03-31T04:38:38Z,,18ae5d7f0c599bf417dc68a67fa6852b6cf54600,Hugo Häggmark +localizationForPlugins,2025-03-31T04:38:38Z,2025-08-29T14:46:39Z,18ae5d7f0c599bf417dc68a67fa6852b6cf54600,Hugo Häggmark localeFormatPreference,2025-03-31T13:59:07Z,,4ad0492d3dc8ee81b388384e3e6c329746f46df7,Laura Fernández useScopesNavigationEndpoint,2025-03-31T15:20:00Z,,c321afdeb710db302558ac63a8f0569c4706d003,Tobias Skarhed xrayApplicationSignals,2025-04-01T14:42:02Z,2025-06-13T18:26:14Z,1aea65f6d5aae0ea9d54a6d943b27e1004f4ef85,Isabella Siu queryServiceFromExplore,2025-04-02T10:00:33Z,,135fbf6258d85382a3c80b696b4e410fd6efe809,Gábor Farkas azureMonitorLogsBuilderEditor,2025-04-02T14:15:25Z,,3b73ebb21096f69f4dcb287896e2cbfec65e48d7,Alyssa (Bull) Joyner multiTenantTempCredentials,2025-04-02T20:25:50Z,,6a699b69bace2aecfe3631a5e050cda69180b97f,Isabella Siu -extensionSidebar,2025-04-03T10:16:35Z,2025-09-01T10:14:17Z,f27790268254294be7c09832e9a73676fd9a629c,Sven Grossmann +extensionSidebar,2025-04-03T10:16:35Z,2025-07-31T22:56:50Z,f27790268254294be7c09832e9a73676fd9a629c,Sven Grossmann alertingRulePermanentlyDelete,2025-04-03T11:18:25Z,,3450d243b95acd5bf86fbbf3b183cba4b2eb8301,Sonia Aguilar logsPanelControls,2025-04-07T14:38:55Z,,e2a6f9a84928cfb2f958522b1f50fdf3051f51de,Matias Chomicki metricsFromProfiles,2025-04-09T10:55:28Z,,ceed8243784500557f2686bb478695cc125a0e19,Piotr Jamróz @@ -475,14 +476,14 @@ pluginsAutoUpdate,2025-04-16T11:44:39Z,,c947732e0de99395a6ee4e0161e27c3e884168ac alertingListViewV2PreviewToggle,2025-04-22T08:50:34Z,,512df0091a1df6c7d19243e288ec458250b49813,Konrad Lalik alertRuleUseFiredAtForStartsAt,2025-04-22T11:16:38Z,,3a054d5e00abc212a741fc1aad8cf266a188de1f,Fayzal Ghantiwala alertingBulkActionsInUI,2025-04-24T14:49:59Z,,674fdd1d323ef041dbed93b44ae58afaaf1a8b64,Sonia Aguilar -multiTenantFrontend,2025-04-25T09:24:25Z,,7b492d7e1610da99460cb121ec542c5b755fe6d1,Ryan McKinley +multiTenantFrontend,2025-04-25T09:24:25Z,2025-07-31T22:56:50Z,7b492d7e1610da99460cb121ec542c5b755fe6d1,Ryan McKinley extensionsReadOnlyProxy,2025-05-06T04:55:23Z,2025-07-01T04:10:57Z,bcb2a7e36f7ae5190ab45669f4a0d04660313842,Levente Balogh kubernetesAggregatorCapTokenAuth,2025-05-15T18:14:23Z,,aa2cf8e398ee05353cc488ca22e3c8ee65dd5c53,Charandas alertingImportYAMLUI,2025-05-21T15:59:41Z,,fc5472615fc8b823f1c18c4eae2b6ce3ddcc76f9,Sonia Aguilar teamHttpHeadersTempo,2025-05-22T19:13:31Z,,249e2f3d34f5175e62a17629646994d3116e64f6,Cory Forseth restoreDashboards,2025-05-23T14:35:54Z,,0cb6f9584bf77a7dbdfcc6ce1032db2b4bdc15e2,Alex Khomenko postgresDSUsePGX,2025-05-26T06:54:18Z,2025-06-03T12:45:07Z,1e383b0c1e98734ad4bf974f0435512d10c33246,Zoltán Bedi -skipTokenRotationIfRecent,2025-06-03T06:59:40Z,,86f2bf294044129e802ddeab6f4ac8e9f79dee9e,xavi +skipTokenRotationIfRecent,2025-06-03T06:59:40Z,2025-10-23T08:02:41Z,86f2bf294044129e802ddeab6f4ac8e9f79dee9e,xavi alertEnrichment,2025-06-06T12:16:07Z,,f81031f945b55b644a3ad6a6bcf99d445beb4ad4,Steve Simpson alertingImportAlertmanagerAPI,2025-06-10T08:32:50Z,,f14ed750f53899d72153defc6c787200c22816ca,Alexander Akhmetov preferLibraryPanelTitle,2025-06-17T11:21:21Z,,e90134bb6f2a9de0ce520f4a00427de2d80c6667,Oscar Kilhed @@ -494,42 +495,93 @@ kubernetesLibraryPanels,2025-06-25T22:21:56Z,,79fe8a9902335c7a28af30e467b904a4cc enableAppChromeExtensions,2025-06-30T04:32:08Z,,15293a2ceb083108c0f15490933db523aa915c7d,Hugo Häggmark foldersAppPlatformAPI,2025-07-03T14:15:23Z,,e76f470b444499f70825e9e5eb6b1775ff086c3c,Andrej Ocenas tempoAlerting,2025-07-15T13:36:36Z,,68b9a5f57c30976a1356a0bdb381821e2643384f,Piotr Jamróz -provisioningSecretsService,2025-07-15T13:43:17Z,2025-08-22T16:38:28Z,d39a47a89b9bfc2940f27ca8469e576a659bfc63,Stephanie Hingtgen +provisioningSecretsService,2025-07-15T13:43:17Z,2025-07-31T22:56:50Z,d39a47a89b9bfc2940f27ca8469e576a659bfc63,Stephanie Hingtgen sharingDashboardImage,2025-07-15T21:07:39Z,,b691b3288d5d18366e405ad4e881e7e9d9d6de96,Nathan Marrs -enablePluginImporter,2025-07-16T04:42:28Z,,5b82e056972f959908cd42c9934ba56e8adbf143,Hugo Häggmark +enablePluginImporter,2025-07-16T04:42:28Z,2025-10-23T04:18:23Z,5b82e056972f959908cd42c9934ba56e8adbf143,Hugo Häggmark otelLogsFormatting,2025-07-16T15:42:14Z,,974103c6fa20701c5467c67fb24f173aea9113fb,Matias Chomicki alertingAIAnalyzeCentralStateHistory,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIGenAlertRules,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIGenTemplates,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIImproveAlertRules,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingNotificationHistory,2025-07-17T13:26:26Z,,bccc980b902987fc9a2707deca766412b47bbe2a,Vadim Stepanov -pluginAssetProvider,2025-07-17T15:20:35Z,,f6ed9e6ff0e799f3e69532809a7e444efb3c610e,Will Browne +pluginAssetProvider,2025-07-17T15:20:35Z,2025-10-10T09:35:22Z,f6ed9e6ff0e799f3e69532809a7e444efb3c610e,Will Browne unifiedStorageSearchDualReaderEnabled,2025-07-18T12:43:56Z,,2dba473015a6e7e75a5d117bf766a6c24d404d38,maicon kubernetesLibraryPanelConnections,2025-07-21T12:53:46Z,2025-07-30T16:46:19Z,5ec3a2b758dfd0cc63cc42759d6f01a88dfbb6bc,Stephanie Hingtgen -dashboardDsAdHocFiltering,2025-07-23T08:12:25Z,,aedd7b6e3408a04a84ef02c535439fd17aa2e0e6,Sam Jewell +dashboardDsAdHocFiltering,2025-07-23T08:12:25Z,2025-11-10T17:17:49Z,aedd7b6e3408a04a84ef02c535439fd17aa2e0e6,Sam Jewell alertingAIFeedback,2025-07-23T12:38:09Z,,7c872f0e8aa6d05061f546da98d94fe223192fa2,Sonia Aguilar alertingProvenanceLockWrites,2025-07-23T18:16:06Z,,7c43c061a827e4802f40d74f451a15a24db6abd6,Alexander Akhmetov sqlExpressionsColumnAutoComplete,2025-07-23T21:49:58Z,,5bfed408edcc6f74dd8b0780b4f87bccfea02b01,Kristina timeComparison,2025-07-24T20:07:28Z,,219672722662938633ffae3f6e8bfa89f8f29661,Drew Slobodnjak kubernetesAuthnMutation,2025-07-25T15:05:32Z,,5f6fc38430494e3cb642a09e32415ae62a92ad56,Victor Cinaglia alertmanagerRemoteSecondaryWithRemoteState,2025-07-25T15:06:59Z,,dcb965b7dcf1ec47d0847968fa362d2b308328f8,Santiago -adhocFiltersInTooltips,2025-07-29T17:53:43Z,,2174a84b36b9a086018972654c5ec661c19937a1,Sam Jewell +adhocFiltersInTooltips,2025-07-29T17:53:43Z,2025-11-12T10:05:30Z,2174a84b36b9a086018972654c5ec661c19937a1,Sam Jewell scanRowInvalidDashboardParseFallbackEnabled,2025-07-30T14:18:38Z,,98e37f2ca9b4fa1a6cac0c3a2ae355a5ff60dbd9,Mustafa Sencer Özcan dashboardLevelTimeMacros,2025-07-31T09:49:07Z,,e7cfe0c0237394f6c87d01ad4a5e6f29954212fb,Oscar Kilhed useScopeSingleNodeEndpoint,2025-07-31T14:32:41Z,,972e2f31e5f3dfd8d9f8152b3bb1fe7224e5d1e1,Tobias Skarhed -newLogContext,2025-08-01T11:30:17Z,,3f90c85c4eea9657bac14b49c925ba32d208c54e,Matias Chomicki -kubernetesShortURLs,2025-08-04T12:12:12Z,,e88b54e9d3412508342809fd0bb85f836cd50d73,Ezequiel Victorero -newClickhouseConfigPageDesign,2025-08-05T13:37:28Z,,23b801470889c9c99a02f32e819b4a45d5d67189,Alyssa Joyner -favoriteDatasources,2025-08-08T13:28:17Z,,463e544db9812ef6bf02dfe764585b21a00c172c,Andres Martinez Gotor -kubernetesAuthzResourcePermissionApis,2025-08-11T08:54:36Z,,58c4305d64a8e1d33d4f10f0cb2e4e2c318c2fac,Ieva -unifiedStorageSearchAfterWriteExperimentalAPI,2025-08-13T14:05:15Z,,b9b34223a7e8cd40a496c0d4a2c8386f46136ca7,Will Assis -alertingImportAlertmanagerUI,2025-08-13T15:28:43Z,,587f52cf5b8c480cfe63490a1e77907faa1a263a,Alexander Akhmetov -teamFolders,2025-08-13T16:41:00Z,,5564f699ca45fcbffcf67ce2575ff40a34e263cd,Tom Ratcliffe -grafanaAssistantInProfilesDrilldown,2025-08-19T07:54:00Z,,bbf01a638345e42c0f48dfcc743efbfbb7fedaeb,Piotr Jamróz -savedQueries,2025-08-25T21:22:09Z,,649e9aa8ca9e8f7f16e9198b3858aaf12714a6a8,Ezequiel Victorero -alertingEnrichmentPerRule,2025-08-28T08:30:28Z,,98bd10965b4be99d1b1306a2ddf87b1099df74dd,Sonia Aguilar +alertEnrichmentConditional,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertEnrichmentMultiStep,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingEnrichmentPerRule,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingImportAlertmanagerUI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingTriage,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +azureResourcePickerUpdates,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +dskitBackgroundServices,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +favoriteDatasources,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +grafanaAssistantInProfilesDrilldown,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +graphiteBackendMode,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesAlertingRules,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesAuthzResourcePermissionApis,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesShortURLs,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +newClickhouseConfigPageDesign,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +newLogContext,2025-07-31T22:56:50Z,,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +pluginContainers,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +prometheusTypeMigration,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +restrictedPluginApis,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +savedQueries,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +secretsManagementAppPlatformUI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +teamFolders,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +unifiedStorageSearchAfterWriteExperimentalAPI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +vizActionsAuth,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou queryServiceWithConnections,2025-08-28T19:28:26Z,2025-08-29T12:49:57Z,eda94a6434efc84862f9907c40463c02460c400d,Ryan McKinley -alertingTriage,2025-09-01T09:33:33Z,,31114fb47ced7afbd559afe00a49f40914cd7acb,Konrad Lalik -restrictedPluginApis,2025-09-01T09:57:00Z,,d31e682345c5a4a7b3e055a481996d406329e1fa,Levente Balogh -graphiteBackendMode,2025-09-01T15:13:47Z,,0dc283b303a4b0b992e39b9d72c71f1f3a1ea597,Andreas Christou -azureResourcePickerUpdates,2025-09-02T10:02:01Z,,1a8d25375a6cd8adc3f64558b08ca9fd2ae9782d,Andreas Christou +alertingEnrichmentAssistantInvestigations,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +alertingUseNewSimplifiedRoutingHashAlgorithm,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +cdnPluginsLoadFirst,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +cdnPluginsUrls,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +dashboardLibrary,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +dashboardUndoRedo,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +filterOutBotsFromFrontendLogs,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesAuthZHandlerRedirect,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesCorrelations,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesStars,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +queryCacheRequestDeduplication,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +starsFromAPIServer,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +tempoSearchBackendMigration,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +unifiedStorageUseFullNgram,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +useKubernetesShortURLsAPI,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +useMultipleScopeNodesEndpoint,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +alertingGenerateSimplifiedRoutingWithOldHashes,2025-10-01T16:45:36Z,2025-10-01T19:21:33Z,0da9d4989652218d73ea9aa176ed300dba9aa884,Alexander Akhmetov +unlimitedLayoutsNesting,2025-10-10T12:15:54Z,,b5613b37249f42e18ef62f987202ba43858e5992,Sergej-Vlasov +enableDashboardEmptyExtensions,2025-10-13T07:03:13Z,,85174e3313952f522e1325b17a4e34c898d8617e,Matt Cowley +kubernetesAuthzZanzanaSync,2025-10-13T19:37:13Z,,0e341643292d903bc5fc882aa3332966284958ff,Gabriel MABILLE +grafanaPathfinder,2025-10-14T10:40:40Z,2025-10-22T08:06:21Z,c9f402c764d41a7ff85ef26979f8b4fcbe1b4b81,Jay Clifford +kubernetesLogsDrilldown,2025-10-16T21:31:42Z,,b3f9dad044f65479f99bb174af5725a8863ec915,Liza Detrick +preventPanelChromeOverflow,2025-10-17T14:40:08Z,,4cf11b721aeade73b37551382f5202ecef94d49a,Ashley Harrison +pluginStoreServiceLoading,2025-10-17T20:01:43Z,,69628baa9ddde2851dcccfe6c3c7d8c3e8b3197b,Todd Treece +onlyStoreActionSets,2025-10-20T15:02:56Z,,0a0311a2b230991a55d5b2d393d1cef0326e26f9,Ieva +kubernetesQueryCaching,2025-10-20T16:11:25Z,,17771e0e1df9d70ef8a2f90a7468a1652c462aa1,Lucy Chen +newGauge,2025-10-20T16:33:19Z,,7df537c9fc84ad6d277cee249e87db08bc6a81b7,Torkel Ödegaard +zanzanaNoLegacyClient,2025-10-21T14:03:17Z,,adf1224e82d83277e169646db477462a9a4db8e2,Alexander Zobnin +interactiveLearning,2025-10-22T08:06:21Z,,1abb5aa0f9c77d18775377a7459c091a9adddaa3,Jack Baldry +pluginInstallAPISync,2025-10-24T12:09:26Z,,dc77da11cf4cb3c4e705477cf2fb97e6004daf2f,Todd Treece +dashboardTemplates,2025-10-28T20:05:32Z,,8263803e81f26bdce676c5676b7cc4838cdf9042,Nathan Marrs +panelTimeSettings,2025-10-29T08:06:23Z,,5a031b370f4eeb873ed305c11a0235017fd54307,Torkel Ödegaard +jaegerEnableGrpcEndpoint,2025-10-31T18:19:16Z,,d0ea82633f757dcaa2562e94b2de0b40f9c0e229,Jocelyn Collado-Kuri +timeRangePan,2025-11-05T01:39:46Z,,e067b1de98f564045dfbb726546ef06957eeb3a6,Jesse David Peterson +kubernetesAnnotations,2025-11-06T18:22:20Z,,95ea7584752ddeb2aa224d2d8d9aba2de81f5aa3,Serge Zaitsev +suggestedDashboards,2025-11-07T13:38:59Z,,e5ed003fb219d4b8ffa2c2a44abda4e6ff54d91a,Alexa Vargas +grafanaAdvisorAppInstaller,2025-11-12T14:32:21Z,2025-11-14T11:25:30Z,d83c35fd71f435dc0fdc83b6af43dd7bc3682d22,Andres Martinez Gotor +newPanelPadding,2025-11-12T15:40:46Z,,1f558b1e066ed124c408ed52e95ce60bccfc135b,Torkel Ödegaard +awsDatasourcesHttpProxy,2025-11-12T18:51:23Z,,ec9f39d54a0489ca36a9059ad1b31ab7a58ca20a,Isabella Siu +newVizSuggestions,2025-11-12T19:26:29Z,,a194219365400e6dd5929453b17d212d1efd386c,Adela Almasan +alertingUIUseBackendFilters,2025-11-13T14:52:14Z,,44a92d252b18a571f5858aa7a7519f365d89ad7b,Alexander Akhmetov +transformationsEmptyPlaceholder,2025-11-17T13:57:05Z,,c4f2f3f6f4038a7428157a3629b428c340c08b52,Natalia Bernarte Oses +ttlPluginInstanceManager,2025-11-18T11:17:23Z,,e00eb854f54c972286a39d8ed101f1a423084f06,Will Browne diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8a874135d00..24f9769ea4f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -28,7 +28,7 @@ unifiedRequestLog,GA,@grafana/grafana-backend-group,false,false,false renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true -enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true +enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,false extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false @@ -51,7 +51,7 @@ enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,f disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false -kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,true +kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,false kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false @@ -78,7 +78,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true -dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true +dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true @@ -119,7 +119,7 @@ logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,fa alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false -alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true +alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,false pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false azureMonitorPrometheusExemplars,GA,@grafana/partner-datasources,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false @@ -200,14 +200,14 @@ azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,f localeFormatPreference,preview,@grafana/grafana-frontend-platform,false,false,false unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false alertingRulePermanentlyDelete,GA,@grafana/alerting-squad,false,false,true -alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true +alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,false multiTenantTempCredentials,experimental,@grafana/aws-datasources,false,false,false unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true grafanaAssistantInProfilesDrilldown,GA,@grafana/observability-traces-and-profiling,false,false,true postgresDSUsePGX,experimental,@grafana/oss-big-tent,false,false,false -tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,true +tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,false pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false @@ -241,7 +241,7 @@ newLogContext,experimental,@grafana/observability-logs,false,false,true newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,false interactiveLearning,preview,@grafana/pathfinder,false,false,false -alertingTriage,experimental,@grafana/alerting-squad,false,false,true +alertingTriage,experimental,@grafana/alerting-squad,false,false,false graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false azureResourcePickerUpdates,preview,@grafana/partner-datasources,false,false,true prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f6c70624e8d..fdc48861894 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -19,10 +19,6 @@ const ( // Enables public dashboard sharing to be restricted to only allowed emails FlagPublicDashboardsEmailSharing = "publicDashboardsEmailSharing" - // FlagPublicDashboardsScene - // Enables public dashboard rendering using scenes - FlagPublicDashboardsScene = "publicDashboardsScene" - // FlagLokiExperimentalStreaming // Support new streaming approach for loki (prototype, needs special loki build) FlagLokiExperimentalStreaming = "lokiExperimentalStreaming" @@ -35,10 +31,6 @@ const ( // Configurable storage for dashboards, datasources, and resources FlagStorage = "storage" - // FlagCanvasPanelNesting - // Allow elements nesting - FlagCanvasPanelNesting = "canvasPanelNesting" - // FlagLogRequestsInstrumentedAsUnknown // Logs the path for requests that are instrumented as unknown FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown" @@ -63,30 +55,10 @@ const ( // Rule backtesting API for alerting FlagAlertingBacktesting = "alertingBacktesting" - // FlagLogsContextDatasourceUi - // Allow datasource to provide custom UI for context view - FlagLogsContextDatasourceUi = "logsContextDatasourceUi" - - // FlagLokiShardSplitting - // Use stream shards to split queries into smaller subqueries - FlagLokiShardSplitting = "lokiShardSplitting" - - // FlagLokiQuerySplitting - // Split large interval queries into subqueries with smaller time intervals - FlagLokiQuerySplitting = "lokiQuerySplitting" - // FlagIndividualCookiePreferences // Support overriding cookie preferences per user FlagIndividualCookiePreferences = "individualCookiePreferences" - // FlagInfluxdbBackendMigration - // Query InfluxDB InfluxQL without the proxy - FlagInfluxdbBackendMigration = "influxdbBackendMigration" - - // FlagStarsFromAPIServer - // populate star status from apiserver - FlagStarsFromAPIServer = "starsFromAPIServer" - // FlagKubernetesStars // Routes stars requests from /api to the /apis endpoint FlagKubernetesStars = "kubernetesStars" @@ -119,22 +91,10 @@ const ( // Refactor time range variables flow to reduce number of API calls made when query variables are chained FlagRefactorVariablesTimeRange = "refactorVariablesTimeRange" - // FlagFaroDatasourceSelector - // Enable the data source selector within the Frontend Apps section of the Frontend Observability - FlagFaroDatasourceSelector = "faroDatasourceSelector" - // FlagEnableDatagridEditing // Enables the edit functionality in the datagrid panel FlagEnableDatagridEditing = "enableDatagridEditing" - // FlagExtraThemes - // Enables extra themes - FlagExtraThemes = "extraThemes" - - // FlagLogsExploreTableVisualisation - // A table visualisation for logs in Explore - FlagLogsExploreTableVisualisation = "logsExploreTableVisualisation" - // FlagAwsDatasourcesTempCredentials // Support temporary security credentials in AWS plugins for Grafana Cloud customers FlagAwsDatasourcesTempCredentials = "awsDatasourcesTempCredentials" @@ -175,14 +135,6 @@ const ( // Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval FlagConfigurableSchedulerTick = "configurableSchedulerTick" - // FlagDashgpt - // Enable AI powered features in dashboards - FlagDashgpt = "dashgpt" - - // FlagAiGeneratedDashboardChanges - // Enable AI powered features for dashboards to auto-summary changes when saving - FlagAiGeneratedDashboardChanges = "aiGeneratedDashboardChanges" - // FlagReportingRetries // Enables rendering retries for the reporting feature FlagReportingRetries = "reportingRetries" @@ -223,10 +175,6 @@ const ( // Enables k8s short url api and uses it under the hood when handling legacy /api FlagKubernetesShortURLs = "kubernetesShortURLs" - // FlagUseKubernetesShortURLsAPI - // Routes short url requests from /api to the /apis endpoint in the frontend. Depends on kubernetesShortURLs - FlagUseKubernetesShortURLsAPI = "useKubernetesShortURLsAPI" - // FlagKubernetesAlertingRules // Adds support for Kubernetes alerting and recording rules FlagKubernetesAlertingRules = "kubernetesAlertingRules" @@ -275,14 +223,6 @@ const ( // Rewrite requests targeting /ds/query to the query service FlagQueryServiceRewrite = "queryServiceRewrite" - // FlagQueryServiceFromUI - // Routes requests to the new query service - FlagQueryServiceFromUI = "queryServiceFromUI" - - // FlagQueryServiceFromExplore - // Routes explore requests to the new query service - FlagQueryServiceFromExplore = "queryServiceFromExplore" - // FlagCloudWatchBatchQueries // Runs CloudWatch metrics queries as separate batches FlagCloudWatchBatchQueries = "cloudWatchBatchQueries" @@ -311,58 +251,14 @@ const ( // Change the way annotation permissions work by scoping them to folders and dashboards. FlagAnnotationPermissionUpdate = "annotationPermissionUpdate" - // FlagDashboardSceneForViewers - // Enables dashboard rendering using Scenes for viewer roles - FlagDashboardSceneForViewers = "dashboardSceneForViewers" - - // FlagDashboardSceneSolo - // Enables rendering dashboards using scenes for solo panels - FlagDashboardSceneSolo = "dashboardSceneSolo" - - // FlagDashboardScene - // Enables dashboard rendering using scenes for all roles - FlagDashboardScene = "dashboardScene" - // FlagDashboardNewLayouts // Enables experimental new dashboard layouts FlagDashboardNewLayouts = "dashboardNewLayouts" - // FlagDashboardUndoRedo - // Enables undo/redo in dynamic dashboards - FlagDashboardUndoRedo = "dashboardUndoRedo" - - // FlagUnlimitedLayoutsNesting - // Enables unlimited dashboard panel grouping - FlagUnlimitedLayoutsNesting = "unlimitedLayoutsNesting" - - // FlagPanelFilterVariable - // Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard - FlagPanelFilterVariable = "panelFilterVariable" - // FlagPdfTables // Enables generating table data as PDF in reporting FlagPdfTables = "pdfTables" - // FlagCanvasPanelPanZoom - // Allow pan and zoom in canvas panel - FlagCanvasPanelPanZoom = "canvasPanelPanZoom" - - // FlagTimeComparison - // Enables time comparison option in supported panels - FlagTimeComparison = "timeComparison" - - // FlagLogsInfiniteScrolling - // Enables infinite scrolling for the Logs panel in Explore and Dashboards - FlagLogsInfiniteScrolling = "logsInfiniteScrolling" - - // FlagTableSharedCrosshair - // Enables shared crosshair in table panel - FlagTableSharedCrosshair = "tableSharedCrosshair" - - // FlagKubernetesFeatureToggles - // Use the kubernetes API for feature toggle management in the frontend - FlagKubernetesFeatureToggles = "kubernetesFeatureToggles" - // FlagCloudRBACRoles // Enabled grafana cloud specific RBAC roles FlagCloudRBACRoles = "cloudRBACRoles" @@ -399,14 +295,6 @@ const ( // In-development feature flag for the scope api using the app platform. FlagScopeApi = "scopeApi" - // FlagUseScopeSingleNodeEndpoint - // Use the single node endpoint for the scope api. This is used to fetch the scope parent node. - FlagUseScopeSingleNodeEndpoint = "useScopeSingleNodeEndpoint" - - // FlagUseMultipleScopeNodesEndpoint - // Makes the frontend use the 'names' param for fetching multiple scope nodes at once - FlagUseMultipleScopeNodesEndpoint = "useMultipleScopeNodesEndpoint" - // FlagLogQLScope // In-development feature that will allow injection of labels into loki queries. FlagLogQLScope = "logQLScope" @@ -415,10 +303,6 @@ const ( // Enables SQL Expressions, which can execute SQL queries against data source results. FlagSqlExpressions = "sqlExpressions" - // FlagSqlExpressionsColumnAutoComplete - // Enables column autocomplete for SQL Expressions - FlagSqlExpressionsColumnAutoComplete = "sqlExpressionsColumnAutoComplete" - // FlagKubernetesAggregator // Enable grafana's embedded kube-aggregator FlagKubernetesAggregator = "kubernetesAggregator" @@ -471,14 +355,6 @@ const ( // Enable suggested dashboards when creating new dashboards FlagSuggestedDashboards = "suggestedDashboards" - // FlagLogsExploreTableDefaultVisualization - // Sets the logs table as default visualisation in logs explore - FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization" - - // FlagAlertingListViewV2 - // Enables the new alert list view design - FlagAlertingListViewV2 = "alertingListViewV2" - // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" @@ -543,26 +419,6 @@ const ( // Enables new combobox style UI for the Ad hoc filters variable in scenes architecture FlagNewFiltersUI = "newFiltersUI" - // FlagVizActionsAuth - // Allows authenticated API calls in actions - FlagVizActionsAuth = "vizActionsAuth" - - // FlagAlertingPrometheusRulesPrimary - // Uses Prometheus rules as the primary source of truth for ruler-enabled data sources - FlagAlertingPrometheusRulesPrimary = "alertingPrometheusRulesPrimary" - - // FlagExploreLogsShardSplitting - // Used in Logs Drilldown to split queries into multiple queries based on the number of shards - FlagExploreLogsShardSplitting = "exploreLogsShardSplitting" - - // FlagExploreLogsAggregatedMetrics - // Used in Logs Drilldown to query by aggregated metrics - FlagExploreLogsAggregatedMetrics = "exploreLogsAggregatedMetrics" - - // FlagExploreLogsLimitedTimeRange - // Used in Logs Drilldown to limit the time range - FlagExploreLogsLimitedTimeRange = "exploreLogsLimitedTimeRange" - // FlagAppPlatformGrpcClientAuth // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" @@ -571,10 +427,6 @@ const ( // Enable the groupsync extension for managing Group Attribute Sync feature FlagGroupAttributeSync = "groupAttributeSync" - // FlagAlertingQueryAndExpressionsStepMode - // Enables step mode for alerting queries and expressions - FlagAlertingQueryAndExpressionsStepMode = "alertingQueryAndExpressionsStepMode" - // FlagImprovedExternalSessionHandling // Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. FlagImprovedExternalSessionHandling = "improvedExternalSessionHandling" @@ -611,10 +463,6 @@ const ( // Enables time pickers sync FlagTimeRangeProvider = "timeRangeProvider" - // FlagTimeRangePan - // Enables time range panning functionality - FlagTimeRangePan = "timeRangePan" - // FlagAzureMonitorDisableLogLimit // Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit" @@ -627,14 +475,6 @@ const ( // Enable passwordless login via magic link authentication FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication" - // FlagExploreMetricsRelatedLogs - // Display Related Logs in Grafana Metrics Drilldown - FlagExploreMetricsRelatedLogs = "exploreMetricsRelatedLogs" - - // FlagPrometheusSpecialCharsInLabelValues - // Adds support for quotes and special characters in label values for Prometheus queries - FlagPrometheusSpecialCharsInLabelValues = "prometheusSpecialCharsInLabelValues" - // FlagEnableExtensionsAdminPage // Enables the extension admin page regardless of development mode FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage" @@ -643,14 +483,6 @@ const ( // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" - // FlagCrashDetection - // Enables browser crash detection reporting to Faro. - FlagCrashDetection = "crashDetection" - - // FlagAlertingUIOptimizeReducer - // Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query - FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer" - // FlagAzureMonitorEnableUserAuth // Enables user auth for Azure Monitor datasource only FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth" @@ -683,10 +515,6 @@ const ( // Enable AI-analyze central state history. FlagAlertingAIAnalyzeCentralStateHistory = "alertingAIAnalyzeCentralStateHistory" - // FlagAlertingNotificationsStepMode - // Enables simplified step mode in the notifications section - FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode" - // FlagUnifiedStorageSearchUI // Enable unified storage search UI FlagUnifiedStorageSearchUI = "unifiedStorageSearchUI" @@ -695,10 +523,6 @@ const ( // Enables cross cluster search in the Elasticsearch data source FlagElasticsearchCrossClusterSearch = "elasticsearchCrossClusterSearch" - // FlagUnifiedHistory - // Displays the navigation history so the user can navigate back to previous pages - FlagUnifiedHistory = "unifiedHistory" - // FlagLokiLabelNamesQueryApi // Defaults to using the Loki `/labels` API instead of `/series` FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi" @@ -731,53 +555,25 @@ const ( // Enables less memory intensive Elasticsearch result parsing FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing" - // FlagDatasourceConnectionsTab - // Shows defined connections for a data source in the plugins detail page - FlagDatasourceConnectionsTab = "datasourceConnectionsTab" - // FlagFetchRulesUsingPost // Use a POST request to list rules by passing down the namespaces user has access to FlagFetchRulesUsingPost = "fetchRulesUsingPost" - // FlagNewLogsPanel - // Enables the new logs panel - FlagNewLogsPanel = "newLogsPanel" - // FlagGrafanaconThemes // Enables the temporary themes for GrafanaCon FlagGrafanaconThemes = "grafanaconThemes" - // FlagAlertingJiraIntegration - // Enables the new Jira integration for contact points in cloud alert managers. - FlagAlertingJiraIntegration = "alertingJiraIntegration" - // FlagAlertingUseNewSimplifiedRoutingHashAlgorithm FlagAlertingUseNewSimplifiedRoutingHashAlgorithm = "alertingUseNewSimplifiedRoutingHashAlgorithm" - // FlagUseScopesNavigationEndpoint - // Use the scopes navigation endpoint instead of the dashboardbindings endpoint - FlagUseScopesNavigationEndpoint = "useScopesNavigationEndpoint" - // FlagScopeSearchAllLevels // Enable scope search to include all levels of the scope node tree FlagScopeSearchAllLevels = "scopeSearchAllLevels" - // FlagAlertingRuleVersionHistoryRestore - // Enables the alert rule version history restore feature - FlagAlertingRuleVersionHistoryRestore = "alertingRuleVersionHistoryRestore" - // FlagNewShareReportDrawer // Enables the report creation drawer in a dashboard FlagNewShareReportDrawer = "newShareReportDrawer" - // FlagRendererDisableAppPluginsPreload - // Disable pre-loading app plugins when the request is coming from the renderer - FlagRendererDisableAppPluginsPreload = "rendererDisableAppPluginsPreload" - - // FlagAssetSriChecks - // Enables SRI checks for Grafana JavaScript assets - FlagAssetSriChecks = "assetSriChecks" - // FlagAlertRuleRestore // Enables the alert rule restore feature FlagAlertRuleRestore = "alertRuleRestore" @@ -786,14 +582,6 @@ const ( // Enables running Infinity queries in parallel FlagInfinityRunQueriesInParallel = "infinityRunQueriesInParallel" - // FlagAlertingMigrationUI - // Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules - FlagAlertingMigrationUI = "alertingMigrationUI" - - // FlagAlertingImportYAMLUI - // Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules - FlagAlertingImportYAMLUI = "alertingImportYAMLUI" - // FlagAzureMonitorLogsBuilderEditor // Enables the logs builder mode for the Azure Monitor data source FlagAzureMonitorLogsBuilderEditor = "azureMonitorLogsBuilderEditor" @@ -806,10 +594,6 @@ const ( // Enables the unified storage grpc connection pool FlagUnifiedStorageGrpcConnectionPool = "unifiedStorageGrpcConnectionPool" - // FlagAlertingRulePermanentlyDelete - // Enables UI functionality to permanently delete alert rules - FlagAlertingRulePermanentlyDelete = "alertingRulePermanentlyDelete" - // FlagAlertingRuleRecoverDeleted // Enables the UI functionality to recover and view deleted alert rules FlagAlertingRuleRecoverDeleted = "alertingRuleRecoverDeleted" @@ -818,22 +602,6 @@ const ( // use multi-tenant path for awsTempCredentials FlagMultiTenantTempCredentials = "multiTenantTempCredentials" - // FlagUnifiedNavbars - // Enables unified navbars - FlagUnifiedNavbars = "unifiedNavbars" - - // FlagLogsPanelControls - // Enables a control component for the logs panel in Explore - FlagLogsPanelControls = "logsPanelControls" - - // FlagMetricsFromProfiles - // Enables creating metrics from profiles and storing them as recording rules - FlagMetricsFromProfiles = "metricsFromProfiles" - - // FlagGrafanaAssistantInProfilesDrilldown - // Enables integration with Grafana Assistant in Profiles Drilldown - FlagGrafanaAssistantInProfilesDrilldown = "grafanaAssistantInProfilesDrilldown" - // FlagPostgresDSUsePGX // Enables using PGX instead of libpq for PostgreSQL datasource FlagPostgresDSUsePGX = "postgresDSUsePGX" @@ -846,18 +614,10 @@ const ( // Enables auto-updating of users installed plugins FlagPluginsAutoUpdate = "pluginsAutoUpdate" - // FlagAlertingListViewV2PreviewToggle - // Enables the alerting list view v2 preview toggle - FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle" - // FlagAlertRuleUseFiredAtForStartsAt // Use FiredAt for StartsAt when sending alerts to Alertmaanger FlagAlertRuleUseFiredAtForStartsAt = "alertRuleUseFiredAtForStartsAt" - // FlagAlertingBulkActionsInUI - // Enables the alerting bulk actions in the UI - FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI" - // FlagKubernetesAuthzApis // Registers AuthZ /apis endpoint FlagKubernetesAuthzApis = "kubernetesAuthzApis" @@ -902,10 +662,6 @@ const ( // Enables the UI to see imported Alertmanager configuration FlagAlertingImportAlertmanagerUI = "alertingImportAlertmanagerUI" - // FlagSharingDashboardImage - // Enables image sharing functionality for dashboards - FlagSharingDashboardImage = "sharingDashboardImage" - // FlagPreferLibraryPanelTitle // Prefer library panel title over viz panel title. FlagPreferLibraryPanelTitle = "preferLibraryPanelTitle" @@ -918,22 +674,6 @@ const ( // Enables new design for the InfluxDB data source configuration page FlagNewInfluxDSConfigPageDesign = "newInfluxDSConfigPageDesign" - // FlagEnableAppChromeExtensions - // Set this to true to enable all app chrome extensions registered by plugins. - FlagEnableAppChromeExtensions = "enableAppChromeExtensions" - - // FlagEnableDashboardEmptyExtensions - // Set this to true to enable all dashboard empty state extensions registered by plugins. - FlagEnableDashboardEmptyExtensions = "enableDashboardEmptyExtensions" - - // FlagFoldersAppPlatformAPI - // Enables use of app platform API for folders - FlagFoldersAppPlatformAPI = "foldersAppPlatformAPI" - - // FlagOtelLogsFormatting - // Applies OTel formatting templates to displayed logs - FlagOtelLogsFormatting = "otelLogsFormatting" - // FlagAlertingNotificationHistory // Enables the notification history feature FlagAlertingNotificationHistory = "alertingNotificationHistory" @@ -942,26 +682,10 @@ const ( // Enable dual reader for unified storage search FlagUnifiedStorageSearchDualReaderEnabled = "unifiedStorageSearchDualReaderEnabled" - // FlagDashboardLevelTimeMacros - // Supports __from and __to macros that always use the dashboard level time range - FlagDashboardLevelTimeMacros = "dashboardLevelTimeMacros" - // FlagAlertmanagerRemoteSecondaryWithRemoteState // Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications. FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState" - // FlagRestrictedPluginApis - // Enables sharing a list of APIs with a list of plugins - FlagRestrictedPluginApis = "restrictedPluginApis" - - // FlagFavoriteDatasources - // Enable favorite datasources - FlagFavoriteDatasources = "favoriteDatasources" - - // FlagNewLogContext - // New Log Context component - FlagNewLogContext = "newLogContext" - // FlagNewClickhouseConfigPageDesign // Enables new design for the Clickhouse data source configuration page FlagNewClickhouseConfigPageDesign = "newClickhouseConfigPageDesign" @@ -982,10 +706,6 @@ const ( // Enables the Graphite data source full backend mode FlagGraphiteBackendMode = "graphiteBackendMode" - // FlagAzureResourcePickerUpdates - // Enables the updated Azure Monitor resource picker - FlagAzureResourcePickerUpdates = "azureResourcePickerUpdates" - // FlagPrometheusTypeMigration // Checks for deprecated Prometheus authentication methods (SigV4 and Azure), installs the relevant data source, and migrates the Prometheus data sources FlagPrometheusTypeMigration = "prometheusTypeMigration" @@ -1010,18 +730,6 @@ const ( // Enable syncing plugin installations to the installs API FlagPluginInstallAPISync = "pluginInstallAPISync" - // FlagNewGauge - // Enable new gauge visualization - FlagNewGauge = "newGauge" - - // FlagNewVizSuggestions - // Enable new visualization suggestions - FlagNewVizSuggestions = "newVizSuggestions" - - // FlagPreventPanelChromeOverflow - // Restrict PanelChrome contents with overflow: hidden; - FlagPreventPanelChromeOverflow = "preventPanelChromeOverflow" - // FlagJaegerEnableGrpcEndpoint // Enable querying trace data through Jaeger's gRPC endpoint (HTTP) FlagJaegerEnableGrpcEndpoint = "jaegerEnableGrpcEndpoint" @@ -1053,16 +761,4 @@ const ( // FlagAwsDatasourcesHttpProxy // Enables http proxy settings for aws datasources FlagAwsDatasourcesHttpProxy = "awsDatasourcesHttpProxy" - - // FlagTransformationsEmptyPlaceholder - // Show transformation quick-start cards in empty transformations state - FlagTransformationsEmptyPlaceholder = "transformationsEmptyPlaceholder" - - // FlagTtlPluginInstanceManager - // Enable TTL plugin instance manager - FlagTtlPluginInstanceManager = "ttlPluginInstanceManager" - - // FlagRudderstackUpgrade - // Enables the new version of rudderstack - FlagRudderstackUpgrade = "rudderstackUpgrade" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 44ed33ad19c..48eba662c0a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -62,7 +62,6 @@ "description": "Enable configuration of alert enrichments in Grafana Cloud.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -77,7 +76,6 @@ "description": "Enable conditional alert enrichment steps.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -92,7 +90,6 @@ "description": "Allow multiple steps per enrichment.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -133,7 +130,6 @@ "description": "Enable AI-analyze central state history.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -148,7 +144,6 @@ "description": "Enable AI-generated feedback from the Grafana UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -163,7 +158,6 @@ "description": "Enable AI-generated alert rules.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -178,7 +172,6 @@ "description": "Enable AI-generated alerting templates.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -193,7 +186,6 @@ "description": "Enable AI-improve alert rules labels and annotations.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -221,7 +213,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -229,14 +220,16 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-29T15:01:38Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2024-05-29T15:01:38Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the new central alert history.", "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "frontend": true + "codeowner": "@grafana/alerting-squad" } }, { @@ -249,7 +242,6 @@ "description": "Disables the ability to send alerts to an external Alertmanager datasource.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -263,7 +255,6 @@ "description": "Enable Assistant Investigations enrichment type.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -279,7 +270,6 @@ "description": "Enable Assistant Investigations enrichment type in the UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -294,7 +284,6 @@ "description": "Enable enrichment per rule in the alerting UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -322,7 +311,6 @@ "description": "Enables the API to import Alertmanager configuration", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -337,7 +325,6 @@ "description": "Enables the UI to see imported Alertmanager configuration", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -420,7 +407,6 @@ "description": "Enables the notification history feature", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -462,7 +448,6 @@ "description": "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -504,7 +489,6 @@ "description": "Enable rule notification message section extension.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -520,7 +504,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -528,15 +511,16 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-27T14:39:26Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2025-03-27T14:39:26Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the UI functionality to recover and view deleted alert rules", "stage": "GA", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -552,7 +536,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -588,15 +571,16 @@ { "metadata": { "name": "alertingTriage", - "resourceVersion": "1756386724059", - "creationTimestamp": "2025-09-01T09:33:33Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2025-09-01T09:33:33Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the alerting triage feature", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -625,7 +609,6 @@ "description": "Enables the UI to use certain backend-side filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -644,7 +627,6 @@ "stage": "preview", "codeowner": "@grafana/alerting-squad", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -664,7 +646,6 @@ "stage": "deprecated", "codeowner": "@grafana/alerting-squad", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -706,7 +687,6 @@ "description": "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -733,7 +713,6 @@ "description": "Enables the gRPC client to authenticate with the App Platform by using ID \u0026 access tokens", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -760,7 +739,6 @@ "description": "Enables the gRPC server for authorization", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -895,8 +873,7 @@ "description": "Allow elements nesting", "stage": "experimental", "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -963,8 +940,6 @@ "stage": "preview", "codeowner": "@grafana/identity-access-team", "requiresRestart": true, - "allowSelfServe": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -990,7 +965,6 @@ "description": "Enables cross-account querying in CloudWatch datasources", "stage": "GA", "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true, "expression": "true" } }, @@ -1048,7 +1022,6 @@ "description": "Correlations page", "stage": "GA", "codeowner": "@grafana/datapro", - "allowSelfServe": true, "expression": "true" } }, @@ -1138,14 +1111,16 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-10-23T08:55:45Z" + "resourceVersion": "1763533458962", + "creationTimestamp": "2024-10-23T08:55:45Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" + } }, "spec": { "description": "Enables experimental new dashboard layouts", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "codeowner": "@grafana/dashboards-squad" } }, { @@ -1266,7 +1241,6 @@ "stage": "GA", "codeowner": "@grafana/observability-metrics", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -1319,8 +1293,7 @@ "description": "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", "stage": "experimental", "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromAdminPage": true + "requiresRestart": true } }, { @@ -1333,7 +1306,6 @@ "description": "Disable envelope encryption (emergency only)", "stage": "GA", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "expression": "false" } }, @@ -1380,7 +1352,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1441,7 +1412,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1457,7 +1427,6 @@ "stage": "experimental", "codeowner": "@grafana/dashboards-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1465,14 +1434,16 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-24T14:46:31Z" + "resourceVersion": "1763539789149", + "creationTimestamp": "2023-04-24T14:46:31Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:09:49.149669 +0000 UTC" + } }, "spec": { "description": "Enables the edit functionality in the datagrid panel", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/dataviz-squad" } }, { @@ -1498,8 +1469,7 @@ "description": "Enables native HTTP Histograms", "stage": "experimental", "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromAdminPage": true + "requiresRestart": true } }, { @@ -1514,7 +1484,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1541,7 +1510,6 @@ "description": "Enables the scopes usage in Metrics Explore", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1634,8 +1602,7 @@ "spec": { "description": "Automatic service account and token setup for plugins", "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "codeowner": "@grafana/identity-access-team" } }, { @@ -1705,7 +1672,6 @@ "description": "Highlight Grafana Enterprise features", "stage": "GA", "codeowner": "@grafana/grafana-operator-experience-squad", - "allowSelfServe": true, "expression": "false" } }, @@ -1751,7 +1717,6 @@ "description": "Use a POST request to list rules by passing down the namespaces user has access to", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1781,7 +1746,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-search-navigate-organise", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1886,7 +1850,6 @@ "description": "Enables Grafana-managed recording rules.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1917,7 +1880,6 @@ "stage": "GA", "codeowner": "@grafana/grafana-frontend-platform", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -1958,7 +1920,6 @@ "description": "Enable groupBy variable support in scenes dashboards", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1989,8 +1950,7 @@ "spec": { "description": "Run the GRPC server", "stage": "preview", - "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true + "codeowner": "@grafana/search-and-storage" } }, { @@ -2003,7 +1963,6 @@ "description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -2017,7 +1976,6 @@ "description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -2121,7 +2079,6 @@ "stage": "experimental", "codeowner": "@grafana/sharing-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2239,7 +2196,6 @@ "description": "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2256,7 +2212,6 @@ "description": "Enables create, delete, and update mutations for resources owned by IAM identity", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2270,7 +2225,6 @@ "description": "Registers AuthZ /apis endpoint", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2285,7 +2239,6 @@ "description": "Enables K8s AuthZ endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2299,7 +2252,6 @@ "description": "Registers AuthZ resource permission /apis endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2316,7 +2268,6 @@ "description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2336,17 +2287,16 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1755157224830", + "resourceVersion": "1763533458962", "creationTimestamp": "2024-06-05T14:34:23Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-08-14 07:40:24.830741 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" } }, "spec": { "description": "Use the kubernetes API in the frontend for dashboards", "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true, "expression": "true" } }, @@ -2360,8 +2310,7 @@ "description": "Use the kubernetes API for feature toggle management in the frontend", "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -2459,7 +2408,6 @@ "description": "Populate Zanzana on AuthZ CRDs creation or update", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2498,7 +2446,6 @@ "description": "In-development feature that will allow injection of labels into loki queries.", "stage": "privatePreview", "codeowner": "@grafana/observability-logs", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -2541,7 +2488,6 @@ "stage": "GA", "codeowner": "@grafana/observability-logs", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -2648,7 +2594,6 @@ "stage": "GA", "codeowner": "@grafana/observability-logs", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -2687,7 +2632,6 @@ "description": "Pick the dual write mode from database configs", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2792,7 +2736,6 @@ "description": "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2903,7 +2846,6 @@ "description": "Enables the report creation drawer in a dashboard", "stage": "preview", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2931,7 +2873,6 @@ "description": "Require that sub claims is present in oauth tokens.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2961,7 +2902,6 @@ "description": "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3043,8 +2983,7 @@ "spec": { "description": "Search for dashboards using panel title", "stage": "preview", - "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true + "codeowner": "@grafana/search-and-storage" } }, { @@ -3057,7 +2996,6 @@ "description": "Enable passwordless login via magic link authentication", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3127,7 +3065,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -3289,7 +3226,6 @@ "description": "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3318,7 +3254,6 @@ "description": "In-development feature that will allow injection of labels into prometheus queries.", "stage": "GA", "codeowner": "@grafana/oss-big-tent", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3404,7 +3339,6 @@ "description": "Enables public dashboard sharing to be restricted to only allowed emails", "stage": "preview", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3540,8 +3474,7 @@ "spec": { "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", "stage": "preview", - "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/dashboards-squad" } }, { @@ -3554,7 +3487,6 @@ "description": "Require that refresh tokens are present in oauth tokens.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3585,7 +3517,6 @@ "description": "Enables reload of dashboards on scopes, time range and variables changes", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3598,8 +3529,7 @@ "spec": { "description": "Uses JWT-based auth for rendering instead of relying on remote cache", "stage": "preview", - "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { @@ -3613,7 +3543,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3643,7 +3572,6 @@ "description": "Enables restore deleted dashboards feature", "stage": "experimental", "codeowner": "@grafana/grafana-search-navigate-organise", - "hideFromAdminPage": true, "expression": "false" } }, @@ -3661,7 +3589,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -3730,7 +3657,6 @@ "description": "In-development feature flag for the scope api using the app platform.", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true, "expression": "false" } }, @@ -3744,7 +3670,6 @@ "description": "Enables the use of scope filters in Grafana", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3761,7 +3686,6 @@ "description": "Enable scope search to include all levels of the scope node tree", "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3829,7 +3753,6 @@ "description": "Skip token rotation if it was already rotated less than 5 seconds ago", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3845,8 +3768,7 @@ "description": "Enables previous SQL data source dataset dropdown behavior", "stage": "preview", "codeowner": "@grafana/oss-big-tent", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -3903,7 +3825,6 @@ "stage": "GA", "codeowner": "@grafana/identity-access-team", "requiresRestart": true, - "allowSelfServe": true, "expression": "true" } }, @@ -4012,7 +3933,6 @@ "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -4059,14 +3979,16 @@ { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-15T13:36:36Z" + "resourceVersion": "1763532089512", + "creationTimestamp": "2025-07-15T13:36:36Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 06:01:29.512182 +0000 UTC" + } }, "spec": { "description": "Enables creating alerts from Tempo data source", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true + "codeowner": "@grafana/observability-traces-and-profiling" } }, { @@ -4170,7 +4092,6 @@ "stage": "GA", "codeowner": "@grafana/observability-metrics", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -4227,7 +4148,6 @@ "description": "Writes error logs to the request logger", "stage": "GA", "codeowner": "@grafana/grafana-backend-group", - "hideFromAdminPage": true, "expression": "true" } }, @@ -4253,7 +4173,6 @@ "description": "Enables the unified storage grpc connection pool", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4268,7 +4187,6 @@ "description": "Enables the unified storage history pruner", "stage": "GA", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -4283,7 +4201,6 @@ "description": "Enable unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4302,7 +4219,6 @@ "stage": "experimental", "codeowner": "@grafana/search-and-storage", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4317,7 +4233,6 @@ "description": "Enable dual reader for unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4331,7 +4246,6 @@ "description": "Enable sprinkles on unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4345,7 +4259,6 @@ "description": "Enable unified storage search UI", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4360,7 +4273,6 @@ "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4404,7 +4316,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4420,7 +4331,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4439,7 +4349,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4470,7 +4379,6 @@ "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4484,7 +4392,6 @@ "description": "Use openFGA as authorization engine.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4501,7 +4408,6 @@ "description": "Use openFGA as main authorization engine and disable legacy RBAC clietn.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } } diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index d6a7c66802d..57e308de4ec 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -50,17 +50,14 @@ func TestFeatureToggleFiles(t *testing.T) { lookup := map[string]featuretoggleapi.FeatureSpec{} for _, flag := range standardFeatureFlags { lookup[flag.Name] = featuretoggleapi.FeatureSpec{ - Description: flag.Description, - Stage: flag.Stage.String(), - Owner: string(flag.Owner), - RequiresDevMode: flag.RequiresDevMode, - FrontendOnly: flag.FrontendOnly, - RequiresRestart: flag.RequiresRestart, - AllowSelfServe: flag.AllowSelfServe, - HideFromAdminPage: flag.HideFromAdminPage, - HideFromDocs: flag.HideFromDocs, - Expression: flag.Expression, - // EnabledVersion: ???, + Description: flag.Description, + Stage: flag.Stage.String(), + Owner: string(flag.Owner), + RequiresDevMode: flag.RequiresDevMode, + FrontendOnly: flag.FrontendOnly, + RequiresRestart: flag.RequiresRestart, + HideFromDocs: flag.HideFromDocs, + Expression: flag.Expression, } // Replace them all @@ -187,9 +184,6 @@ func verifyFlagsConfiguration(t *testing.T) { if flag.Name != strings.TrimSpace(flag.Name) { t.Errorf("flag Name should not start/end with spaces. See: %s", flag.Name) } - if flag.AllowSelfServe && (flag.Stage != FeatureStageGeneralAvailability && flag.Stage != FeatureStagePublicPreview && flag.Stage != FeatureStageDeprecated) { - t.Errorf("only allow self-serving GA, PublicPreview and Deprecated toggles") - } if flag.Owner == "" { t.Errorf("feature %s does not have an owner. please fill the FeatureFlag.Owner property", flag.Name) } @@ -340,6 +334,10 @@ package featuremgmt const (`) for _, flag := range standardFeatureFlags { + if flag.FrontendOnly { + continue // no need to have the golang constant for frontend only flags + } + data.CamelCase = strcase.ToCamel(flag.Name) data.Flag = flag data.Ext = "" From a4cbbe10c0d36a89334707f4f3f8e4526391ade6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 20 Nov 2025 14:55:45 +0100 Subject: [PATCH 002/423] Provisioning: Add retry logic for nanogit client operations (#114216) * chore(deps): update nanogit to v0.3.0 in go.mod and go.sum files * Add retry logic for nanogit client operations - Configure retry logic in withGitContext to ensure all Git operations have retry support - Use nanogit's ExponentialBackoffRetrier with 8 attempts (~10s retry window) - Retry transient network errors and HTTP-specific server errors (5xx for GET/DELETE, 429 for all) - Rename logger function to withGitContext to better reflect its responsibilities * fix: resolve staticcheck S1008 linting issue in retry_client.go Simplify return statement to use errors.As directly instead of if-return pattern * Revert "fix: resolve staticcheck S1008 linting issue in retry_client.go" This reverts commit bd367b5629bd49d9d15db708352b9ed1805376ab. --- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 +- .../pkg/repository/git/repository.go | 80 ++++++++++++++++--- .../pkg/repository/git/repository_test.go | 10 +-- go.mod | 2 +- go.sum | 4 +- go.work.sum | 4 +- 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 9bb12d5ae4f..a72aa77fe68 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -10,7 +10,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b + github.com/grafana/nanogit v0.3.0 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.33.0 diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 78f06781b15..92a4d53c282 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -70,8 +70,8 @@ github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= +github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= 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= diff --git a/apps/provisioning/pkg/repository/git/repository.go b/apps/provisioning/pkg/repository/git/repository.go index 4d002e4f831..82d5ca6bd1d 100644 --- a/apps/provisioning/pkg/repository/git/repository.go +++ b/apps/provisioning/pkg/repository/git/repository.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/nanogit/options" "github.com/grafana/nanogit/protocol" "github.com/grafana/nanogit/protocol/hash" + "github.com/grafana/nanogit/retry" ) type RepositoryConfig struct { @@ -144,7 +145,7 @@ func isValidGitURL(gitURL string) bool { // Test implements provisioning.Repository. func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - ctx, _ = r.logger(ctx, "") + ctx, _ = r.withGitContext(ctx, "") t := string(r.config.Spec.Type) @@ -219,7 +220,7 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er // Read implements provisioning.Repository. func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) finalPath := safepath.Join(r.gitConfig.Path, filePath) // Resolve ref to commit hash @@ -271,7 +272,7 @@ func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*reposi } func (r *gitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) // Resolve ref to commit hash refHash, err := r.resolveRefToHash(ctx, ref) @@ -319,7 +320,7 @@ func (r *gitRepository) Create(ctx context.Context, path, ref string, data []byt if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { return err @@ -364,7 +365,7 @@ func (r *gitRepository) Update(ctx context.Context, path, ref string, data []byt if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) // Check if trying to update a directory if safepath.IsDir(path) { @@ -411,7 +412,7 @@ func (r *gitRepository) Write(ctx context.Context, path string, ref string, data ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) info, err := r.Read(ctx, path, ref) if err != nil && !(errors.Is(err, repository.ErrFileNotFound)) { return fmt.Errorf("check if file exists before writing: %w", err) @@ -431,7 +432,7 @@ func (r *gitRepository) Delete(ctx context.Context, path, ref, comment string) e if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { @@ -454,7 +455,7 @@ func (r *gitRepository) Move(ctx context.Context, oldPath, newPath, ref, comment if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { @@ -545,6 +546,7 @@ func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provis } func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) { + ctx, _ = r.withGitContext(ctx, "") refs, err := r.client.ListRefs(ctx) if err != nil { return nil, fmt.Errorf("list refs: %w", err) @@ -566,7 +568,7 @@ func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, e } func (r *gitRepository) LatestRef(ctx context.Context) (string, error) { - ctx, _ = r.logger(ctx, "") + ctx, _ = r.withGitContext(ctx, "") branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch)) if err != nil { return "", fmt.Errorf("get branch ref: %w", err) @@ -583,7 +585,7 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r return nil, fmt.Errorf("ref cannot be empty") } - ctx, logger := r.logger(ctx, ref) + ctx, logger := r.withGitContext(ctx, ref) // Resolve base ref to hash var baseHash hash.Hash @@ -671,11 +673,15 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r } func (r *gitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ctx = ensureRetryContext(ctx) + ctx, _ = r.withGitContext(ctx, "") return NewStagedGitRepository(ctx, r, opts) } // resolveRefToHash resolves a ref (branch name or commit hash) to a commit hash func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.Hash, error) { + ctx, _ = r.withGitContext(ctx, ref) + // Use default branch if ref is empty if ref == "" { ref = r.gitConfig.Branch @@ -706,6 +712,8 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash. // ensureBranchExists checks if a branch exists and creates it if it doesn't, // returning the branch reference to avoid duplicate GetRef calls func (r *gitRepository) ensureBranchExists(ctx context.Context, branchName string) (nanogit.Ref, error) { + ctx, _ = r.withGitContext(ctx, branchName) + if !IsValidGitBranchName(branchName) { return nanogit.Ref{}, &apierrors.StatusError{ ErrStatus: metav1.Status{ @@ -802,7 +810,57 @@ func (r *gitRepository) commitAndPush(ctx context.Context, writer nanogit.Staged return nil } -func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) { +// defaultGitRetrier returns a default retrier configuration for Git operations. +// +// Retry attempts will happen when: +// - Network errors occur: connection timeouts, temporary network failures, or connection errors +// - HTTP 5xx server errors: For GET and DELETE operations (idempotent) +// - HTTP 429 Too Many Requests: For all operations (rate limiting is temporary) +// +// The retry behavior: +// - Total attempts: 8 (1 initial attempt + 7 retries) +// - Initial delay: 100ms before the first retry +// - Exponential backoff: delay doubles after each failed attempt (100ms → 200ms → 400ms → 800ms → 1.6s → 3.2s → 5s) +// - Maximum delay: capped at 5 seconds +// - Jitter: enabled to prevent thundering herd problems +// - Total retry window: approximately 10 seconds from first attempt to last retry +// +// All attempts will fail when: +// - The Git server is completely unavailable or unreachable +// - Network connectivity issues persist beyond the retry window (~10 seconds) +// - The server returns transient errors consistently for the entire retry duration +// - Context cancellation occurs before retries complete +// +// Non-transient errors (e.g., 4xx client errors except 429, authentication failures) are not retried and returned immediately. +func defaultGitRetrier() *retry.ExponentialBackoffRetrier { + return retry.NewExponentialBackoffRetrier(). + WithMaxAttempts(8). // 1 initial + 7 retries = 8 total attempts (~10s total retry window) + WithInitialDelay(100 * time.Millisecond). + WithMaxDelay(5 * time.Second). + WithMultiplier(2.0). + WithJitter() +} + +// ensureRetryContext ensures that retry logic is configured in the context. +// This function should be called at the beginning of all methods that make client calls +// to guarantee retry logic is always present, regardless of context state. +func ensureRetryContext(ctx context.Context) context.Context { + // Only add retrier if one doesn't already exist in the context + if retry.FromContext(ctx).MaxAttempts() <= 1 { + ctx = retry.ToContext(ctx, defaultGitRetrier()) + } + return ctx +} + +// withGitContext sets up the context with logging, git repository metadata, and retry logic. +// This function should be called at the beginning of all public methods to ensure: +// - Proper logging context with git repository details +// - Retry logic is configured for all Git operations +// - Context is properly prepared for nanogit client calls +func (r *gitRepository) withGitContext(ctx context.Context, ref string) (context.Context, logging.Logger) { + // Ensure retry logic is configured first, before any early returns + ctx = ensureRetryContext(ctx) + logger := logging.FromContext(ctx) type containsGit int diff --git a/apps/provisioning/pkg/repository/git/repository_test.go b/apps/provisioning/pkg/repository/git/repository_test.go index d3946b7b6d4..4958ab372ab 100644 --- a/apps/provisioning/pkg/repository/git/repository_test.go +++ b/apps/provisioning/pkg/repository/git/repository_test.go @@ -2163,7 +2163,7 @@ func TestGitRepository_commitAndPush(t *testing.T) { } } -func TestGitRepository_logger(t *testing.T) { +func TestGitRepository_withGitContext(t *testing.T) { gitRepo := &gitRepository{ config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2179,7 +2179,7 @@ func TestGitRepository_logger(t *testing.T) { t.Run("creates new logger context", func(t *testing.T) { ctx := context.Background() - newCtx, logger := gitRepo.logger(ctx, "feature-branch") + newCtx, logger := gitRepo.withGitContext(ctx, "feature-branch") require.NotNil(t, newCtx) require.NotNil(t, logger) @@ -2188,7 +2188,7 @@ func TestGitRepository_logger(t *testing.T) { t.Run("uses default branch when ref is empty", func(t *testing.T) { ctx := context.Background() - newCtx, logger := gitRepo.logger(ctx, "") + newCtx, logger := gitRepo.withGitContext(ctx, "") require.NotNil(t, newCtx) require.NotNil(t, logger) @@ -2198,10 +2198,10 @@ func TestGitRepository_logger(t *testing.T) { ctx := context.Background() // First call creates the logger context - ctx1, logger1 := gitRepo.logger(ctx, "branch1") + ctx1, logger1 := gitRepo.withGitContext(ctx, "branch1") // Second call should return the existing logger context - ctx2, logger2 := gitRepo.logger(ctx1, "branch2") + ctx2, logger2 := gitRepo.withGitContext(ctx1, "branch2") // When logger context already exists, it should return the same context require.Equal(t, ctx1, ctx2) diff --git a/go.mod b/go.mod index 23756c59ae0..d8df0cd6bd7 100644 --- a/go.mod +++ b/go.mod @@ -106,7 +106,7 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.283.0 // @grafana/plugins-platform-backend github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b // indirect; @grafana/grafana-git-ui-sync-team + github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling diff --git a/go.sum b/go.sum index 61068fdef0b..1c294bc1fe2 100644 --- a/go.sum +++ b/go.sum @@ -1661,8 +1661,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLm github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= +github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= diff --git a/go.work.sum b/go.work.sum index 9dc15683534..bbfe799f72c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -937,7 +937,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= @@ -1323,7 +1322,6 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= @@ -1374,6 +1372,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs= github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/segmentio/parquet-go v0.0.0-20220811205829-7efc157d28af/go.mod h1:PxYdAI6cGd+s1j4hZDQbz3VFgobF5fDA0weLeNWKTE4= @@ -1920,7 +1919,6 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= From 3999d108f7f21a072990400e8cfcf7c4dd6bcdfc Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 20 Nov 2025 14:00:33 +0000 Subject: [PATCH 003/423] Folders: Make `listFolders` call correct API and fix tags sorting (#114181) --- .../browse-dashboards/api/services.ts | 19 ++++++++++--------- public/app/features/search/service/unified.ts | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index 98be615583b..2bb1ec6d39e 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -1,8 +1,7 @@ -import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; -import { NestedFolderDTO } from 'app/features/search/service/types'; +import { DashboardQueryResult } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; import { AccessControlAction } from 'app/types/accessControl'; @@ -17,22 +16,24 @@ export async function listFolders( page = 1, pageSize = PAGE_SIZE ): Promise { - const backendSrv = getBackendSrv(); + const searcher = getGrafanaSearcher(); - // TODO: what to do here for unified search? - let folders: NestedFolderDTO[] = []; + let folders: DashboardQueryResult[] = []; if (contextSrv.hasPermission(AccessControlAction.FoldersRead)) { - folders = await backendSrv.get('/api/folders', { - parentUid: parentUID, - page, + const foldersResults = await searcher.search({ + kind: ['folder'], + location: parentUID || 'general', + from: (page - 1) * pageSize, // our pages are 1-indexed, so we need to -1 to convert that to correct value to skip limit: pageSize, + offset: (page - 1) * pageSize, }); + folders = foldersResults.view.toArray(); } return folders.map((item) => ({ kind: 'folder', uid: item.uid, - title: item.title, + title: item.name, parentTitle, parentUID, managedBy: item.managedBy, diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 4ab551997a3..8d1af58f9d9 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -386,7 +386,9 @@ export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFr ...hit, uid: hit.name, url: toURL(hit.resource, hit.name, hit.title), - tags: hit.tags || [], + // Sort tags so we aren't reliant on the backend having done this for us + // Sorting order can be different between APIs/search implementations + tags: (hit.tags || []).sort(), folder: hit.folder || 'general', location, name: hit.title, // 🤯 FIXME hit.name is k8s name, eg grafana dashboards UID From 7d179120b6387c679d66b54fbbe41d7d15cc25c6 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 20 Nov 2025 14:03:24 +0000 Subject: [PATCH 004/423] Plugins: Replace CDN class with FS CDN type (#113968) replace cdn class with fs type --- apps/plugins/kinds/plugin.cue | 4 +- .../apis/plugins/v0alpha1/plugin_spec_gen.go | 1 - apps/plugins/pkg/apis/plugins_manifest.go | 2 +- apps/plugins/pkg/app/install/registrar.go | 1 - .../plugins/pkg/app/install/registrar_test.go | 13 --- pkg/api/frontendsettings_test.go | 14 +++- pkg/plugins/ifaces.go | 17 +++- pkg/plugins/localfiles.go | 6 +- .../angularinspector/angularinspector.go | 2 +- .../angularinspector/angularinspector_test.go | 7 +- pkg/plugins/manager/loader/loader.go | 34 +------- .../manager/pipeline/validation/steps.go | 7 +- pkg/plugins/manager/pluginfakes/fakes.go | 12 ++- .../manager/signature/manifest_test.go | 2 +- pkg/plugins/pluginassets/pluginassets_test.go | 9 --- pkg/plugins/plugins.go | 3 +- pkg/plugins/test_utils.go | 2 +- .../pluginsintegration/loader/loader_test.go | 8 +- .../pluginsintegration/pipeline/steps.go | 2 +- .../pluginassets/pluginassets.go | 10 +-- .../pluginassets/pluginassets_test.go | 80 ++++++++++++------- 21 files changed, 122 insertions(+), 114 deletions(-) diff --git a/apps/plugins/kinds/plugin.cue b/apps/plugins/kinds/plugin.cue index 4f381082969..50c475cd430 100644 --- a/apps/plugins/kinds/plugin.cue +++ b/apps/plugins/kinds/plugin.cue @@ -13,7 +13,7 @@ pluginV0Alpha1: { id: string version: string url?: string - class: "core" | "external" | "cdn" + class: "core" | "external" } } routes: { @@ -233,4 +233,4 @@ pluginV0Alpha1: { title?: string description?: string }] -} \ No newline at end of file +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go index d46a0a4aed1..9ae14bcbdc5 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go @@ -21,5 +21,4 @@ type PluginSpecClass string const ( PluginSpecClassCore PluginSpecClass = "core" PluginSpecClassExternal PluginSpecClass = "external" - PluginSpecClassCdn PluginSpecClass = "cdn" ) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 428971aa0b5..09c57eaa186 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,7 +20,7 @@ import ( ) var ( - rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external","cdn"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaPluginv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) ) diff --git a/apps/plugins/pkg/app/install/registrar.go b/apps/plugins/pkg/app/install/registrar.go index fe747eb2806..3977fd88cf0 100644 --- a/apps/plugins/pkg/app/install/registrar.go +++ b/apps/plugins/pkg/app/install/registrar.go @@ -23,7 +23,6 @@ type Class = string const ( ClassCore Class = "core" ClassExternal Class = "external" - ClassCDN Class = "cdn" ) type Source = string diff --git a/apps/plugins/pkg/app/install/registrar_test.go b/apps/plugins/pkg/app/install/registrar_test.go index 36359896ae6..f1e0adbc508 100644 --- a/apps/plugins/pkg/app/install/registrar_test.go +++ b/apps/plugins/pkg/app/install/registrar_test.go @@ -446,19 +446,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class) }, }, - { - name: "cdn class is mapped correctly", - install: PluginInstall{ - ID: "plugin-cdn", - Version: "3.0.0", - Class: ClassCDN, - Source: SourcePluginStore, - }, - namespace: "org-3", - validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { - require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCDN), p.Spec.Class) - }, - }, { name: "source annotation is set correctly", install: PluginInstall{ diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index cb09065dc85..ba67837be5d 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/login/social/socialimpl" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -265,6 +266,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -302,6 +304,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -339,6 +342,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Preload: true, }, Angular: plugins.AngularMeta{Detected: true}, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -404,12 +408,12 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { }, }, { - desc: "app plugin with CDN class", + desc: "app plugin with CDN fs", pluginStore: func() pluginstore.Store { return &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ { - Class: plugins.ClassCDN, + Class: plugins.ClassExternal, Module: fmt.Sprintf("/%s/module.js", "test-app"), JSONData: plugins.JSONData{ ID: "test-app", @@ -417,6 +421,9 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }}, }, }, } @@ -545,6 +552,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -594,6 +602,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -633,6 +642,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index e50f059e815..9b719b8b180 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -72,12 +72,27 @@ type UpdateInfo struct { type FS interface { fs.FS - Type() string + Type() FSType Base() string Files() ([]string, error) Rel(string) (string, error) } +type FSType string + +const ( + FSTypeCDN FSType = "cdn" + FSTypeLocal FSType = "local" +) + +func (f FSType) CDN() bool { + return f == FSTypeCDN +} + +func (f FSType) Local() bool { + return f == FSTypeLocal +} + type FSRemover interface { Remove() error } diff --git a/pkg/plugins/localfiles.go b/pkg/plugins/localfiles.go index a6584641a55..073ab0e844d 100644 --- a/pkg/plugins/localfiles.go +++ b/pkg/plugins/localfiles.go @@ -30,8 +30,8 @@ func NewLocalFS(basePath string) LocalFS { return LocalFS{basePath: basePath} } -func (f LocalFS) Type() string { - return "local" +func (f LocalFS) Type() FSType { + return FSTypeLocal } // fileIsAllowed takes an absolute path to a file and an os.FileInfo for that file, and it checks if access to that @@ -219,7 +219,7 @@ func NewStaticFS(fs FS) (StaticFS, error) { }, nil } -func (f StaticFS) Type() string { +func (f StaticFS) Type() FSType { return f.FS.Type() } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go index 98840189389..0ee0f9d9ee9 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go @@ -36,7 +36,7 @@ func NewPatternListInspector(detectorsProvider angulardetector.DetectorsProvider func (i *PatternsListInspector) Inspect(ctx context.Context, p *plugins.Plugin) (isAngular bool, err error) { // CDN plugins are ignored because they should not be using Angular - if p.Class == plugins.ClassCDN { + if p.FS.Type().CDN() { return false, nil } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go index 47c5af990b9..1e29443d97d 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" ) type fakeDetector struct { @@ -81,7 +82,11 @@ func TestPatternsListInspector(t *testing.T) { { name: "CDN plugins return false without calling detectors", plugin: &plugins.Plugin{ - Class: plugins.ClassCDN, + FS: &pluginfakes.FakePluginFS{ + TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }, + }, }, fakeDetectors: []*fakeDetector{ {returns: true}, diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index e6bbc257f23..d4943cee865 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -18,8 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) -const concurrencyLimit = 32 - type Loader struct { cfg *pluginsCfg.PluginManagementCfg discovery discovery.Discoverer @@ -87,37 +85,13 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins st = time.Now() validatedPlugins := []*plugins.Plugin{} - type validateResult struct { - bootstrappedPlugin *plugins.Plugin - err error - } - validateResults := make(chan validateResult, len(bootstrappedPlugins)) - - var limitSize int - if src.PluginClass(ctx) == plugins.ClassCDN { - limitSize = min(len(bootstrappedPlugins), concurrencyLimit) - } else { - limitSize = 1 - } - limit := make(chan struct{}, limitSize) for _, bootstrappedPlugin := range bootstrappedPlugins { - limit <- struct{}{} - go func(p *plugins.Plugin) { - err := l.validation.Validate(ctx, p) - validateResults <- validateResult{ - bootstrappedPlugin: bootstrappedPlugin, - err: err, - } - <-limit - }(bootstrappedPlugin) - } - for i := 0; i < len(bootstrappedPlugins); i++ { - r := <-validateResults - if r.err != nil { - l.recordError(ctx, r.bootstrappedPlugin, r.err) + err := l.validation.Validate(ctx, bootstrappedPlugin) + if err != nil { + l.recordError(ctx, bootstrappedPlugin, err) continue } - validatedPlugins = append(validatedPlugins, r.bootstrappedPlugin) + validatedPlugins = append(validatedPlugins, bootstrappedPlugin) } l.log.Debug("Validated", "class", src.PluginClass(ctx), "duration", time.Since(st), "total", len(validatedPlugins)) diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go index bffac88fc0b..02b8b119884 100644 --- a/pkg/plugins/manager/pipeline/validation/steps.go +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -57,7 +57,7 @@ func newModuleJSValidator() *ModuleJSValidator { func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error { // CDN plugins are ignored because the module.js is guaranteed to exist - if p.Class == plugins.ClassCDN { + if p.FS.Type().CDN() { return nil } @@ -96,6 +96,11 @@ func newAngularDetector(cfg *config.PluginManagementCfg, angularInspector angula } func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error { + // CDN plugins are ignored because they should not be using Angular + if p.FS.Type().CDN() { + return nil + } + if p.IsExternalPlugin() { var err error diff --git a/pkg/plugins/manager/pluginfakes/fakes.go b/pkg/plugins/manager/pluginfakes/fakes.go index 614c95075ad..e449fd9bb2e 100644 --- a/pkg/plugins/manager/pluginfakes/fakes.go +++ b/pkg/plugins/manager/pluginfakes/fakes.go @@ -433,6 +433,7 @@ func (f *FakeActionSetRegistry) RegisterActionSets(_ context.Context, _ string, type FakePluginFS struct { OpenFunc func(name string) (fs.File, error) RemoveFunc func() error + TypeFunc func() plugins.FSType RelFunc func(string) (string, error) base string @@ -444,10 +445,6 @@ func NewFakePluginFS(base string) *FakePluginFS { } } -func (f *FakePluginFS) Type() string { - return "fake" -} - func (f *FakePluginFS) Open(name string) (fs.File, error) { if f.OpenFunc != nil { return f.OpenFunc(name) @@ -462,6 +459,13 @@ func (f *FakePluginFS) Rel(_ string) (string, error) { return "", nil } +func (f *FakePluginFS) Type() plugins.FSType { + if f.TypeFunc != nil { + return f.TypeFunc() + } + return "fake" +} + func (f *FakePluginFS) Base() string { return f.base } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 5b8125d15fa..d399268b464 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -345,7 +345,7 @@ func newPathSeparatorOverrideFS(sep string, ufs plugins.FS) (fsPathSeparatorFile }, nil } -func (f fsPathSeparatorFiles) Type() string { +func (f fsPathSeparatorFiles) Type() plugins.FSType { return f.FS.Type() } diff --git a/pkg/plugins/pluginassets/pluginassets_test.go b/pkg/plugins/pluginassets/pluginassets_test.go index 3dede1ce9f0..4f0520d115b 100644 --- a/pkg/plugins/pluginassets/pluginassets_test.go +++ b/pkg/plugins/pluginassets/pluginassets_test.go @@ -42,15 +42,6 @@ func TestLocalProvider_Module(t *testing.T) { }, expected: "public/plugins/external-plugin/module.js", }, - { - name: "CDN plugin should use standard path", - plugin: PluginInfo{ - JsonData: plugins.JSONData{ID: "cdn-plugin"}, - Class: plugins.ClassCDN, - FS: plugins.NewLocalFS("/cdn/plugins/cdn-plugin"), - }, - expected: "public/plugins/cdn-plugin/module.js", - }, } for _, tt := range tests { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 2c394dcc632..2d8241008d9 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -496,7 +496,7 @@ func (p *Plugin) IsCorePlugin() bool { } func (p *Plugin) IsExternalPlugin() bool { - return !p.IsCorePlugin() + return p.Class == ClassExternal } type Class string @@ -504,7 +504,6 @@ type Class string const ( ClassCore Class = "core" ClassExternal Class = "external" - ClassCDN Class = "cdn" ) func (c Class) String() string { diff --git a/pkg/plugins/test_utils.go b/pkg/plugins/test_utils.go index 15545bcedbd..f2a1dae3dc1 100644 --- a/pkg/plugins/test_utils.go +++ b/pkg/plugins/test_utils.go @@ -47,7 +47,7 @@ func (f inMemoryFS) Open(fn string) (fs.File, error) { return &inMemoryFile{path: fn, reader: bytes.NewReader(f.files[fn])}, nil } -func (f inMemoryFS) Type() string { +func (f inMemoryFS) Type() FSType { return "in-memory" } diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 2ac557edaaa..60f5ac757a1 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -1076,9 +1076,9 @@ func TestLoader_AngularClass(t *testing.T) { expAngularDetectionRun: true, }, { - name: "other-class plugin should run angular detection", - class: "CDN", // (enterprise-only class) - expAngularDetectionRun: true, + name: "other class plugin should skip angular detection", + class: "foo", + expAngularDetectionRun: false, }, } { t.Run(tc.name, func(t *testing.T) { @@ -1086,7 +1086,7 @@ func TestLoader_AngularClass(t *testing.T) { PluginClassFunc: func(ctx context.Context) plugins.Class { return tc.class }, - DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, + DiscoverFunc: sources.NewLocalSource(tc.class, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, } // if angularDetected = true, it means that the detection has run l := newLoaderWithOpts(t, &config.PluginManagementCfg{}, loaderDepOpts{ diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 7ed269332de..43f1dbb06e3 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -149,7 +149,7 @@ func ReportFSMetrics(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, err return p, nil } - metrics.SetPluginFSInformation(p.ID, p.FS.Type()) + metrics.SetPluginFSInformation(p.ID, string(p.FS.Type())) return p, nil } diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets.go b/pkg/services/pluginsintegration/pluginassets/pluginassets.go index cecf8ea78dc..4d9a7ec1a53 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets.go @@ -70,12 +70,12 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi // Since the parent plugin is not explicitly configured as script loading compatible, // If the plugin is either loaded from the CDN (via its parent) or contains Angular, we should use fetch - if s.cdnEnabled(p.Parent.ID, p.Class) || p.Angular.Detected { + if s.cdnEnabled(p.Parent.ID, p.FS) || p.Angular.Detected { return plugins.LoadingStrategyFetch } } - if !s.cdnEnabled(p.ID, p.Class) && !p.Angular.Detected { + if !s.cdnEnabled(p.ID, p.FS) && !p.Angular.Detected { return plugins.LoadingStrategyScript } @@ -142,7 +142,7 @@ func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSB // CDN plugins have the version as part of the URL, which acts as a cache-buster. // Needed due to: https://github.com/grafana/plugin-tools/pull/1426 // FS plugins build before this change will have SRI mismatch issues. - if !s.cdnEnabled(p.ID, p.Class) { + if !s.cdnEnabled(p.ID, p.FS) { return "", nil } @@ -185,8 +185,8 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { return false } -func (s *Service) cdnEnabled(pluginID string, class plugins.Class) bool { - return s.cdn.PluginSupported(pluginID) || class == plugins.ClassCDN +func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool { + return s.cdn.PluginSupported(pluginID) || fs.Type().CDN() } // convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go index a747e7e4e89..28f9d015fff 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -58,7 +59,7 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: futureVersion, }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { @@ -66,16 +67,16 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ // NOTE: cdn key is not set }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { - name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, // NOTE: cdn key is not set }), - plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal)), + plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { @@ -107,14 +108,14 @@ func TestService_Calculate(t *testing.T) { { name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular", pluginSettings: setting.PluginSettings{}, - plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin { + plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS()), func(p pluginstore.Plugin) pluginstore.Plugin { p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"} return p }), expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, is configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, and plugin is configured as CDN enabled", pluginSettings: newPluginSettings(pluginID, map[string]string{ "cdn": "true", CreatePluginVersionCfgKey: incompatVersion, @@ -127,7 +128,7 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, }), - plugin: newPlugin(pluginID, withAngular(true)), + plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyFetch, }, { @@ -140,19 +141,25 @@ func TestService_Calculate(t *testing.T) { expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has the CDN class", + name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, }), - plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassCDN)), + plugin: newPlugin(pluginID, withAngular(false), withFS( + &pluginfakes.FakePluginFS{ + TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }, + }, + )), expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: "invalidSemver", }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, } @@ -183,9 +190,9 @@ func TestService_ModuleHash(t *testing.T) { features *config.Features store []pluginstore.Plugin - // Can be used to configure plugin's class - // cdn class = loaded from CDN with no files on disk - // external class = files on disk but served from CDN only if cdn=true + // Can be used to configure plugin's fs + // fs cdn type = loaded from CDN with no files on disk + // fs local type = files on disk but served from CDN only if cdn=true plugin pluginstore.Plugin // When true, set cdn=true in config @@ -204,7 +211,7 @@ func TestService_ModuleHash(t *testing.T) { pluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), + withClass(plugins.ClassExternal), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -236,7 +243,6 @@ func TestService_ModuleHash(t *testing.T) { pluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: false}, @@ -261,7 +267,6 @@ func TestService_ModuleHash(t *testing.T) { parentPluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -269,7 +274,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))), withParent(parentPluginID), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -284,7 +288,6 @@ func TestService_ModuleHash(t *testing.T) { parentPluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -292,7 +295,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), withParent(parentPluginID), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -308,14 +310,12 @@ func TestService_ModuleHash(t *testing.T) { "grand-parent-app", withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))), - withClass(plugins.ClassCDN), ), newPlugin( "parent-datasource", withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))), withParent("grand-parent-app"), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -323,7 +323,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))), withParent("parent-datasource"), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -378,11 +377,17 @@ func TestService_ModuleHash(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var pluginSettings setting.PluginSettings if tc.cdn { - pluginSettings = newPluginSettings(pluginID, map[string]string{ - "cdn": "true", - }) - } else { - require.NotEqual(t, plugins.ClassCDN, tc.plugin.Class, "plugin should not have the CDN class because CDN is disabled") + pluginSettings = setting.PluginSettings{ + pluginID: { + "cdn": "true", + }, + parentPluginID: map[string]string{ + "cdn": "true", + }, + "grand-parent-app": map[string]string{ + "cdn": "true", + }, + } } features := tc.features if features == nil { @@ -439,8 +444,24 @@ func TestService_ModuleHash_Cache(t *testing.T) { withInfo(plugins.Info{Version: "1.0.0"}), withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), ) + + pCfg = &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.grafana.com", + PluginSettings: setting.PluginSettings{ + pluginID: { + "cdn": "true", + }, + }, + Features: config.Features{SriChecksEnabled: true}, + } + svc = ProvideService( + pCfg, + pluginscdn.ProvideService(pCfg), + signature.ProvideService(pCfg, statickey.New()), + pluginstore.NewFakePluginStore(), + ) + k := svc.moduleHashCacheKey(pV1) _, ok := svc.moduleHashCache.Load(k) @@ -461,7 +482,6 @@ func TestService_ModuleHash_Cache(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), // different fs for different hash withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ) mhV2 := svc.ModuleHash(context.Background(), pV2) require.NotEqual(t, mhV2, mhV1, "different version should have different hash") From 41276676eb62912a58cc05684e29986f5731563c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 20 Nov 2025 15:12:07 +0100 Subject: [PATCH 005/423] Provisioning: add retry logic for transient errors in Kubernetes client (#114215) * feat: add retry logic for transient errors in Kubernetes client Add retry wrapper for dynamic.ResourceInterface that automatically retries transient errors using Kubernetes' wait.ExponentialBackoff utility. - Implements retry logic with exponential backoff for all Kubernetes API operations - Detects transient errors: ServiceUnavailable, ServerTimeout, TooManyRequests, InternalError, Timeout, and network errors - Uses wait.ExponentialBackoff from k8s.io/apimachinery/pkg/util/wait - Respects context cancellation - Includes comprehensive unit tests Part of https://github.com/grafana/git-ui-sync-project/issues/634 * docs: add detailed documentation for defaultRetryBackoff Document when retry attempts will happen, what errors trigger retries, and the retry behavior (attempts, delays, exponential backoff, jitter). * feat: add logging and increase retry attempts for Kubernetes client - Add context logger to track retry attempts (Info for retries, Warn for exhaustion) - Increase retry attempts from 5 to 8 steps (~10 seconds total retry window) - Document when all retry attempts will fail: * API server completely unavailable/unreachable * Network connectivity issues persist beyond retry window * Consistent transient errors for entire retry duration * Context cancellation before retries complete * chore: update retry client documentation * fix: resolve linting issues in retry client - Replace type assertions with errors.As for wrapped errors - Remove deprecated Temporary() check (deprecated since Go 1.18) - Update tests to remove temporary error test case * fix: resolve staticcheck S1008 linting issue in retry_client.go Simplify return statement to use errors.As directly instead of if-return pattern --- .../apis/provisioning/resources/client.go | 6 +- .../provisioning/resources/retry_client.go | 296 ++++++++++ .../resources/retry_client_test.go | 507 ++++++++++++++++++ 3 files changed, 807 insertions(+), 2 deletions(-) create mode 100644 pkg/registry/apis/provisioning/resources/retry_client.go create mode 100644 pkg/registry/apis/provisioning/resources/retry_client_test.go diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go index f12bcf7b053..388e5311f72 100644 --- a/pkg/registry/apis/provisioning/resources/client.go +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -221,10 +221,11 @@ func (c *resourceClients) ForKind(ctx context.Context, gvk schema.GroupVersionKi return nil, schema.GroupVersionResource{}, err } } + baseClient := dynamic.Resource(gvr).Namespace(c.namespace) info = &clientInfo{ gvk: gvk, gvr: gvr, - client: dynamic.Resource(gvr).Namespace(c.namespace), + client: newRetryResourceInterface(baseClient, defaultRetryBackoff()), } c.byKind[gvk] = info c.byResource[gvr] = info @@ -274,10 +275,11 @@ func (c *resourceClients) ForResource(ctx context.Context, gvr schema.GroupVersi return nil, schema.GroupVersionKind{}, fmt.Errorf("getting kind for resource for %s: %w", gvr.String(), err) } } + baseClient := dynamic.Resource(gvr).Namespace(c.namespace) info = &clientInfo{ gvk: gvk, gvr: gvr, - client: dynamic.Resource(gvr).Namespace(c.namespace), + client: newRetryResourceInterface(baseClient, defaultRetryBackoff()), } c.byKind[gvk] = info c.byResource[gvr] = info diff --git a/pkg/registry/apis/provisioning/resources/retry_client.go b/pkg/registry/apis/provisioning/resources/retry_client.go new file mode 100644 index 00000000000..cc09443e018 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/retry_client.go @@ -0,0 +1,296 @@ +package resources + +import ( + "context" + "errors" + "net" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" +) + +// defaultRetryBackoff returns a default backoff configuration for retries. +// +// Retry attempts will happen when: +// - The Kubernetes API returns transient errors: ServiceUnavailable (503), ServerTimeout (504), +// TooManyRequests (429), InternalError (500), or Timeout errors +// - Network errors occur: connection timeouts, temporary network failures, or connection errors +// +// The retry behavior: +// - Total attempts: 8 (1 initial attempt + 7 retries) +// - Initial delay: 100ms before the first retry +// - Exponential backoff: delay doubles after each failed attempt (100ms → 200ms → 400ms → 800ms → 1.6s → 3.2s → 5s) +// - Maximum delay: capped at 5 seconds +// - Jitter: 10% randomization to prevent thundering herd problems +// - Total retry window: approximately 10 seconds from first attempt to last retry +// +// All attempts will fail when: +// - The Kubernetes API server is completely unavailable or unreachable +// - Network connectivity issues persist beyond the retry window (~10 seconds) +// - The API server returns transient errors consistently for the entire retry duration +// - Context cancellation occurs before retries complete +// +// Non-transient errors (e.g., NotFound, BadRequest, Forbidden) are not retried and returned immediately. +func defaultRetryBackoff() wait.Backoff { + return wait.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 8, // 1 initial attempt + 7 retries = 8 total attempts (~10s total retry window) + Cap: 5 * time.Second, + } +} + +// retryResourceInterface wraps a dynamic.ResourceInterface with retry logic for transient errors +type retryResourceInterface struct { + client dynamic.ResourceInterface + backoff wait.Backoff +} + +// newRetryResourceInterface creates a new ResourceInterface wrapper with retry logic +func newRetryResourceInterface(client dynamic.ResourceInterface, backoff wait.Backoff) dynamic.ResourceInterface { + return &retryResourceInterface{ + client: client, + backoff: backoff, + } +} + +// isTransientError determines if an error is transient and should be retried +func isTransientError(err error) bool { + if err == nil { + return false + } + + // Check for Kubernetes API transient errors + if apierrors.IsServiceUnavailable(err) { + return true + } + if apierrors.IsServerTimeout(err) { + return true + } + if apierrors.IsTooManyRequests(err) { + return true + } + if apierrors.IsInternalError(err) { + return true + } + if apierrors.IsTimeout(err) { + return true + } + + // Check for network errors + var netErr net.Error + if errors.As(err, &netErr) { + if netErr.Timeout() { + return true + } + } + + // Check for connection errors + var opErr *net.OpError + return errors.As(err, &opErr) +} + +// retryWithBackoff executes a function with exponential backoff retry logic using wait.ExponentialBackoff +func (r *retryResourceInterface) retryWithBackoff(ctx context.Context, fn func() error) error { + var lastErr error + attempt := 0 + logger := logging.FromContext(ctx) + + err := wait.ExponentialBackoff(r.backoff, func() (bool, error) { + attempt++ + + // Check if context is cancelled + if ctx.Err() != nil { + logger.Debug("Retry cancelled due to context cancellation", "attempt", attempt) + return false, ctx.Err() + } + + err := fn() + if err == nil { + if attempt > 1 { + logger.Debug("Operation succeeded after retry", "attempt", attempt) + } + return true, nil // success, stop retrying + } + + // If not a transient error, return immediately without retrying + if !isTransientError(err) { + logger.Debug("Non-transient error, not retrying", "attempt", attempt, "error", err) + return false, err + } + + // Transient error, retry + lastErr = err + logger.Info("Transient error encountered, retrying", "attempt", attempt, "max_attempts", r.backoff.Steps, "error", err) + return false, nil + }) + + // If wait.ExponentialBackoff returned an error, it means we exhausted retries + if err != nil { + if lastErr != nil { + logger.Warn("All retry attempts exhausted", "total_attempts", attempt, "error", lastErr) + return lastErr + } + logger.Warn("All retry attempts exhausted", "total_attempts", attempt, "error", err) + return err + } + + return nil +} + +// Create implements dynamic.ResourceInterface +func (r *retryResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Create(ctx, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Update implements dynamic.ResourceInterface +func (r *retryResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Update(ctx, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// UpdateStatus implements dynamic.ResourceInterface +func (r *retryResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.UpdateStatus(ctx, obj, options) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Delete implements dynamic.ResourceInterface +func (r *retryResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { + return r.retryWithBackoff(ctx, func() error { + return r.client.Delete(ctx, name, options, subresources...) + }) +} + +// DeleteCollection implements dynamic.ResourceInterface +func (r *retryResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error { + return r.retryWithBackoff(ctx, func() error { + return r.client.DeleteCollection(ctx, options, listOptions) + }) +} + +// Get implements dynamic.ResourceInterface +func (r *retryResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Get(ctx, name, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// List implements dynamic.ResourceInterface +func (r *retryResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { + var result *unstructured.UnstructuredList + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.List(ctx, opts) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Watch implements dynamic.ResourceInterface +func (r *retryResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + // Watch operations are long-lived and shouldn't be retried in the same way + // Return the watch interface directly + return r.client.Watch(ctx, opts) +} + +// Patch implements dynamic.ResourceInterface +func (r *retryResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Patch(ctx, name, pt, data, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Apply implements dynamic.ResourceInterface +func (r *retryResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Apply(ctx, name, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// ApplyStatus implements dynamic.ResourceInterface +func (r *retryResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.ApplyStatus(ctx, name, obj, options) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} diff --git a/pkg/registry/apis/provisioning/resources/retry_client_test.go b/pkg/registry/apis/provisioning/resources/retry_client_test.go new file mode 100644 index 00000000000..dab40bcb001 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/retry_client_test.go @@ -0,0 +1,507 @@ +package resources + +import ( + "context" + "errors" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" +) + +func TestIsTransientError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "service unavailable", + err: apierrors.NewServiceUnavailable("service unavailable"), + expected: true, + }, + { + name: "server timeout", + err: apierrors.NewServerTimeout(schema.GroupResource{}, "operation", 0), + expected: true, + }, + { + name: "too many requests", + err: apierrors.NewTooManyRequests("too many requests", 0), + expected: true, + }, + { + name: "internal error", + err: apierrors.NewInternalError(errors.New("internal error")), + expected: true, + }, + { + name: "network timeout error", + err: &net.DNSError{Err: "timeout", IsTimeout: true}, + expected: true, + }, + // Note: Temporary() is deprecated in Go 1.18+, so we no longer check for temporary errors + // Timeout errors are still checked and will be retried + { + name: "network op error", + err: &net.OpError{Op: "read", Err: errors.New("connection refused")}, + expected: true, + }, + { + name: "not found error", + err: apierrors.NewNotFound(schema.GroupResource{}, "resource"), + expected: false, + }, + { + name: "bad request error", + err: apierrors.NewBadRequest("bad request"), + expected: false, + }, + { + name: "forbidden error", + err: apierrors.NewForbidden(schema.GroupResource{}, "resource", errors.New("forbidden")), + expected: false, + }, + { + name: "generic error", + err: errors.New("generic error"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isTransientError(tt.err) + assert.Equal(t, tt.expected, result, "isTransientError(%v) = %v, want %v", tt.err, result, tt.expected) + }) + } +} + +func TestRetryResourceInterface_Create(t *testing.T) { + tests := []struct { + name string + setupMock func(*MockDynamicResourceInterface) + backoff wait.Backoff + expectedCalls int + expectError bool + }{ + { + name: "success on first attempt", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{}, nil).Once() + }, + backoff: defaultRetryBackoff(), + expectedCalls: 1, + expectError: false, + }, + { + name: "success after transient errors", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Twice() + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + }, + backoff: wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }, + expectedCalls: 3, + expectError: false, + }, + { + name: "max retries exceeded", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")) + }, + backoff: wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 3, // Only 3 steps = 2 retries + 1 initial + Cap: 100 * time.Millisecond, + }, + expectedCalls: 3, + expectError: true, + }, + { + name: "non-transient error - no retry", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewBadRequest("bad request")).Once() + }, + backoff: defaultRetryBackoff(), + expectedCalls: 1, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + tt.setupMock(mockClient) + + retryClient := newRetryResourceInterface(mockClient, tt.backoff) + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + mockClient.AssertNumberOfCalls(t, "Create", tt.expectedCalls) + }) + } +} + +func TestRetryResourceInterface_Update(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.Update(context.Background(), obj, metav1.UpdateOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Update", 2) +} + +func TestRetryResourceInterface_Get(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServerTimeout(schema.GroupResource{}, "operation", 0)).Twice() + mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.Get(context.Background(), "test-resource", metav1.GetOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Get", 3) +} + +func TestRetryResourceInterface_Delete(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(apierrors.NewTooManyRequests("too many requests", 0)).Once() + mockClient.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + err := retryClient.Delete(context.Background(), "test-resource", metav1.DeleteOptions{}) + + assert.NoError(t, err) + mockClient.AssertNumberOfCalls(t, "Delete", 2) +} + +func TestRetryResourceInterface_List(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("List", mock.Anything, mock.Anything). + Return(nil, apierrors.NewInternalError(errors.New("internal error"))).Once() + mockClient.On("List", mock.Anything, mock.Anything). + Return(&unstructured.UnstructuredList{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.List(context.Background(), metav1.ListOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "List", 2) +} + +func TestRetryResourceInterface_Patch(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, &net.OpError{Op: "read", Err: errors.New("connection refused")}).Once() + mockClient.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.Patch(context.Background(), "test-resource", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Patch", 2) +} + +func TestRetryResourceInterface_Apply(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Apply", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("Apply", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.Apply(context.Background(), "test-resource", obj, metav1.ApplyOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Apply", 2) +} + +func TestRetryResourceInterface_UpdateStatus(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("UpdateStatus", mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("UpdateStatus", mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.UpdateStatus(context.Background(), obj, metav1.UpdateOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "UpdateStatus", 2) +} + +func TestRetryResourceInterface_ApplyStatus(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("ApplyStatus", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("ApplyStatus", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.ApplyStatus(context.Background(), "test-resource", obj, metav1.ApplyOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "ApplyStatus", 2) +} + +func TestRetryResourceInterface_DeleteCollection(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything). + Return(apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + err := retryClient.DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}) + + assert.NoError(t, err) + mockClient.AssertNumberOfCalls(t, "DeleteCollection", 2) +} + +func TestRetryResourceInterface_Watch(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockWatch := &mockWatch{} + mockClient.On("Watch", mock.Anything, mock.Anything).Return(mockWatch, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, defaultRetryBackoff()) + + watch, err := retryClient.Watch(context.Background(), metav1.ListOptions{}) + + assert.NoError(t, err) + assert.Equal(t, mockWatch, watch) + // Watch should not retry, so only one call + mockClient.AssertNumberOfCalls(t, "Watch", 1) +} + +func TestRetryResourceInterface_ContextCancellation(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Maybe() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 1 * time.Second, + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(ctx, obj, metav1.CreateOptions{}) + + assert.Error(t, err) + assert.Equal(t, context.Canceled, err) + // Should not retry after context cancellation + mockClient.AssertNumberOfCalls(t, "Create", 0) +} + +func TestRetryResourceInterface_ExponentialBackoff(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + callCount := 0 + + // Set up expectations for multiple calls + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Twice() + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(&unstructured.Unstructured{}, nil).Once() + + start := time.Now() + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 50 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 1 * time.Second, + }) + + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + + duration := time.Since(start) + + assert.NoError(t, err) + assert.Equal(t, 3, callCount) + // Should have waited at least initialDelay + (initialDelay * multiplier) = 50ms + 100ms = 150ms + assert.GreaterOrEqual(t, duration, 100*time.Millisecond) + // But not too long (with some buffer for jitter) + assert.Less(t, duration, 1*time.Second) +} + +func TestRetryResourceInterface_MaxDelayRespected(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + callCount := 0 + + // Set up expectations for multiple calls - always return transient error + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")). + Maybe() // Allow unlimited calls + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 50 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, // 5 total attempts + Cap: 200 * time.Millisecond, // Max delay is 200ms (cap should prevent exponential growth beyond this) + }) + + start := time.Now() + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + duration := time.Since(start) + + assert.Error(t, err) + // Should have retried multiple times + assert.GreaterOrEqual(t, callCount, 2, "should have retried at least once") + // Verify that the delay was capped - if it wasn't capped, duration would be much longer + // With cap at 200ms and Steps=5, max total time should be reasonable + // The cap ensures delays don't grow exponentially beyond 200ms + assert.Less(t, duration, 2*time.Second, "duration should be reasonable due to cap") +} + +func TestDefaultRetryBackoff(t *testing.T) { + backoff := defaultRetryBackoff() + + assert.Equal(t, 100*time.Millisecond, backoff.Duration) + assert.Equal(t, 2.0, backoff.Factor) + assert.Equal(t, 0.1, backoff.Jitter) + assert.Equal(t, 8, backoff.Steps) // Updated to 8 steps for ~10s total retry window + assert.Equal(t, 5*time.Second, backoff.Cap) +} + +// mockWatch implements watch.Interface for testing +type mockWatch struct{} + +func (m *mockWatch) Stop() {} + +func (m *mockWatch) ResultChan() <-chan watch.Event { + return make(chan watch.Event) +} + +var _ watch.Interface = (*mockWatch)(nil) From 834f1c5e98e75083e777d0cc9ed46813fa0214f1 Mon Sep 17 00:00:00 2001 From: Kristina Demeshchik Date: Thu, 20 Nov 2025 09:13:19 -0500 Subject: [PATCH 006/423] TS checker: Increase memory limit (#114236) Increase ts checker memory limit --- scripts/webpack/webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 050f54a2c3a..f48aada0ed7 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -135,7 +135,7 @@ module.exports = (env = {}) => { async: true, // don't block webpack emit typescript: { mode: 'write-references', - memoryLimit: 5096, + memoryLimit: 8192, diagnosticOptions: { semantic: true, syntactic: true, From 7db72e03b55f8293061e387aebea4ad4ce1e3712 Mon Sep 17 00:00:00 2001 From: Robby Milo Date: Thu, 20 Nov 2025 07:39:10 -0700 Subject: [PATCH 007/423] remove image rendering docs (#114217) --- .../setup-grafana/image-rendering/_index.md | 617 ------------------ .../image-rendering/monitoring/index.md | 221 ------- .../image-rendering/troubleshooting/index.md | 156 ----- 3 files changed, 994 deletions(-) delete mode 100644 docs/sources/setup-grafana/image-rendering/_index.md delete mode 100644 docs/sources/setup-grafana/image-rendering/monitoring/index.md delete mode 100644 docs/sources/setup-grafana/image-rendering/troubleshooting/index.md diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md deleted file mode 100644 index 32df8dc2154..00000000000 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ /dev/null @@ -1,617 +0,0 @@ ---- -aliases: - - ../administration/image_rendering/ - - ../image-rendering/ -description: Image rendering -keywords: - - grafana - - image - - rendering - - plugin -labels: - products: - - enterprise - - oss -title: Set up image rendering -weight: 1000 ---- - -# Set up image rendering - -Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in alert notifications, [PDF export](../../dashboards/create-reports/#export-dashboard-as-pdf), and [Reporting](../../dashboards/create-reports/). PDF Export and Reporting are available only in [Grafana Enterprise](../../introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). - -While an image is being rendered, the PNG image is temporarily written to the file system. When the image is rendered, the PNG image is temporarily written to the `png` folder in the Grafana `data` folder. - -A background job runs every 10 minutes and removes temporary images. You can configure how long an image should be stored before being removed by configuring the [temp_data_lifetime](../configure-grafana/#temp_data_lifetime) setting. - -You can also render a PNG by hovering over the panel to display the actions menu in the top-right corner, and then clicking **Share > Share link**. The **Render image** option is displayed in the link settings. - -## Alerting and render limits - -Alert notifications can include images, but rendering many images at the same time can overload the server where the renderer is running. For instructions of how to configure this, see [max_concurrent_screenshots](../configure-grafana/#max_concurrent_screenshots). - -## Install Grafana Image Renderer plugin - -{{< admonition type="caution" >}} -Starting with Grafana v12.2, the Grafana Image Renderer plugin is deprecated and is no longer maintained. - -Instead, use the Grafana Image Renderer remote rendering service. -{{< /admonition >}} - -{{< admonition type="note" >}} -All PhantomJS support has been removed. Instead, use the Grafana Image Renderer plugin or remote rendering service. -{{< /admonition >}} - -To install the plugin, refer to the [Grafana Image Renderer Installation instructions](/grafana/plugins/grafana-image-renderer/?tab=installation#installation). - -### Memory requirements - -Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Grafana recommends a minimum of 16GB of free memory on the system rendering images. - -Rendering multiple images in parallel requires an even bigger memory footprint. You can use the remote rendering service in order to render images on a remote system, so your local system resources are not affected. - -## Configuration - -The Grafana Image Renderer plugin has a number of configuration options that are used in plugin or remote rendering modes. - -In plugin mode, you can specify them directly in the [Grafana configuration file](../configure-grafana/#plugingrafana-image-renderer). - -In remote rendering mode, you can specify them in a `.json` [configuration file](#configuration-file) or, for some of them, you can override the configuration defaults using environment variables. - -### Configuration file - -You can update your settings by using a configuration file, see [default.json](https://github.com/grafana/grafana-image-renderer/tree/master/default.json) for defaults. Note that any configured environment variable takes precedence over configuration file settings. - -You can volume mount your custom configuration file when starting the docker container: - -```bash -docker run -d --name=renderer --network=host -v /some/path/config.json:/home/nonroot/config.json grafana/grafana-image-renderer:latest -``` - -You can see a docker-compose example using a custom configuration file [here](https://github.com/grafana/grafana-image-renderer/tree/master/devenv/docker/custom-config). - -{{< admonition type="note" >}} -The configuration files were located in `/usr/src/app` up until v4.0.0 and later. -After this point, they are located in `/home/nonroot`. -{{< /admonition >}} - -### Security - -{{< admonition type="note" >}} -This feature is available in Image Renderer v3.6.1 and later. -{{< /admonition >}} - -You can restrict access to the rendering endpoint by specifying a secret token. The token should be configured in the Grafana configuration file and the renderer configuration file. This token is important when you run the plugin in remote rendering mode. - -Renderer versions v3.6.1 or later require a Grafana version with this feature. These include: - -- Grafana v9.1.2 or later -- Grafana v9.0.8 or later patch releases -- Grafana v8.5.11 or later patch releases -- Grafana v8.4.11 or later patch releases -- Grafana v8.3.11 or later patch releases - -```bash -AUTH_TOKEN=- -``` - -```json -{ - "service": { - "security": { - "authToken": "-" - } - } -} -``` - -See [Grafana configuration](../configure-grafana/#renderer_token) for how to configure the token in Grafana. - -### Rendering mode - -You can instruct how headless browser instances are created by configuring a rendering mode. Default is `default`, other supported values are `clustered` and `reusable`. - -#### Default - -Default mode will create a new browser instance on each request. When handling multiple concurrent requests, this mode increases memory usage as it will launch multiple browsers at the same time. If you want to set a maximum number of browser to open, you'll need to use the [clustered mode](#clustered). - -{{< admonition type="note" >}} -When using the `default` mode, it's recommended to not remove the default Chromium flag `--disable-gpu`. When receiving a lot of concurrent requests, not using this flag can cause Puppeteer `newPage` function to freeze, causing request timeouts and leaving browsers open. -{{< /admonition >}} - -```bash -RENDERING_MODE=default -``` - -```json -{ - "rendering": { - "mode": "default" - } -} -``` - -#### Clustered - -With the `clustered` mode, you can configure how many browser instances or incognito pages can execute concurrently. Default is `browser` and will ensure a maximum amount of browser instances can execute concurrently. Mode `context` will ensure a maximum amount of incognito pages can execute concurrently. You can also configure the maximum concurrency allowed, which per default is `5`, and the maximum duration of a rendering request, which per default is `30` seconds. - -Using a cluster of incognito pages is more performant and consumes less CPU and memory than a cluster of browsers. However, if one page crashes it can bring down the entire browser with it (making all the rendering requests happening at the same time fail). Also, each page isn't guaranteed to be totally clean (cookies and storage might bleed-through as seen [here](https://bugs.chromium.org/p/chromium/issues/detail?id=754576)). - -```bash -RENDERING_MODE=clustered -RENDERING_CLUSTERING_MODE=browser -RENDERING_CLUSTERING_MAX_CONCURRENCY=5 -RENDERING_CLUSTERING_TIMEOUT=30 -``` - -```json -{ - "rendering": { - "mode": "clustered", - "clustering": { - "mode": "browser", - "maxConcurrency": 5, - "timeout": 30 - } - } -} -``` - -#### Reusable (experimental) - -When using the rendering mode `reusable`, one browser instance will be created and reused. A new incognito page will be opened for each request. This mode is experimental since, if the browser instance crashes, it will not automatically be restarted. You can achieve a similar behavior using `clustered` mode with a high `maxConcurrency` setting. - -```bash -RENDERING_MODE=reusable -``` - -```json -{ - "rendering": { - "mode": "reusable" - } -} -``` - -#### Optimize the performance, CPU and memory usage of the image renderer - -The performance and resources consumption of the different modes depend a lot on the number of concurrent requests your service is handling. To understand how many concurrent requests your service is handling, [monitor your image renderer service](monitoring/). - -With no concurrent requests, the different modes show very similar performance and CPU / memory usage. - -When handling concurrent requests, we see the following trends: - -- To improve performance and reduce CPU and memory consumption, use [clustered](#clustered) mode with `RENDERING_CLUSTERING_MODE` set as `context`. This parallelizes incognito pages instead of browsers. -- If you use the [clustered](#clustered) mode with a `maxConcurrency` setting below your average number of concurrent requests, performance will drop as the rendering requests will need to wait for the other to finish before getting access to an incognito page / browser. - -To achieve better performance, monitor the machine on which your service is running. If you don't have enough memory and / or CPU, every rendering step will be slower than usual, increasing the duration of every rendering request. - -### Other available settings - -{{< admonition type="note" >}} -Please note that not all settings are available using environment variables. If there is no example using environment variable below, it means that you need to update the configuration file. -{{< /admonition >}} - -#### HTTP host - -Change the listening host of the HTTP server. Default is unset and will use the local host. - -```bash -HTTP_HOST=localhost -``` - -```json -{ - "service": { - "host": "localhost" - } -} -``` - -#### HTTP port - -Change the listening port of the HTTP server. Default is `8081`. Setting `0` will automatically assign a port not in use. - -```bash -HTTP_PORT=0 -``` - -```json -{ - "service": { - "port": 0 - } -} -``` - -#### HTTP protocol - -{{< admonition type="note" >}} -HTTPS protocol is supported in the image renderer v3.11.0 and later. -{{< /admonition >}} - -Change the protocol of the server, it can be `http` or `https`. Default is `http`. - -```bash -HTTP_PROTOCOL=https -``` - -```json -{ - "service": { - "protocol": "https" - } -} -``` - -#### HTTPS certificate and key file - -Path to the image renderer certificate and key file used to start an HTTPS server. - -```bash -HTTP_CERT_FILE=./path/to/cert -HTTP_CERT_KEY=./path/to/key -``` - -```json -{ - "service": { - "certFile": "./path/to/cert", - "certKey": "./path/to/key" - } -} -``` - -#### HTTPS min TLS version - -Minimum TLS version allowed. Accepted values are: `TLSv1.2`, `TLSv1.3`. Default is `TLSv1.2`. - -```bash -HTTP_MIN_TLS_VERSION=TLSv1.2 -``` - -```json -{ - "service": { - "minTLSVersion": "TLSv1.2" - } -} -``` - -#### Enable Prometheus metrics - -You can enable [Prometheus](https://prometheus.io/) metrics endpoint `/metrics` using the environment variable `ENABLE_METRICS`. Node.js and render request duration metrics are included, see [Enable Prometheus metrics endpoint](monitoring/#enable-prometheus-metrics-endpoint) for details. - -Default is `false`. - -```bash -ENABLE_METRICS=true -``` - -```json -{ - "service": { - "metrics": { - "enabled": true, - "collectDefaultMetrics": true, - "requestDurationBuckets": [1, 5, 7, 9, 11, 13, 15, 20, 30] - } - } -} -``` - -#### Enable detailed timing metrics - -With the [Prometheus metrics enabled](#enable-prometheus-metrics), you can also enable detailed metrics to get the duration of every rendering step. - -Default is `false`. - -```bash -# Available from v3.9.0+ -RENDERING_TIMING_METRICS=true -``` - -```json -{ - "rendering": { - "timingMetrics": true - } -} -``` - -#### Log level - -Change the log level. Default is `info` and will include log messages with level `error`, `warning` and `info`. - -```bash -LOG_LEVEL=debug -``` - -```json -{ - "service": { - "logging": { - "level": "debug", - "console": { - "json": false, - "colorize": true - } - } - } -} -``` - -#### Verbose logging - -Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is `false` and will only capture and log error messages. When enabled (`true`) debug messages are captured and logged as well. - -Note that you need to change log level to `debug`, see above, for the verbose information to be included in the logs. - -```bash -RENDERING_VERBOSE_LOGGING=true -``` - -```json -{ - "rendering": { - "verboseLogging": true - } -} -``` - -#### Capture browser output - -Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service. Default is `false`. -This can be useful to enable (`true`) when troubleshooting. - -```bash -RENDERING_DUMPIO=true -``` - -```json -{ - "rendering": { - "dumpio": true - } -} -``` - -#### Tracing - -{{< admonition type="note" >}} -Tracing is supported in the image renderer v3.12.6 and later. -{{< /admonition >}} - -Set the tracing URL to enable OpenTelemetry Tracing. The default is empty (disabled). -You can also configure the service name that will be set in the traces. The default is `grafana-image-renderer`. - -```bash -RENDERING_TRACING_URL="http://localhost:4318/v1/traces" -``` - -```json -{ - "rendering": { - "tracing": { - "url": "http://localhost:4318/v1/traces", - "serviceName": "grafana-renderer" - } - } -} -``` - -#### Custom Chrome/Chromium - -If you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) -installed on your system, then you can use this instead of the pre-packaged version of Chromium. - -{{< admonition type="note" >}} -Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not compatible with the [Grafana Image renderer plugin](/grafana/plugins/grafana-image-renderer). -{{< /admonition >}} - -You need to make sure that the Chrome/Chromium executable is available for the Grafana/image rendering service process. - -```bash -CHROME_BIN="/usr/bin/chromium-browser" -``` - -```json -{ - "rendering": { - "chromeBin": "/usr/bin/chromium-browser" - } -} -``` - -#### Start browser with additional arguments - -Additional arguments to pass to the headless browser instance. Defaults are `--no-sandbox,--disable-gpu`. The list of Chromium flags can be found [here](https://peter.sh/experiments/chromium-command-line-switches/) and the list of flags used as defaults by Puppeteer can be found [there](https://cri.dev/posts/2020-04-04-Full-list-of-Chromium-Puppeteer-flags/). Multiple arguments is separated with comma-character. - -```bash -RENDERING_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-accelerated-2d-canvas,--disable-gpu,--window-size=1280x758 -``` - -```json -{ - "rendering": { - "args": [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-accelerated-2d-canvas", - "--disable-gpu", - "--window-size=1280x758" - ] - } -} -``` - -#### Ignore HTTPS errors - -Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. -Due to the security risk it's not recommended to ignore HTTPS errors. - -```bash -IGNORE_HTTPS_ERRORS=true -``` - -```json -{ - "rendering": { - "ignoresHttpsErrors": true - } -} -``` - -#### Default timezone - -Instruct headless browser instance to use a default timezone when not provided by Grafana, .e.g. when rendering panel image of alert. See [ICU’s metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs. Fallbacks to `TZ` environment variable if not set. - -```bash -BROWSER_TZ=Europe/Stockholm -``` - -```json -{ - "rendering": { - "timezone": "Europe/Stockholm" - } -} -``` - -#### Default language - -Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert. -Refer to the HTTP header Accept-Language to understand how to format this value. - -```bash -# Available from v3.9.0+ -RENDERING_LANGUAGE="fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" -``` - -```json -{ - "rendering": { - "acceptLanguage": "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" - } -} -``` - -#### Viewport width - -Default viewport width when width is not specified in the rendering request. Default is `1000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_WIDTH=1000 -``` - -```json -{ - "rendering": { - "width": 1000 - } -} -``` - -#### Viewport height - -Default viewport height when height is not specified in the rendering request. Default is `500`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_HEIGHT=500 -``` - -```json -{ - "rendering": { - "height": 500 - } -} -``` - -#### Viewport maximum width - -Limit the maximum viewport width that can be requested. Default is `3000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_WIDTH=1000 -``` - -```json -{ - "rendering": { - "maxWidth": 1000 - } -} -``` - -#### Viewport maximum height - -Limit the maximum viewport height that can be requested. Default is `3000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_HEIGHT=500 -``` - -```json -{ - "rendering": { - "maxHeight": 500 - } -} -``` - -#### Device scale factor - -Specify default device scale factor for rendering images. `2` is enough for monitor resolutions, `4` would be better for printed material. Setting a higher value affects performance and memory. Default is `1`. -This can be overridden in the rendering request. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_DEVICE_SCALE_FACTOR=2 -``` - -```json -{ - "rendering": { - "deviceScaleFactor": 2 - } -} -``` - -#### Maximum device scale factor - -Limit the maximum device scale factor that can be requested. Default is `4`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_DEVICE_SCALE_FACTOR=4 -``` - -```json -{ - "rendering": { - "maxDeviceScaleFactor": 4 - } -} -``` - -#### Page zoom level - -The following command sets a page zoom level. The default value is `1`. A value of `1.5` equals 150% zoom. - -```bash -RENDERING_VIEWPORT_PAGE_ZOOM_LEVEL=1 -``` - -```json -{ - "rendering": { - "pageZoomLevel": 1 - } -} -``` diff --git a/docs/sources/setup-grafana/image-rendering/monitoring/index.md b/docs/sources/setup-grafana/image-rendering/monitoring/index.md deleted file mode 100644 index ad4334f52e3..00000000000 --- a/docs/sources/setup-grafana/image-rendering/monitoring/index.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -aliases: - - ../../image-rendering/monitoring/ -description: Image rendering monitoring -keywords: - - grafana - - image - - rendering - - plugin - - monitoring -labels: - products: - - enterprise - - oss -title: Monitor the image renderer -weight: 100 ---- - -# Monitor the image renderer - -Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Monitoring your service can help you allocate the right amount of resources to your rendering service and set the right [rendering mode](../#rendering-mode). - -## Enable Prometheus metrics endpoint - -Configure this service to expose a Prometheus metrics endpoint. For information on how to configure and monitor this service using Prometheus as a data source, refer to [Grafana Image Rendering Service dashboard](/grafana/dashboards/12203). - -**Metrics endpoint output example:** - -``` -# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds. -# TYPE process_cpu_user_seconds_total counter -process_cpu_user_seconds_total 0.536 1579444523566 - -# HELP process_cpu_system_seconds_total Total system CPU time spent in seconds. -# TYPE process_cpu_system_seconds_total counter -process_cpu_system_seconds_total 0.064 1579444523566 - -# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. -# TYPE process_cpu_seconds_total counter -process_cpu_seconds_total 0.6000000000000001 1579444523566 - -# HELP process_start_time_seconds Start time of the process since unix epoch in seconds. -# TYPE process_start_time_seconds gauge -process_start_time_seconds 1579444433 - -# HELP process_resident_memory_bytes Resident memory size in bytes. -# TYPE process_resident_memory_bytes gauge -process_resident_memory_bytes 52686848 1579444523568 - -# HELP process_virtual_memory_bytes Virtual memory size in bytes. -# TYPE process_virtual_memory_bytes gauge -process_virtual_memory_bytes 2055344128 1579444523568 - -# HELP process_heap_bytes Process heap size in bytes. -# TYPE process_heap_bytes gauge -process_heap_bytes 1996390400 1579444523568 - -# HELP process_open_fds Number of open file descriptors. -# TYPE process_open_fds gauge -process_open_fds 31 1579444523567 - -# HELP process_max_fds Maximum number of open file descriptors. -# TYPE process_max_fds gauge -process_max_fds 1573877 - -# HELP nodejs_eventloop_lag_seconds Lag of event loop in seconds. -# TYPE nodejs_eventloop_lag_seconds gauge -nodejs_eventloop_lag_seconds 0.000915922 1579444523567 - -# HELP nodejs_active_handles Number of active libuv handles grouped by handle type. Every handle type is C++ class name. -# TYPE nodejs_active_handles gauge -nodejs_active_handles{type="WriteStream"} 2 1579444523566 -nodejs_active_handles{type="Server"} 1 1579444523566 -nodejs_active_handles{type="Socket"} 9 1579444523566 -nodejs_active_handles{type="ChildProcess"} 2 1579444523566 - -# HELP nodejs_active_handles_total Total number of active handles. -# TYPE nodejs_active_handles_total gauge -nodejs_active_handles_total 14 1579444523567 - -# HELP nodejs_active_requests Number of active libuv requests grouped by request type. Every request type is C++ class name. -# TYPE nodejs_active_requests gauge -nodejs_active_requests{type="FSReqCallback"} 2 - -# HELP nodejs_active_requests_total Total number of active requests. -# TYPE nodejs_active_requests_total gauge -nodejs_active_requests_total 2 1579444523567 - -# HELP nodejs_heap_size_total_bytes Process heap size from node.js in bytes. -# TYPE nodejs_heap_size_total_bytes gauge -nodejs_heap_size_total_bytes 13725696 1579444523567 - -# HELP nodejs_heap_size_used_bytes Process heap size used from node.js in bytes. -# TYPE nodejs_heap_size_used_bytes gauge -nodejs_heap_size_used_bytes 12068008 1579444523567 - -# HELP nodejs_external_memory_bytes Nodejs external memory size in bytes. -# TYPE nodejs_external_memory_bytes gauge -nodejs_external_memory_bytes 1728962 1579444523567 - -# HELP nodejs_heap_space_size_total_bytes Process heap space size total from node.js in bytes. -# TYPE nodejs_heap_space_size_total_bytes gauge -nodejs_heap_space_size_total_bytes{space="read_only"} 262144 1579444523567 -nodejs_heap_space_size_total_bytes{space="new"} 1048576 1579444523567 -nodejs_heap_space_size_total_bytes{space="old"} 9809920 1579444523567 -nodejs_heap_space_size_total_bytes{space="code"} 425984 1579444523567 -nodejs_heap_space_size_total_bytes{space="map"} 1052672 1579444523567 -nodejs_heap_space_size_total_bytes{space="large_object"} 1077248 1579444523567 -nodejs_heap_space_size_total_bytes{space="code_large_object"} 49152 1579444523567 -nodejs_heap_space_size_total_bytes{space="new_large_object"} 0 1579444523567 - -# HELP nodejs_heap_space_size_used_bytes Process heap space size used from node.js in bytes. -# TYPE nodejs_heap_space_size_used_bytes gauge -nodejs_heap_space_size_used_bytes{space="read_only"} 32296 1579444523567 -nodejs_heap_space_size_used_bytes{space="new"} 601696 1579444523567 -nodejs_heap_space_size_used_bytes{space="old"} 9376600 1579444523567 -nodejs_heap_space_size_used_bytes{space="code"} 286688 1579444523567 -nodejs_heap_space_size_used_bytes{space="map"} 704320 1579444523567 -nodejs_heap_space_size_used_bytes{space="large_object"} 1064872 1579444523567 -nodejs_heap_space_size_used_bytes{space="code_large_object"} 3552 1579444523567 -nodejs_heap_space_size_used_bytes{space="new_large_object"} 0 1579444523567 - -# HELP nodejs_heap_space_size_available_bytes Process heap space size available from node.js in bytes. -# TYPE nodejs_heap_space_size_available_bytes gauge -nodejs_heap_space_size_available_bytes{space="read_only"} 229576 1579444523567 -nodejs_heap_space_size_available_bytes{space="new"} 445792 1579444523567 -nodejs_heap_space_size_available_bytes{space="old"} 417712 1579444523567 -nodejs_heap_space_size_available_bytes{space="code"} 20576 1579444523567 -nodejs_heap_space_size_available_bytes{space="map"} 343632 1579444523567 -nodejs_heap_space_size_available_bytes{space="large_object"} 0 1579444523567 -nodejs_heap_space_size_available_bytes{space="code_large_object"} 0 1579444523567 -nodejs_heap_space_size_available_bytes{space="new_large_object"} 1047488 1579444523567 - -# HELP nodejs_version_info Node.js version info. -# TYPE nodejs_version_info gauge -nodejs_version_info{version="v14.16.1",major="14",minor="16",patch="1"} 1 - -# HELP grafana_image_renderer_service_http_request_duration_seconds duration histogram of http responses labeled with: status_code -# TYPE grafana_image_renderer_service_http_request_duration_seconds histogram -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="1",status_code="200"} 0 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="5",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="7",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="9",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="11",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="13",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="15",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="20",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="30",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="+Inf",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_sum{status_code="200"} 10.492873834 -grafana_image_renderer_service_http_request_duration_seconds_count{status_code="200"} 4 - -# HELP up 1 = up, 0 = not up -# TYPE up gauge -up 1 - -# HELP grafana_image_renderer_http_request_in_flight A gauge of requests currently being served by the image renderer. -# TYPE grafana_image_renderer_http_request_in_flight gauge -grafana_image_renderer_http_request_in_flight 1 - -# HELP grafana_image_renderer_step_duration_seconds duration histogram of browser steps for rendering an image labeled with: step -# TYPE grafana_image_renderer_step_duration_seconds histogram -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="launch"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="launch"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="launch"} 0.7914972 -grafana_image_renderer_step_duration_seconds_count{step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="newPage"} 0.2217868 -grafana_image_renderer_step_duration_seconds_count{step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="prepare"} 0.0819274 -grafana_image_renderer_step_duration_seconds_count{step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="navigate"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="navigate"} 15.3311258 -grafana_image_renderer_step_duration_seconds_count{step="navigate"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="panelsRendered"} 0.0205577 -grafana_image_renderer_step_duration_seconds_count{step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="screenshot"} 0.2866623 -grafana_image_renderer_step_duration_seconds_count{step="screenshot"} 1 - -# HELP grafana_image_renderer_browser_info A metric with a constant '1 value labeled by version of the browser in use -# TYPE grafana_image_renderer_browser_info gauge -grafana_image_renderer_browser_info{version="HeadlessChrome/79.0.3945.0"} 1 -``` diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md deleted file mode 100644 index 61cd711bd50..00000000000 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -aliases: - - ../../image-rendering/troubleshooting/ -description: Image rendering troubleshooting -keywords: - - grafana - - image - - rendering - - plugin - - troubleshooting -labels: - products: - - enterprise - - oss -menuTitle: Troubleshooting -title: Troubleshoot image rendering -weight: 200 ---- - -# Troubleshoot image rendering - -In this section, you'll learn how to enable logging for the image renderer and you'll find the most common issues. - -## Enable debug logging - -To troubleshoot the image renderer, different kind of logs are available. - -You can enable debug log messages for rendering in the Grafana configuration file and inspect the Grafana server logs. - -```bash -[log] -filters = rendering:debug -``` - -You can also enable more logs in image renderer service itself by enabling [debug logging](#enable-debug-logging). - -## Missing libraries - -The plugin and rendering service uses [Chromium browser](https://www.chromium.org/) which depends on certain libraries. -If you don't have all of those libraries installed in your system you may encounter errors when trying to render an image, e.g. - -```bash -Rendering failed: Error: Failed to launch chrome!/var/lib/grafana/plugins/grafana-image-renderer/chrome-linux/chrome: -error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory\n\n\nTROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md -``` - -In general you can use the [`ldd`]() utility to figure out what shared libraries -are not installed in your system: - -```bash -cd -ldd chrome-headless-shell/linux-132.0.6781.0/chrome-headless-shell-linux64/chrome-headless-shell - linux-vdso.so.1 (0x00007fff1bf65000) - libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2047945000) - libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2047924000) - librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f204791a000) - libX11.so.6 => not found - libX11-xcb.so.1 => not found - libxcb.so.1 => not found - libXcomposite.so.1 => not found - ... -``` - -You can find a reference to all the relevant Debian packages for the service to function [in the Dockerfile](https://github.com/grafana/grafana-image-renderer/blob/master/Dockerfile). -If you are using an operating system that is not Debian 12, you should look up what each of those packages are called on your system. - -## Certificate signed by internal certificate authorities - -In many cases, Grafana runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptographic operations such as the validation of certificates. - -If you are using the Grafana Image Renderer with a Grafana server that uses a certificate signed by such a custom CA (for example a company-internal CA), rendering images will fail and you will see messages like this in the Grafana log: - -``` -t=2019-12-04T12:39:22+0000 lvl=error msg="Render request failed" logger=rendering error=map[] url="https://192.168.106.101:3443/d-solo/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m&from=1575438321300&to=1575459921300&var-Host=master1&panelId=4&width=1000&height=500&tz=Europe%2FBerlin&render=1" timestamp=0001-01-01T00:00:00.000Z -t=2019-12-04T12:39:22+0000 lvl=error msg="Rendering failed." logger=context userId=1 orgId=1 uname=admin error="Rendering failed: Error: net::ERR_CERT_AUTHORITY_INVALID at https://192.168.106.101:3443/d-solo/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m&from=1575438321300&to=1575459921300&var-Host=master1&panelId=4&width=1000&height=500&tz=Europe%2FBerlin&render=1" -t=2019-12-04T12:39:22+0000 lvl=error msg="Request Completed" logger=context userId=1 orgId=1 uname=admin method=GET path=/render/d-solo/zxDJxNaZk/graphite-metrics status=500 remote_addr=192.168.106.101 time_ms=310 size=1722 referer="https://grafana.xxx-xxx/d/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m" -``` - -If this happens, then you have to add the certificate to the trust store. If you have the certificate file for the internal root CA in the file `internal-root-ca.crt.pem`, then use these commands to create a user specific NSS trust store for the Grafana user (`grafana` for the purpose of this example) and execute the following steps: - -**Linux:** - -``` -[root@server ~]# [ -d /usr/share/grafana/.pki/nssdb ] || mkdir -p /usr/share/grafana/.pki/nssdb -[root@server ~]# certutil -d sql:/usr/share/grafana/.pki/nssdb -A -n internal-root-ca -t C -i /etc/pki/tls/certs/internal-root-ca.crt.pem -[root@server ~]# chown -R grafana: /usr/share/grafana/.pki/nssdb -``` - -You may also have to use other tooling than `certutil`, such as `update-ca-certificates` and its accompanying paths. -This depends on the Linux distribution you use; distributions often have a wiki with this type of information. - -**Windows:** - -``` -certutil –addstore "Root" /internal-root-ca.crt.pem -``` - -**Container:** - -```Dockerfile -FROM grafana/grafana-image-renderer:latest - -# Elevate our permissions to access system resources. -USER root - -RUN mkdir -p /usr/local/share/ca-certificates/ -# Convert from .pem to .crt -RUN openssl x509 -inform PEM -in rootCA.pem -out /usr/local/share/ca-certificates/rootCA.crt - -# Regenerate the CA certificates in the container. -RUN update-ca-certificates --fresh - -# Reassume the nonroot user for the service execution. -USER nonroot - -# Some CA certificates also need to explicitly be included in the user's network security services database. -# certutil is shipped in v4.0.8 and onwards of the image. -RUN mkdir -p /home/nonroot/.pki/nssdb -RUN certutil -d sql:/home/nonroot/.pki/nssdb -A -n internal-root-ca -t C -i /usr/local/share/ca-certificates/rootCA.crt -``` - -{{< admonition type="note" >}} -The container image was based on Alpine until v4.0.0. -After this point, it is based on distroless Debian. -{{< /admonition >}} - -## Custom Chrome/Chromium - -As a last resort, if you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) -installed on your system, then you can configure the Grafana Image renderer plugin to use this -instead of the pre-packaged version of Chromium. - -{{< admonition type="note" >}} -Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not -compatible with the [Grafana Image renderer plugin](/grafana/plugins/grafana-image-renderer). -{{< /admonition >}} - -To override the path to the Chrome/Chromium executable in plugin mode, set an environment variable and make sure that it's available for the Grafana process. For example: - -```bash -export GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/chromium-browser" -``` - -In remote rendering mode, you need to set the environment variable or update the configuration file and make sure that it's available for the image rendering service process: - -```bash -CHROME_BIN="/usr/bin/chromium-browser" -``` - -```json -{ - "rendering": { - "chromeBin": "/usr/bin/chromium-browser" - } -} -``` From 65fd15bbf988b83fabd545b5bb33c4ae1fb9f8fc Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Thu, 20 Nov 2025 15:47:44 +0100 Subject: [PATCH 008/423] feat: define unified disable migrations flag (#114120) chore: add US data migrations config --- pkg/setting/setting.go | 4 +- pkg/setting/setting_unified_storage.go | 57 +++++++++++++++++++- pkg/storage/unified/migrations/migrations.go | 8 ++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index fb69aa60259..a8b48c67f29 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -578,7 +578,9 @@ type Cfg struct { ShortLinkExpiration int // Unified Storage - UnifiedStorage map[string]UnifiedStorageConfig + UnifiedStorage map[string]UnifiedStorageConfig + // DisableDataMigrations will disable resources data migration to unified storage at startup + DisableDataMigrations bool MaxPageSizeBytes int IndexPath string IndexWorkers int diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 15bed66b6bb..29cb5d0270a 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -5,8 +5,15 @@ import ( "time" "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/util/osutil" ) +var migratedUnifiedResources = []string{ + //"playlists.playlist.grafana.app", + "folders.folder.grafana.app", + "dashboards.dashboard.grafana.app", +} + // read storage configs from ini file. They look like: // [unified_storage..] // = @@ -51,8 +58,14 @@ func (cfg *Cfg) setUnifiedStorageConfig() { // Set indexer config for unified storage section := cfg.Raw.Section("unified_storage") - - cfg.EnableSearch = section.Key("enable_search").MustBool(false) + // TODO: Re-enable once migrations are ready and disabled on cloud + //cfg.DisableDataMigrations = section.Key("disable_data_migrations").MustBool(false) + cfg.DisableDataMigrations = true + if !cfg.DisableDataMigrations && cfg.getUnifiedStorageType() == "unified" { + cfg.enforceMigrationToUnifiedConfigs() + } else { + cfg.EnableSearch = section.Key("enable_search").MustBool(false) + } cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0) cfg.IndexPath = section.Key("index_path").String() cfg.IndexWorkers = section.Key("index_workers").MustInt(10) @@ -84,3 +97,43 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.MaxFileIndexAge = section.Key("max_file_index_age").MustDuration(0) cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("") } + +// enforceMigrationToUnifiedConfigs enforces configurations required to run migrated resources in mode 5 +// All migrated resources in MigratedUnifiedResources are set to mode 5 and unified search is enabled +func (cfg *Cfg) enforceMigrationToUnifiedConfigs() { + section := cfg.Raw.Section("unified_storage") + cfg.EnableSearch = section.Key("enable_search").MustBool(true) + if !cfg.EnableSearch { + cfg.Logger.Info("Enforcing enable_search for unified storage") + section.Key("enable_search").SetValue("true") + cfg.EnableSearch = true + } + for _, resource := range migratedUnifiedResources { + cfg.Logger.Info("Enforcing mode 5 for resource in unified storage", "resource", resource) + if oldCfg, ok := cfg.UnifiedStorage[resource]; ok { + cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", oldCfg) + } + cfg.UnifiedStorage[resource] = UnifiedStorageConfig{ + DualWriterMode: 5, + DualWriterMigrationDataSyncDisabled: true, + } + } +} + +// getUnifiedStorageType returns the configured storage type without creating or mutating keys. +// Precedence: env > ini > default ("unified"). +// Used to decide unified storage behavior early without side effects. +func (cfg *Cfg) getUnifiedStorageType() string { + const ( + grafanaAPIServerSectionName = "grafana-apiserver" + storageTypeKeyName = "storage_type" + defaultStorageType = "unified" + ) + if envStorageType := (osutil.RealEnv{}).Getenv(EnvKey(grafanaAPIServerSectionName, storageTypeKeyName)); envStorageType != "" { + return envStorageType + } + if cfg.Raw.Section(grafanaAPIServerSectionName).HasKey(storageTypeKeyName) { + return cfg.Raw.Section(grafanaAPIServerSectionName).Key(storageTypeKeyName).Value() + } + return defaultStorageType +} diff --git a/pkg/storage/unified/migrations/migrations.go b/pkg/storage/unified/migrations/migrations.go index 195b7f5d154..a30777e308a 100644 --- a/pkg/storage/unified/migrations/migrations.go +++ b/pkg/storage/unified/migrations/migrations.go @@ -17,6 +17,7 @@ import ( ) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") +var logger = log.New("storage.unified.migrations") // UnifiedStorageMigrationProvider provides unified storage migrations as a background service type UnifiedStorageMigrationProvider interface { @@ -56,7 +57,11 @@ func (p *UnifiedStorageMigrationProviderImpl) Run(ctx context.Context) error { if os.Getenv("GRAFANA_TEST_DB") != "" { return nil } - + // skip migrations if disabled in config + if p.cfg.DisableDataMigrations { + logger.Info("Data migrations are disabled, skipping") + return nil + } // TODO: Re-enable once migrations are ready // return RegisterMigrations(p.legacyMigrator, p.cfg, p.client, p.sqlStore) return nil @@ -74,7 +79,6 @@ func RegisterMigrations( ) error { ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") defer span.End() - logger := log.New("storage.unified.migrations.folders-dashboards") mg := migrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unified_storage") mg.AddCreateMigration() From b5a50e7772aa0a49cb8f00f04d371792e2673f62 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 20 Nov 2025 15:51:50 +0100 Subject: [PATCH 009/423] `grafana-iam`: Use the UniStore as the default store (#113614) * `grafana-iam`: Use the UniStore as the default store * Refactor all instantiations * Remove enableDualWriter * Nit. dw is clear enough * Use the correct access control client --- pkg/registry/apis/iam/models.go | 15 ++- pkg/registry/apis/iam/register.go | 212 ++++++++++++++---------------- 2 files changed, 112 insertions(+), 115 deletions(-) diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index c960b7a8655..309b266ccb4 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -8,6 +8,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" + "github.com/grafana/grafana/pkg/registry/apis/iam/sso" + "github.com/grafana/grafana/pkg/registry/apis/iam/team" + "github.com/grafana/grafana/pkg/registry/apis/iam/teambinding" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -42,7 +46,13 @@ type ExternalGroupMappingStorageBackend interface{ resource.StorageBackend } // This is used just so wire has something unique to return type IdentityAccessManagementAPIBuilder struct { // Stores - store legacy.LegacyIdentityStore + store legacy.LegacyIdentityStore + + userLegacyStore *user.LegacyStore + saLegacyStore *serviceaccount.LegacyStore + legacyTeamStore *team.LegacyStore + teamBindingLegacyStore *teambinding.LegacyBindingStore + ssoLegacyStore *sso.LegacyStore coreRolesStorage CoreRoleStorageBackend rolesStorage RoleStorageBackend resourcePermissionsStorage resource.StorageBackend @@ -78,7 +88,4 @@ type IdentityAccessManagementAPIBuilder struct { // Toggle for enabling authz management apis features featuremgmt.FeatureToggles - - // Toggle for enabling dual writer - enableDualWriter bool } diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index cc229f2ed4b..2f214fd0630 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -31,7 +31,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" - "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -76,8 +75,16 @@ func RegisterAPIService( authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) registerMetrics(reg) + //nolint:staticcheck // not yet migrated to OpenFeature + enableAuthnMutation := features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + builder := &IdentityAccessManagementAPIBuilder{ store: store, + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), + ssoLegacyStore: sso.NewLegacyStore(ssoService), coreRolesStorage: coreRolesStorage, rolesStorage: rolesStorage, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), @@ -93,7 +100,6 @@ func RegisterAPIService( reg: reg, logger: log.New("iam.apis"), features: features, - enableDualWriter: true, dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features), @@ -113,18 +119,16 @@ func NewAPIService( store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) - noopStorage := noopstorage.ProvideStorageBackend() registerMetrics(reg) return &IdentityAccessManagementAPIBuilder{ - store: store, - display: user.NewLegacyDisplayREST(store), - resourcePermissionsStorage: resourcePermissionsStorage, - externalGroupMappingStorage: noopStorage, - logger: log.New("iam.apis"), - features: features, - zClient: zClient, - zTickets: make(chan bool, MaxConcurrentZanzanaWrites), - reg: reg, + store: store, + display: user.NewLegacyDisplayREST(store), + resourcePermissionsStorage: resourcePermissionsStorage, + logger: log.New("iam.apis"), + features: features, + zClient: zClient, + zTickets: make(chan bool, MaxConcurrentZanzanaWrites), + reg: reg, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { // For now only authorize resourcepermissions resource @@ -183,8 +187,7 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storage := map[string]rest.Storage{} - //nolint:staticcheck // not yet migrated to OpenFeature - enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + //nolint:staticcheck // not yet migrated to OpenFeature enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync) @@ -197,69 +200,63 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge }) teamResource := iamv0.TeamResourceInfo - teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, enableAuthnMutation) - storage[teamResource.StoragePath()] = teamLegacyStore - storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) + teamUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamResource, opts.OptsGetter) + if err != nil { + return err + } + storage[teamResource.StoragePath()] = teamUniStore - if b.enableDualWriter { - teamStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamResource, opts.OptsGetter) + if b.legacyTeamStore != nil { + dw, err := opts.DualWriteBuilder(teamResource.GroupResource(), b.legacyTeamStore, teamUniStore) if err != nil { return err } - teamDW, err := opts.DualWriteBuilder(teamResource.GroupResource(), teamLegacyStore, teamStore) - if err != nil { - return err - } - - storage[teamResource.StoragePath()] = teamDW + storage[teamResource.StoragePath()] = dw } + storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) + teamBindingResource := iamv0.TeamBindingResourceInfo - teamBindingLegacyStore := teambinding.NewLegacyBindingStore(b.store, enableAuthnMutation) - storage[teamBindingResource.StoragePath()] = teamBindingLegacyStore + teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) + if err != nil { + return err + } + storage[teamBindingResource.StoragePath()] = teamBindingUniStore - if b.enableDualWriter { - teamBindingStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) + // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks + if enableZanzanaSync { + b.logger.Info("Enabling hooks for TeamBinding to sync to Zanzana") + teamBindingUniStore.AfterCreate = b.AfterTeamBindingCreate + teamBindingUniStore.AfterDelete = b.AfterTeamBindingDelete + teamBindingUniStore.BeginUpdate = b.BeginTeamBindingUpdate + } + + if b.teamBindingLegacyStore != nil { + dw, err := opts.DualWriteBuilder(teamBindingResource.GroupResource(), b.teamBindingLegacyStore, teamBindingUniStore) if err != nil { return err } - - teamBindingDW, err := opts.DualWriteBuilder(teamBindingResource.GroupResource(), teamBindingLegacyStore, teamBindingStore) - if err != nil { - return err - } - - // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks - if enableZanzanaSync { - b.logger.Info("Enabling hooks for TeamBinding to sync to Zanzana") - teamBindingStore.AfterCreate = b.AfterTeamBindingCreate - teamBindingStore.AfterDelete = b.AfterTeamBindingDelete - teamBindingStore.BeginUpdate = b.BeginTeamBindingUpdate - } - - storage[teamBindingResource.StoragePath()] = teamBindingDW + storage[teamBindingResource.StoragePath()] = dw } // User store registration userResource := iamv0.UserResourceInfo - legacyStore := user.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) - storage[userResource.StoragePath()] = legacyStore + userUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) + if err != nil { + return err + } + storage[userResource.StoragePath()] = userUniStore - if b.enableDualWriter { - store, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) - if err != nil { - return err - } + if enableZanzanaSync { + b.logger.Info("Enabling hooks for User to sync basic role assignments to Zanzana") + userUniStore.AfterCreate = b.AfterUserCreate + userUniStore.BeginUpdate = b.BeginUserUpdate + userUniStore.AfterDelete = b.AfterUserDelete + } - if enableZanzanaSync { - b.logger.Info("Enabling hooks for User to sync basic role assignments to Zanzana") - store.AfterCreate = b.AfterUserCreate - store.BeginUpdate = b.BeginUserUpdate - store.AfterDelete = b.AfterUserDelete - } - - dw, err := opts.DualWriteBuilder(userResource.GroupResource(), legacyStore, store) + if b.userLegacyStore != nil { + dw, err := opts.DualWriteBuilder(userResource.GroupResource(), b.userLegacyStore, userUniStore) if err != nil { return err } @@ -270,50 +267,46 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) // Service Accounts store registration - serviceAccountResource := iamv0.ServiceAccountResourceInfo - saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) - storage[serviceAccountResource.StoragePath()] = saLegacyStore - - if b.enableDualWriter { - store, err := grafanaregistry.NewRegistryStore(opts.Scheme, serviceAccountResource, opts.OptsGetter) - if err != nil { - return err - } - - dw, err := opts.DualWriteBuilder(serviceAccountResource.GroupResource(), saLegacyStore, store) - if err != nil { - return err - } - - storage[serviceAccountResource.StoragePath()] = dw - } - - storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) - - if b.sso != nil { - ssoResource := legacyiamv0.SSOSettingResourceInfo - storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) - } - - externalGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo - externalGroupMappingLegacyStore, err := NewLocalStore(externalGroupMappingResource, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.externalGroupMappingStorage) + saResource := iamv0.ServiceAccountResourceInfo + saUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, saResource, opts.OptsGetter) if err != nil { return err } - storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingLegacyStore + storage[saResource.StoragePath()] = saUniStore - if b.enableDualWriter { - externalGroupMappingStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, externalGroupMappingResource, opts.OptsGetter) + if b.saLegacyStore != nil { + dw, err := opts.DualWriteBuilder(saResource.GroupResource(), b.saLegacyStore, saUniStore) + if err != nil { + return err + } + storage[saResource.StoragePath()] = dw + } + + storage[saResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) + + if b.ssoLegacyStore != nil { + ssoResource := legacyiamv0.SSOSettingResourceInfo + storage[ssoResource.StoragePath()] = b.ssoLegacyStore + } + + extGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo + extGroupMappingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, extGroupMappingResource, opts.OptsGetter) + if err != nil { + return err + } + storage[extGroupMappingResource.StoragePath()] = extGroupMappingUniStore + + if b.externalGroupMappingStorage != nil { + extGroupMappingLegacyStore, err := NewLocalStore(extGroupMappingResource, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.externalGroupMappingStorage) if err != nil { return err } - externalGroupMappingDW, err := opts.DualWriteBuilder(externalGroupMappingResource.GroupResource(), externalGroupMappingLegacyStore, externalGroupMappingStore) + dw, err := opts.DualWriteBuilder(extGroupMappingResource.GroupResource(), extGroupMappingLegacyStore, extGroupMappingUniStore) if err != nil { return err } - - storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingDW + storage[extGroupMappingResource.StoragePath()] = dw } //nolint:staticcheck // not yet migrated to OpenFeature @@ -351,7 +344,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } //nolint:staticcheck // not yet migrated to OpenFeature if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { - if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, b.enableDualWriter, enableZanzanaSync); err != nil { + if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil { return err } } @@ -364,11 +357,19 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions, storage map[string]rest.Storage, - enableDualWriter bool, enableZanzanaSync bool, ) error { - var store rest.Storage - // Create the legacy store first + uniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, iamv0.ResourcePermissionInfo, opts.OptsGetter) + if err != nil { + return err + } + storage[iamv0.ResourcePermissionInfo.StoragePath()] = uniStore + + if b.resourcePermissionsStorage == nil { + // No legacy storage configured, nothing more to do + return nil + } + legacyStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) if err != nil { return err @@ -384,23 +385,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( legacyStore.AfterDelete = b.AfterResourcePermissionDelete } - // Set the default store to the legacy store - store = legacyStore - - if enableDualWriter { - // Create the dual write store (UniStore + LegacyStore) - uniStore, err := grafanaregistry.NewRegistryStore(apiGroupInfo.Scheme, iamv0.ResourcePermissionInfo, opts.OptsGetter) - if err != nil { - return err - } - - store, err = opts.DualWriteBuilder(iamv0.ResourcePermissionInfo.GroupResource(), legacyStore, uniStore) - if err != nil { - return err - } + dw, err := opts.DualWriteBuilder(iamv0.ResourcePermissionInfo.GroupResource(), legacyStore, uniStore) + if err != nil { + return err } - storage[iamv0.ResourcePermissionInfo.StoragePath()] = store + storage[iamv0.ResourcePermissionInfo.StoragePath()] = dw return nil } From cb06bba24354811d5e66dd4eba8f9096591f5245 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 20 Nov 2025 15:54:32 +0100 Subject: [PATCH 010/423] Zanzana: Add token namespace to config (#114165) --- pkg/services/authz/zanzana.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 3be1b18dc1d..da77010d0eb 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -118,6 +118,7 @@ type ZanzanaClientConfig struct { Token string TokenExchangeURL string ServerCertFile string + TokenNamespace string } // NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server. From 30c04ab3fc7dbaafe7d031727daa5f3d697ff513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:40:20 +0100 Subject: [PATCH 011/423] feat: inject unified data migrations in dual writer (#114138) * feat: draft changes for on-prem unified migration * feat: further draft changes for on-prem unified migration * fix: remove some tbis * refactor: rename * fix: another approach * fix: background service related issues * fix: address comments * fix: make gen-go * fix: background service related issues * feat: refactor dual writer and legacy migrator * fix: minor issues * feat: working version in oss * fix: wire * fix: revert test data override * fix: enterprise related issues * chore: add todo * fix: revert dual writer method * fix: lint * chore: logger format * fix: reduce log level * fix: log change * fix: disable * fix: address comments * fix: return error on dual writer service * fix: merge conflict --------- Co-authored-by: Rafael Paulovic --- .../commands/datamigrations/stubs.go | 70 --- .../datamigrations/to_unified_storage.go | 41 +- .../dashboard/legacy/legacy_migrator_mock.go | 96 ---- pkg/registry/apis/dashboard/legacy/migrate.go | 460 ------------------ .../apis/dashboard/legacy/migrate_test.go | 154 ------ .../migration_dashboard_accessor_mock.go | 279 +++++++++++ .../apis/dashboard/legacy/sql_dashboards.go | 336 ++++++++++++- .../dashboard/legacy/sql_dashboards_test.go | 146 ++++++ pkg/registry/apis/dashboard/legacy/types.go | 12 +- pkg/registry/apis/dashboard/legacy_storage.go | 2 +- pkg/registry/apis/dashboard/libary_panel.go | 2 +- pkg/registry/apis/dashboard/register.go | 2 +- pkg/registry/apis/dashboard/sub_dto.go | 4 +- .../jobs/migrate/legacy_resources.go | 64 +-- .../jobs/migrate/legacy_resources_test.go | 112 +++-- pkg/registry/apis/provisioning/register.go | 19 +- .../apis/provisioning/resources/object.go | 19 +- .../backgroundsvcs/background_services.go | 3 - pkg/server/wire.go | 8 +- pkg/server/wire_gen.go | 47 +- pkg/server/wireexts_oss.go | 2 + .../apiserver/appinstaller/storage.go | 2 +- pkg/services/apiserver/builder/helper.go | 2 +- pkg/services/apiserver/service.go | 17 +- pkg/services/provisioning/stubs.go | 69 +++ .../dualwrite/dualwriter_mode1_test.go | 12 +- .../dualwrite/dualwriter_mode2_test.go | 12 +- .../dualwrite/dualwriter_mode3_test.go | 12 +- pkg/storage/legacysql/dualwrite/runtime.go | 31 -- .../legacysql/dualwrite/runtime_test.go | 6 +- pkg/storage/legacysql/dualwrite/service.go | 58 +++ .../legacysql/dualwrite/service_test.go | 4 +- pkg/storage/legacysql/dualwrite/static.go | 4 +- .../unified/migrations/contract/migrations.go | 12 + .../migrations/dashboard_folder_migration.go | 93 ---- pkg/storage/unified/migrations/migrations.go | 112 ----- pkg/storage/unified/migrations/migrator.go | 312 ++++++------ .../unified/migrations/migrator_mock.go | 98 ++++ .../unified/migrations/resource_migration.go | 275 +++++++++++ pkg/storage/unified/migrations/service.go | 118 +++++ 40 files changed, 1779 insertions(+), 1348 deletions(-) delete mode 100644 pkg/cmd/grafana-cli/commands/datamigrations/stubs.go delete mode 100644 pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go delete mode 100644 pkg/registry/apis/dashboard/legacy/migrate.go delete mode 100644 pkg/registry/apis/dashboard/legacy/migrate_test.go create mode 100644 pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go create mode 100644 pkg/services/provisioning/stubs.go create mode 100644 pkg/storage/unified/migrations/contract/migrations.go delete mode 100644 pkg/storage/unified/migrations/dashboard_folder_migration.go delete mode 100644 pkg/storage/unified/migrations/migrations.go create mode 100644 pkg/storage/unified/migrations/migrator_mock.go create mode 100644 pkg/storage/unified/migrations/resource_migration.go create mode 100644 pkg/storage/unified/migrations/service.go diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go b/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go deleted file mode 100644 index 22dc15d6fc7..00000000000 --- a/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go +++ /dev/null @@ -1,70 +0,0 @@ -package datamigrations - -import ( - "context" - "path/filepath" - - "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/provisioning/dashboards" -) - -var ( - _ provisioning.ProvisioningService = (*stubProvisioning)(nil) -) - -func newStubProvisioning(path string) (provisioning.ProvisioningService, error) { - cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) - if err != nil { - return nil, err - } - stub := &stubProvisioning{ - path: make(map[string]string), - } - for _, cfg := range cfgs { - stub.path[cfg.Name] = cfg.Options["path"].(string) - } - return &stubProvisioning{}, nil -} - -type stubProvisioning struct { - path map[string]string // name > options.path -} - -// GetAllowUIUpdatesFromConfig implements provisioning.ProvisioningService. -func (s *stubProvisioning) GetAllowUIUpdatesFromConfig(name string) bool { - return false -} - -func (s *stubProvisioning) GetDashboardProvisionerResolvedPath(name string) string { - return s.path[name] -} - -// ProvisionAlerting implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionAlerting(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionDashboards implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionDashboards(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionDatasources implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionDatasources(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionPlugins implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionPlugins(ctx context.Context) error { - panic("unimplemented") -} - -// Run implements provisioning.ProvisioningService. -func (s *stubProvisioning) Run(ctx context.Context) error { - panic("unimplemented") -} - -// RunInitProvisioners implements provisioning.ProvisioningService. -func (s *stubProvisioning) RunInitProvisioners(ctx context.Context) error { - panic("unimplemented") -} diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index f6bdc211f15..2e8a9892ab1 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -24,10 +24,11 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/parquet" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -52,7 +53,6 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err {Group: folders.GROUP, Resource: folders.RESOURCE}, {Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE}, }, - LargeObjects: nil, // TODO... from config Progress: func(count int, msg string) { const minInterval = time.Second shouldPrint := count < 1 || time.Since(last) > minInterval @@ -69,31 +69,26 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err } featureToggles := featuremgmt.ProvideToggles(featureManager) - provisioning, err := newStubProvisioning(cfg.ProvisioningPath) + provisioning, err := provisioning.ProvideStubProvisioningService(cfg) if err != nil { return err } - migrator := legacy.NewDashboardAccess( + grpcClient, err := newUnifiedClient(cfg, sqlStore, featureToggles) + if err != nil { + return err + } + + dashboardAccess := legacy.ProvideMigratorDashboardAccessor( legacysql.NewDatabaseProvider(sqlStore), - authlib.OrgNamespaceFormatter, - nil, // no dashboards.Store provisioning, - nil, // no librarypanels.Service - sort.ProvideService(), - nil, // we don't delete during migration, and this is only need to delete permission. acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featureToggles, ) - client, err := newUnifiedClient(cfg, sqlStore, featureToggles) - if err != nil { - return err - } - if c.Bool("non-interactive") { - opts.Store = client - opts.BlobStore = client + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, grpcClient) + opts.WithHistory = true // always include history in non-interactive mode rsp, err := migrator.Migrate(ctx, opts) if exitErr := handleMigrationError(err, rsp); exitErr != nil { @@ -113,6 +108,8 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err return err } if yes { + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, nil) // no need for grpc client for counting + opts.OnlyCount = true rsp, err := migrator.Migrate(ctx, opts) if err != nil { @@ -141,12 +138,13 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err if err != nil { return err } - start = time.Now() - last = time.Now() - opts.Store, err = newParquetClient(file) + parquetClient, err := newParquetClient(file) if err != nil { return err } + migrator := migrations.ProvideUnifiedMigratorParquet(dashboardAccess, parquetClient) + start = time.Now() + last = time.Now() rsp, err := migrator.Migrate(ctx, opts) if err != nil { return err @@ -172,7 +170,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err req.Kinds = append(req.Kinds, fmt.Sprintf("%s/%s", r.Group, r.Resource)) } - stats, err := client.GetStats(ctx, req) + stats, err := grpcClient.GetStats(ctx, req) if err != nil { return err } @@ -188,10 +186,9 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err return err } if yes { + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, grpcClient) start = time.Now() last = time.Now() - opts.Store = client - opts.BlobStore = client rsp, err := migrator.Migrate(ctx, opts) if err != nil { return err diff --git a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go deleted file mode 100644 index bb7231e86f6..00000000000 --- a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go +++ /dev/null @@ -1,96 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package legacy - -import ( - context "context" - - resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - mock "github.com/stretchr/testify/mock" -) - -// MockLegacyMigrator is an autogenerated mock type for the LegacyMigrator type -type MockLegacyMigrator struct { - mock.Mock -} - -type MockLegacyMigrator_Expecter struct { - mock *mock.Mock -} - -func (_m *MockLegacyMigrator) EXPECT() *MockLegacyMigrator_Expecter { - return &MockLegacyMigrator_Expecter{mock: &_m.Mock} -} - -// Migrate provides a mock function with given fields: ctx, opts -func (_m *MockLegacyMigrator) Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Migrate") - } - - var r0 *resourcepb.BulkResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) *resourcepb.BulkResponse); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*resourcepb.BulkResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, MigrateOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockLegacyMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate' -type MockLegacyMigrator_Migrate_Call struct { - *mock.Call -} - -// Migrate is a helper method to define mock.On call -// - ctx context.Context -// - opts MigrateOptions -func (_e *MockLegacyMigrator_Expecter) Migrate(ctx interface{}, opts interface{}) *MockLegacyMigrator_Migrate_Call { - return &MockLegacyMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, opts)} -} - -func (_c *MockLegacyMigrator_Migrate_Call) Run(run func(ctx context.Context, opts MigrateOptions)) *MockLegacyMigrator_Migrate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(MigrateOptions)) - }) - return _c -} - -func (_c *MockLegacyMigrator_Migrate_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockLegacyMigrator_Migrate_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockLegacyMigrator_Migrate_Call) RunAndReturn(run func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)) *MockLegacyMigrator_Migrate_Call { - _c.Call.Return(run) - return _c -} - -// NewMockLegacyMigrator creates a new instance of MockLegacyMigrator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockLegacyMigrator(t interface { - mock.TestingT - Cleanup(func()) -}) *MockLegacyMigrator { - mock := &MockLegacyMigrator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go deleted file mode 100644 index e43a0fb7629..00000000000 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ /dev/null @@ -1,460 +0,0 @@ -package legacy - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - - "google.golang.org/grpc/metadata" - "k8s.io/apimachinery/pkg/runtime/schema" - - authlib "github.com/grafana/authlib/types" - - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/librarypanels" - "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/search/sort" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/legacysql" - "github.com/grafana/grafana/pkg/storage/unified/apistore" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -type MigrateOptions struct { - Namespace string - Store resourcepb.BulkStoreClient - LargeObjects apistore.LargeObjectSupport - BlobStore resourcepb.BlobStoreClient - Resources []schema.GroupResource - WithHistory bool // only applies to dashboards - OnlyCount bool // just count the values - Progress func(count int, msg string) -} - -// Read from legacy and write into unified storage -// -//go:generate mockery --name LegacyMigrator --structname MockLegacyMigrator --inpackage --filename legacy_migrator_mock.go --with-expecter -type LegacyMigrator interface { - Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) -} - -// This can migrate Folders, Dashboards and LibraryPanels -func ProvideLegacyMigrator( - sql db.DB, // direct access to tables - provisioning provisioning.ProvisioningService, // only needed for dashboard settings - libraryPanelSvc librarypanels.Service, - dashboardPermissionSvc accesscontrol.DashboardPermissionsService, - accessControl accesscontrol.AccessControl, - features featuremgmt.FeatureToggles, -) LegacyMigrator { - dbp := legacysql.NewDatabaseProvider(sql) - return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), dashboardPermissionSvc, accessControl, features) -} - -type BlobStoreInfo struct { - Count int64 - Size int64 -} - -// migrate function -- works for a single kind -type migratorFunc = func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) - -func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - info, err := authlib.ParseNamespace(opts.Namespace) - if err != nil { - return nil, err - } - if opts.Progress == nil { - opts.Progress = func(count int, msg string) {} // noop - } - - // Migrate everything - if len(opts.Resources) < 1 { - return nil, fmt.Errorf("missing resource selector") - } - - migratorFuncs := []migratorFunc{} - settings := resource.BulkSettings{ - RebuildCollection: true, - SkipValidation: true, - } - - for _, res := range opts.Resources { - switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { - case "folder.grafana.app/folders": - migratorFuncs = append(migratorFuncs, a.migrateFolders) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: folders.GROUP, - Resource: folders.RESOURCE, - }) - - case "dashboard.grafana.app/librarypanels": - migratorFuncs = append(migratorFuncs, a.migratePanels) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.LIBRARY_PANEL_RESOURCE, - }) - - case "dashboard.grafana.app/dashboards": - migratorFuncs = append(migratorFuncs, a.migrateDashboards) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.DASHBOARD_RESOURCE, - }) - default: - return nil, fmt.Errorf("unsupported resource: %s", res) - } - } - if opts.OnlyCount { - return a.countValues(ctx, opts) - } - - ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) - if md, ok := metadata.FromOutgoingContext(ctx); ok { - a.log.Debug("bulk grpc request metadata", - "metadata", md, - "collection", settings.Collection, - ) - } else { - a.log.Debug("bulk grpc request, no metadata found", - "collection", settings.Collection, - ) - } - - stream, err := opts.Store.BulkProcess(ctx) - if err != nil { - return nil, err - } - - // Now run each migration - blobStore := BlobStoreInfo{} - a.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) - for _, m := range migratorFuncs { - blobs, err := m(ctx, info.OrgID, opts, stream) - if err != nil { - a.log.Error("error migrating legacy resources", "error", err, "namespace", opts.Namespace) - return nil, err - } - if blobs != nil { - blobStore.Count += blobs.Count - blobStore.Size += blobs.Size - } - } - a.log.Info("finished migrating legacy resources", "blobStore", blobStore) - return stream.CloseAndRecv() -} - -func (a *dashboardSqlAccess) countValues(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - sql, err := a.sql(ctx) - if err != nil { - return nil, err - } - ns, err := authlib.ParseNamespace(opts.Namespace) - if err != nil { - return nil, err - } - orgId := ns.OrgID - rsp := &resourcepb.BulkResponse{} - err = sql.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - for _, res := range opts.Resources { - switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { - case "folder.grafana.app/folders": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = folders.GROUP - summary.Group = folders.RESOURCE - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ - " WHERE is_folder=TRUE AND org_id=?", orgId).Get(&summary.Count) - rsp.Summary = append(rsp.Summary, summary) - - case "dashboard.grafana.app/librarypanels": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = dashboard.GROUP - summary.Resource = dashboard.LIBRARY_PANEL_RESOURCE - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("library_element")+ - " WHERE org_id=?", orgId).Get(&summary.Count) - rsp.Summary = append(rsp.Summary, summary) - - case "dashboard.grafana.app/dashboards": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = dashboard.GROUP - summary.Resource = dashboard.DASHBOARD_RESOURCE - rsp.Summary = append(rsp.Summary, summary) - - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ - " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) - if err != nil { - return err - } - - // Also count history - _, err = sess.SQL(`SELECT COUNT(*) - FROM `+sql.Table("dashboard_version")+` as dv - JOIN `+sql.Table("dashboard")+` as dd - ON dd.id = dv.dashboard_id - WHERE org_id=?`, orgId).Get(&summary.History) - } - if err != nil { - return err - } - } - return nil - }) - return rsp, nil -} - -func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - query := &DashboardQuery{ - OrgID: orgId, - Limit: 100000000, - GetHistory: opts.WithHistory, // include history - AllowFallback: true, // allow fallback to dashboard table during migration - Order: "ASC", // oldest first - } - - blobs := &BlobStoreInfo{} - sql, err := a.sql(ctx) - if err != nil { - return blobs, err - } - - opts.Progress(-1, "migrating dashboards...") - rows, err := a.getRows(ctx, sql, query) - if rows != nil { - defer func() { - _ = rows.Close() - }() - } - if err != nil { - return blobs, err - } - - large := opts.LargeObjects - - // Now send each dashboard - for i := 1; rows.Next(); i++ { - dash := rows.row.Dash - if dash.APIVersion == "" { - dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboard.GROUP) - } - dash.SetNamespace(opts.Namespace) - dash.SetResourceVersion("") // it will be filled in by the backend - - body, err := json.Marshal(dash) - if err != nil { - err = fmt.Errorf("error reading json from: %s // %w", rows.row.Dash.Name, err) - return blobs, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.DASHBOARD_RESOURCE, - Name: rows.Name(), - }, - Value: body, - Folder: rows.row.FolderUID, - Action: resourcepb.BulkRequest_ADDED, - } - if dash.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } else if dash.Generation < 0 { - req.Action = resourcepb.BulkRequest_DELETED - } - - // With large object support - if large != nil && len(body) > large.Threshold() { - obj, err := utils.MetaAccessor(dash) - if err != nil { - return blobs, err - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s Large object (%d)", dash.Generation, dash.Name, len(body))) - err = large.Deconstruct(ctx, req.Key, opts.BlobStore, obj, req.Value) - if err != nil { - return blobs, err - } - - // The smaller version (most of spec removed) - req.Value, err = json.Marshal(dash) - if err != nil { - return blobs, err - } - blobs.Count++ - blobs.Size += int64(len(body)) - } - - opts.Progress(i, fmt.Sprintf("[v:%2d] %s (size:%d / %d|%d)", dash.Generation, dash.Name, len(req.Value), i, rows.count)) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - opts.Progress(i, fmt.Sprintf("stream EOF/cancelled. index=%d", i)) - err = nil - } - return blobs, err - } - } - - if len(rows.rejected) > 0 { - for _, row := range rows.rejected { - id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] - a.log.Warn("rejected dashboard", - "namespace", opts.Namespace, - "dashboard", row.Dash.Name, - "uid", row.Dash.UID, - "id", id, - "version", row.Dash.Generation, - ) - opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) - } - } - - if rows.Error() != nil { - return blobs, rows.Error() - } - - opts.Progress(-2, fmt.Sprintf("finished dashboards... (%d)", rows.count)) - return blobs, err -} - -func (a *dashboardSqlAccess) migrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - query := &DashboardQuery{ - OrgID: orgId, - Limit: 100000000, - GetFolders: true, - Order: "ASC", - } - - sql, err := a.sql(ctx) - if err != nil { - return nil, err - } - - opts.Progress(-1, "migrating folders...") - rows, err := a.getRows(ctx, sql, query) - if rows != nil { - defer func() { - _ = rows.Close() - }() - } - if err != nil { - return nil, err - } - - // Now send each dashboard - for i := 1; rows.Next(); i++ { - dash := rows.row.Dash - dash.APIVersion = "folder.grafana.app/v1beta1" - dash.Kind = "Folder" - dash.SetNamespace(opts.Namespace) - dash.SetResourceVersion("") // it will be filled in by the backend - - spec := map[string]any{ - "title": dash.Spec.Object["title"], - } - description := dash.Spec.Object["description"] - if description != nil { - spec["description"] = description - } - dash.Spec.Object = spec - - body, err := json.Marshal(dash) - if err != nil { - return nil, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: "folder.grafana.app", - Resource: "folders", - Name: rows.Name(), - }, - Value: body, - Folder: rows.row.FolderUID, - Action: resourcepb.BulkRequest_ADDED, - } - if dash.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } else if dash.Generation < 0 { - req.Action = resourcepb.BulkRequest_DELETED - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", dash.Generation, dash.Name, len(req.Value))) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - err = nil - } - return nil, err - } - } - - if rows.Error() != nil { - return nil, rows.Error() - } - - opts.Progress(-2, fmt.Sprintf("finished folders... (%d)", rows.count)) - return nil, err -} - -func (a *dashboardSqlAccess) migratePanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - opts.Progress(-1, "migrating library panels...") - panels, err := a.GetLibraryPanels(ctx, LibraryPanelQuery{ - OrgID: orgId, - Limit: 1000000, - }) - if err != nil { - return nil, err - } - for i, panel := range panels.Items { - meta, err := utils.MetaAccessor(&panel) - if err != nil { - return nil, err - } - body, err := json.Marshal(panel) - if err != nil { - return nil, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.LIBRARY_PANEL_RESOURCE, - Name: panel.Name, - }, - Value: body, - Folder: meta.GetFolder(), - Action: resourcepb.BulkRequest_ADDED, - } - if panel.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", i, meta.GetName(), len(req.Value))) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - err = nil - } - return nil, err - } - } - opts.Progress(-2, fmt.Sprintf("finished panels... (%d)", len(panels.Items))) - return nil, nil -} diff --git a/pkg/registry/apis/dashboard/legacy/migrate_test.go b/pkg/registry/apis/dashboard/legacy/migrate_test.go deleted file mode 100644 index 5b134cb31dc..00000000000 --- a/pkg/registry/apis/dashboard/legacy/migrate_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/storage/legacysql" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" -) - -func TestDashboardMigrationQuery(t *testing.T) { - // Test that migration queries use AllowFallback flag correctly - nodb := &legacysql.LegacyDatabaseHelper{ - Table: func(n string) string { - return "grafana." + n - }, - } - - t.Run("Migration query should enable AllowFallback flag", func(t *testing.T) { - // Create a migration query as would be used in actual migration - migrationQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, // Migration includes history - AllowFallback: true, // This is the key flag for migration - Order: "ASC", // Migration uses ascending order - } - - // Verify UseHistoryTable returns true (requirement for COALESCE logic) - require.True(t, migrationQuery.UseHistoryTable(), "Migration query should use history table") - - // Verify the flag is set correctly - require.True(t, migrationQuery.AllowFallback, "Migration query should allow fallback") - require.True(t, migrationQuery.GetHistory, "Migration query should get history") - require.Equal(t, "ASC", migrationQuery.Order, "Migration should use ascending order") - }) - - t.Run("Regular history query should not use AllowFallback", func(t *testing.T) { - // Regular history query without migration - historyQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - Order: "DESC", - } - - require.True(t, historyQuery.UseHistoryTable(), "History query should use history table") - require.False(t, historyQuery.AllowFallback, "Regular history query should not allow fallback") - require.True(t, historyQuery.GetHistory, "History query should get history") - }) - - t.Run("Migration query template produces COALESCE SQL", func(t *testing.T) { - // Test that the SQL template produces COALESCE logic for migration queries - migrationQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - AllowFallback: true, - Order: "ASC", - } - - req := newQueryReq(nodb, migrationQuery) - req.SQLTemplate = mocks.NewTestingSQLTemplate() - - // Execute the template to get the generated SQL - rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) - require.NoError(t, err) - - sql := rawQuery - - // Verify that COALESCE functions are present in the generated SQL - // These should be used when GetHistory=true AND AllowFallback=true - require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", - "Migration SQL should contain COALESCE for updated timestamp") - require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version)", - "Migration SQL should contain COALESCE for version") - require.Contains(t, sql, "COALESCE(dashboard_version.data, dashboard.data)", - "Migration SQL should contain COALESCE for data") - require.Contains(t, sql, "COALESCE(dashboard_version.api_version, dashboard.api_version)", - "Migration SQL should contain COALESCE for api_version") - require.Contains(t, sql, "COALESCE(dashboard_version.message, '')", - "Migration SQL should contain COALESCE for message with empty string fallback") - - // Verify ORDER BY uses COALESCE as well - require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated) ASC", - "Migration SQL should ORDER BY COALESCED created timestamp") - require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version) ASC", - "Migration SQL should ORDER BY COALESCED version") - - // Verify it doesn't have the strict history table filter that would exclude NULL version entries - require.NotContains(t, sql, "dashboard_version.id IS NOT NULL", - "Migration SQL should not exclude dashboards without version entries") - }) - - t.Run("Regular history query produces strict SQL", func(t *testing.T) { - // Test that regular history queries still use strict dashboard_version fields - historyQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - Order: "DESC", - } - - req := newQueryReq(nodb, historyQuery) - req.SQLTemplate = mocks.NewTestingSQLTemplate() - - rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) - require.NoError(t, err) - - sql := rawQuery - - // Verify that direct dashboard_version fields are used (no COALESCE) - require.Contains(t, sql, "dashboard_version.created as updated", - "Regular history SQL should use direct dashboard_version.created") - require.Contains(t, sql, "dashboard_version.version", - "Regular history SQL should use direct dashboard_version.version") - require.Contains(t, sql, "dashboard_version.data", - "Regular history SQL should use direct dashboard_version.data") - - // NOTE: We intentionally do NOT add dashboard_version.id IS NOT NULL filter - // to allow for cases where dashboard_version entries might be missing - - // Should not contain COALESCE functions - require.NotContains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", - "Regular history SQL should not contain COALESCE for updated") - }) -} - -func TestMigrateDashboardsConfiguration(t *testing.T) { - // Test the actual migration function configuration - - t.Run("Migration options should configure query correctly", func(t *testing.T) { - // Test the migration configuration as used in real migration - opts := MigrateOptions{ - WithHistory: true, // Migration includes history - } - - // This simulates what happens in migrateDashboards function - expectedQuery := &DashboardQuery{ - OrgID: 1, - Limit: 100000000, - GetHistory: opts.WithHistory, // Should be true - AllowFallback: true, // Should be true for migration - Order: "ASC", // Should be ASC for migration - } - - // Verify the configuration matches what migration sets up - require.True(t, expectedQuery.GetHistory, "Migration should enable GetHistory") - require.True(t, expectedQuery.AllowFallback, "Migration should enable AllowFallback") - require.Equal(t, "ASC", expectedQuery.Order, "Migration should use ascending order") - require.Equal(t, 100000000, expectedQuery.Limit, "Migration should use large limit") - - // Verify UseHistoryTable logic - require.True(t, expectedQuery.UseHistoryTable(), "Migration query should use history table") - }) -} diff --git a/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go b/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go new file mode 100644 index 00000000000..43f2ce05ad1 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go @@ -0,0 +1,279 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package legacy + +import ( + context "context" + + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + mock "github.com/stretchr/testify/mock" +) + +// MockMigrationDashboardAccessor is an autogenerated mock type for the MigrationDashboardAccessor type +type MockMigrationDashboardAccessor struct { + mock.Mock +} + +type MockMigrationDashboardAccessor_Expecter struct { + mock *mock.Mock +} + +func (_m *MockMigrationDashboardAccessor) EXPECT() *MockMigrationDashboardAccessor_Expecter { + return &MockMigrationDashboardAccessor_Expecter{mock: &_m.Mock} +} + +// CountResources provides a mock function with given fields: ctx, opts +func (_m *MockMigrationDashboardAccessor) CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for CountResources") + } + + var r0 *resourcepb.BulkResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) *resourcepb.BulkResponse); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.BulkResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, MigrateOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_CountResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountResources' +type MockMigrationDashboardAccessor_CountResources_Call struct { + *mock.Call +} + +// CountResources is a helper method to define mock.On call +// - ctx context.Context +// - opts MigrateOptions +func (_e *MockMigrationDashboardAccessor_Expecter) CountResources(ctx interface{}, opts interface{}) *MockMigrationDashboardAccessor_CountResources_Call { + return &MockMigrationDashboardAccessor_CountResources_Call{Call: _e.mock.On("CountResources", ctx, opts)} +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) Run(run func(ctx context.Context, opts MigrateOptions)) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(MigrateOptions)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) RunAndReturn(run func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Return(run) + return _c +} + +// MigrateDashboards provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateDashboards") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateDashboards_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateDashboards' +type MockMigrationDashboardAccessor_MigrateDashboards_Call struct { + *mock.Call +} + +// MigrateDashboards is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateDashboards(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + return &MockMigrationDashboardAccessor_MigrateDashboards_Call{Call: _e.mock.On("MigrateDashboards", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Return(run) + return _c +} + +// MigrateFolders provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateFolders") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateFolders_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateFolders' +type MockMigrationDashboardAccessor_MigrateFolders_Call struct { + *mock.Call +} + +// MigrateFolders is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateFolders(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateFolders_Call { + return &MockMigrationDashboardAccessor_MigrateFolders_Call{Call: _e.mock.On("MigrateFolders", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Return(run) + return _c +} + +// MigrateLibraryPanels provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateLibraryPanels") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateLibraryPanels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateLibraryPanels' +type MockMigrationDashboardAccessor_MigrateLibraryPanels_Call struct { + *mock.Call +} + +// MigrateLibraryPanels is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateLibraryPanels(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + return &MockMigrationDashboardAccessor_MigrateLibraryPanels_Call{Call: _e.mock.On("MigrateLibraryPanels", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Return(run) + return _c +} + +// NewMockMigrationDashboardAccessor creates a new instance of MockMigrationDashboardAccessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockMigrationDashboardAccessor(t interface { + mock.TestingT + Cleanup(func()) +}) *MockMigrationDashboardAccessor { + mock := &MockMigrationDashboardAccessor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 3d157e09489..7c17ec8b6aa 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -4,7 +4,9 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" + "io" "strconv" "strings" "sync" @@ -13,6 +15,7 @@ import ( "go.opentelemetry.io/otel" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" claims "github.com/grafana/authlib/types" @@ -20,6 +23,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -35,16 +39,30 @@ import ( "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var ( - _ DashboardAccess = (*dashboardSqlAccess)(nil) - tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy") + tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy") ) +type MigrateOptions struct { + Namespace string + Resources []schema.GroupResource + WithHistory bool // only applies to dashboards + OnlyCount bool // just count the values + Progress func(count int, msg string) +} + +type BlobStoreInfo struct { + Count int64 + Size int64 +} + type dashboardRow struct { // The numeric version for this dashboard RV int64 @@ -63,8 +81,9 @@ type dashboardRow struct { type dashboardSqlAccess struct { sql legacysql.LegacyDatabaseProvider namespacer request.NamespaceMapper - provisioning provisioning.ProvisioningService + provisioning provisioning.StubProvisioningService + // TODO: consider enabling this by default for on-prem migrations invalidDashboardParseFallbackEnabled bool // Use for writing (not reading) @@ -73,7 +92,7 @@ type dashboardSqlAccess struct { dashboardPermissionSvc accesscontrol.DashboardPermissionsService accessControl accesscontrol.AccessControl - libraryPanelSvc librarypanels.Service + libraryPanelSvc librarypanels.Service // only used for save dashboard // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent @@ -81,7 +100,27 @@ type dashboardSqlAccess struct { log log.Logger } -func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, +// ProvideMigratorDashboardAccessor creates a DashboardAccess specifically for migration purposes. +// This provider is used by Wire DI and only includes the minimal dependencies needed for migrations. +func ProvideMigratorDashboardAccessor( + sql legacysql.LegacyDatabaseProvider, + provisioning provisioning.StubProvisioningService, + accessControl accesscontrol.AccessControl, + features featuremgmt.FeatureToggles, +) MigrationDashboardAccessor { + return &dashboardSqlAccess{ + sql: sql, + namespacer: claims.OrgNamespaceFormatter, + dashStore: nil, // not needed for migration + provisioning: provisioning, + dashboardPermissionSvc: nil, // not needed for migration + libraryPanelSvc: nil, // not needed for migration + accessControl: accessControl, + invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), + } +} + +func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService, @@ -90,7 +129,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles, -) DashboardAccess { +) *dashboardSqlAccess { dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) return &dashboardSqlAccess{ sql: sql, @@ -101,7 +140,6 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc: dashboardPermissionSvc, libraryPanelSvc: libraryPanelSvc, accessControl: accessControl, - log: log.New("dashboard.legacysql"), invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -149,6 +187,290 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD }, err } +// CountResources counts resources without migrating them +func (a *dashboardSqlAccess) CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + ns, err := claims.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + orgId := ns.OrgID + rsp := &resourcepb.BulkResponse{} + err = sql.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = folders.GROUP + summary.Group = folders.RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=TRUE AND org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/librarypanels": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = dashboardV1.GROUP + summary.Resource = dashboardV1.LIBRARY_PANEL_RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("library_element")+ + " WHERE org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/dashboards": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = dashboardV1.GROUP + summary.Resource = dashboardV1.DASHBOARD_RESOURCE + rsp.Summary = append(rsp.Summary, summary) + + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) + if err != nil { + return err + } + + // Also count history + _, err = sess.SQL(`SELECT COUNT(*) + FROM `+sql.Table("dashboard_version")+` as dv + JOIN `+sql.Table("dashboard")+` as dd + ON dd.id = dv.dashboard_id + WHERE org_id=?`, orgId).Get(&summary.History) + } + if err != nil { + return err + } + } + return nil + }) + return rsp, nil +} + +// MigrateDashboards handles the dashboard migration logic +func (a *dashboardSqlAccess) MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetHistory: opts.WithHistory, // include history + AllowFallback: true, // allow fallback to dashboard table during migration + Order: "ASC", // oldest first + } + + blobs := &BlobStoreInfo{} + sql, err := a.sql(ctx) + if err != nil { + return blobs, err + } + + opts.Progress(-1, "migrating dashboards...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return blobs, err + } + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + if dash.APIVersion == "" { + dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboardV1.GROUP) + } + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + body, err := json.Marshal(dash) + if err != nil { + err = fmt.Errorf("error reading json from: %s // %w", rows.row.Dash.Name, err) + return blobs, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboardV1.GROUP, + Resource: dashboardV1.DASHBOARD_RESOURCE, + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resourcepb.BulkRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resourcepb.BulkRequest_DELETED + } + + opts.Progress(i, fmt.Sprintf("[v:%2d] %s (size:%d / %d|%d)", dash.Generation, dash.Name, len(req.Value), i, rows.count)) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + opts.Progress(i, fmt.Sprintf("stream EOF/cancelled. index=%d", i)) + err = nil + } + return blobs, err + } + } + + if len(rows.rejected) > 0 { + for _, row := range rows.rejected { + id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] + a.log.Warn("rejected dashboard", + "namespace", opts.Namespace, + "dashboard", row.Dash.Name, + "uid", row.Dash.UID, + "id", id, + "version", row.Dash.Generation, + ) + opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) + } + } + + if rows.Error() != nil { + return blobs, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished dashboards... (%d)", rows.count)) + return blobs, err +} + +// MigrateFolders handles the folder migration logic +func (a *dashboardSqlAccess) MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetFolders: true, + Order: "ASC", + } + + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + + opts.Progress(-1, "migrating folders...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return nil, err + } + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + dash.APIVersion = "folder.grafana.app/v1beta1" + dash.Kind = "Folder" + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + spec := map[string]any{ + "title": dash.Spec.Object["title"], + } + description := dash.Spec.Object["description"] + if description != nil { + spec["description"] = description + } + dash.Spec.Object = spec + + body, err := json.Marshal(dash) + if err != nil { + return nil, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: "folder.grafana.app", + Resource: "folders", + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resourcepb.BulkRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resourcepb.BulkRequest_DELETED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", dash.Generation, dash.Name, len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + + if rows.Error() != nil { + return nil, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished folders... (%d)", rows.count)) + return nil, err +} + +// MigrateLibraryPanels handles the library panel migration logic +func (a *dashboardSqlAccess) MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + opts.Progress(-1, "migrating library panels...") + panels, err := a.GetLibraryPanels(ctx, LibraryPanelQuery{ + OrgID: orgId, + Limit: 1000000, + }) + if err != nil { + return nil, err + } + for i, panel := range panels.Items { + meta, err := utils.MetaAccessor(&panel) + if err != nil { + return nil, err + } + body, err := json.Marshal(panel) + if err != nil { + return nil, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboardV1.GROUP, + Resource: dashboardV1.LIBRARY_PANEL_RESOURCE, + Name: panel.Name, + }, + Value: body, + Folder: meta.GetFolder(), + Action: resourcepb.BulkRequest_ADDED, + } + if panel.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", i, meta.GetName(), len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + opts.Progress(-2, fmt.Sprintf("finished panels... (%d)", len(panels.Items))) + return nil, nil +} + var _ resource.ListIterator = (*rowsWrapper)(nil) type rowsWrapper struct { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index e33841c044c..b142b144800 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -21,6 +21,9 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" ) func TestScanRow(t *testing.T) { @@ -529,3 +532,146 @@ func TestParseLibraryPanelRow(t *testing.T) { require.Nil(t, updatedTimestamp) }) } + +func TestDashboardMigrationQuery(t *testing.T) { + // Test that migration queries use AllowFallback flag correctly + nodb := &legacysql.LegacyDatabaseHelper{ + Table: func(n string) string { + return "grafana." + n + }, + } + + t.Run("Migration query should enable AllowFallback flag", func(t *testing.T) { + // Create a migration query as would be used in actual migration + migrationQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, // Migration includes history + AllowFallback: true, // This is the key flag for migration + Order: "ASC", // Migration uses ascending order + } + + // Verify UseHistoryTable returns true (requirement for COALESCE logic) + require.True(t, migrationQuery.UseHistoryTable(), "Migration query should use history table") + + // Verify the flag is set correctly + require.True(t, migrationQuery.AllowFallback, "Migration query should allow fallback") + require.True(t, migrationQuery.GetHistory, "Migration query should get history") + require.Equal(t, "ASC", migrationQuery.Order, "Migration should use ascending order") + }) + + t.Run("Regular history query should not use AllowFallback", func(t *testing.T) { + // Regular history query without migration + historyQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + Order: "DESC", + } + + require.True(t, historyQuery.UseHistoryTable(), "History query should use history table") + require.False(t, historyQuery.AllowFallback, "Regular history query should not allow fallback") + require.True(t, historyQuery.GetHistory, "History query should get history") + }) + + t.Run("Migration query template produces COALESCE SQL", func(t *testing.T) { + // Test that the SQL template produces COALESCE logic for migration queries + migrationQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + AllowFallback: true, + Order: "ASC", + } + + req := newQueryReq(nodb, migrationQuery) + req.SQLTemplate = mocks.NewTestingSQLTemplate() + + // Execute the template to get the generated SQL + rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) + require.NoError(t, err) + + sql := rawQuery + + // Verify that COALESCE functions are present in the generated SQL + // These should be used when GetHistory=true AND AllowFallback=true + require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", + "Migration SQL should contain COALESCE for updated timestamp") + require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version)", + "Migration SQL should contain COALESCE for version") + require.Contains(t, sql, "COALESCE(dashboard_version.data, dashboard.data)", + "Migration SQL should contain COALESCE for data") + require.Contains(t, sql, "COALESCE(dashboard_version.api_version, dashboard.api_version)", + "Migration SQL should contain COALESCE for api_version") + require.Contains(t, sql, "COALESCE(dashboard_version.message, '')", + "Migration SQL should contain COALESCE for message with empty string fallback") + + // Verify ORDER BY uses COALESCE as well + require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated) ASC", + "Migration SQL should ORDER BY COALESCED created timestamp") + require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version) ASC", + "Migration SQL should ORDER BY COALESCED version") + + // Verify it doesn't have the strict history table filter that would exclude NULL version entries + require.NotContains(t, sql, "dashboard_version.id IS NOT NULL", + "Migration SQL should not exclude dashboards without version entries") + }) + + t.Run("Regular history query produces strict SQL", func(t *testing.T) { + // Test that regular history queries still use strict dashboard_version fields + historyQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + Order: "DESC", + } + + req := newQueryReq(nodb, historyQuery) + req.SQLTemplate = mocks.NewTestingSQLTemplate() + + rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) + require.NoError(t, err) + + sql := rawQuery + + // Verify that direct dashboard_version fields are used (no COALESCE) + require.Contains(t, sql, "dashboard_version.created as updated", + "Regular history SQL should use direct dashboard_version.created") + require.Contains(t, sql, "dashboard_version.version", + "Regular history SQL should use direct dashboard_version.version") + require.Contains(t, sql, "dashboard_version.data", + "Regular history SQL should use direct dashboard_version.data") + + // NOTE: We intentionally do NOT add dashboard_version.id IS NOT NULL filter + // to allow for cases where dashboard_version entries might be missing + + // Should not contain COALESCE functions + require.NotContains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", + "Regular history SQL should not contain COALESCE for updated") + }) +} + +func TestMigrateDashboardsConfiguration(t *testing.T) { + // Test the actual migration function configuration + + t.Run("Migration options should configure query correctly", func(t *testing.T) { + // Test the migration configuration as used in real migration + opts := MigrateOptions{ + WithHistory: true, // Migration includes history + } + + // This simulates what happens in migrateDashboards function + expectedQuery := &DashboardQuery{ + OrgID: 1, + Limit: 100000000, + GetHistory: opts.WithHistory, // Should be true + AllowFallback: true, // Should be true for migration + Order: "ASC", // Should be ASC for migration + } + + // Verify the configuration matches what migration sets up + require.True(t, expectedQuery.GetHistory, "Migration should enable GetHistory") + require.True(t, expectedQuery.AllowFallback, "Migration should enable AllowFallback") + require.Equal(t, "ASC", expectedQuery.Order, "Migration should use ascending order") + require.Equal(t, 100000000, expectedQuery.Limit, "Migration should use large limit") + + // Verify UseHistoryTable logic + require.True(t, expectedQuery.UseHistoryTable(), "Migration query should use history table") + }) +} diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index 4ffa7b80863..31d08af16d8 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -55,10 +55,9 @@ type LibraryPanelQuery struct { LastID int64 } -type DashboardAccess interface { +type DashboardAccessor interface { resource.StorageBackend resourcepb.ResourceIndexServer - LegacyMigrator GetDashboard(ctx context.Context, orgId int64, uid string, version int64) (*dashboardV1.Dashboard, int64, error) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardV1.Dashboard, failOnExisting bool) (*dashboardV1.Dashboard, bool, error) @@ -67,3 +66,12 @@ type DashboardAccess interface { // Get a typed list GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardV0.LibraryPanelList, error) } + +//go:generate mockery --name MigrationDashboardAccessor --structname MockMigrationDashboardAccessor --inpackage --filename migration_dashboard_accessor_mock.go --with-expecter +type MigrationDashboardAccessor interface { + // Migration helper methods - these support the separate LegacyMigrator + CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) + MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) + MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) + MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) +} diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index d51bea21bf9..1439574efe2 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -22,7 +22,7 @@ import ( ) type DashboardStorage struct { - Access legacy.DashboardAccess + Access legacy.DashboardAccessor DashboardService dashboards.DashboardService } diff --git a/pkg/registry/apis/dashboard/libary_panel.go b/pkg/registry/apis/dashboard/libary_panel.go index aa2b2705bb1..f170831da2b 100644 --- a/pkg/registry/apis/dashboard/libary_panel.go +++ b/pkg/registry/apis/dashboard/libary_panel.go @@ -30,7 +30,7 @@ var ( ) type LibraryPanelStore struct { - Access legacy.DashboardAccess + Access legacy.DashboardAccessor ResourceInfo utils.ResourceInfo service libraryelements.Service } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index f784fc9c014..fb42543de33 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -169,7 +169,7 @@ func RegisterAPIService( publicDashboardService: publicDashboardService, legacy: &DashboardStorage{ - Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), + Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), DashboardService: dashboardService, }, } diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index f2e9bc0b6df..04a14387fc7 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -30,7 +30,7 @@ type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAcce // The DTO returns everything the UI needs in a single request type DTOConnector struct { getter rest.Getter - legacy legacy.DashboardAccess + legacy legacy.DashboardAccessor unified resource.ResourceClient largeObjects apistore.LargeObjectSupport accessControl accesscontrol.AccessControl @@ -42,7 +42,7 @@ type DTOConnector struct { func NewDTOConnector( getter rest.Getter, largeObjects apistore.LargeObjectSupport, - legacyAccess legacy.DashboardAccess, + legacyAccess legacy.DashboardAccessor, resourceClient resource.ResourceClient, accessControl accesscontrol.AccessControl, scheme *runtime.Scheme, diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go index b7b4bb45a4f..48add96ee16 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/parquet" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -29,7 +30,7 @@ type LegacyResourcesMigrator interface { type legacyResourcesMigrator struct { repositoryResources resources.RepositoryResourcesFactory parsers resources.ParserFactory - legacyMigrator legacy.LegacyMigrator + dashboardAccess legacy.MigrationDashboardAccessor signerFactory signature.SignerFactory clients resources.ClientFactory exportFn export.ExportFn @@ -38,7 +39,7 @@ type legacyResourcesMigrator struct { func NewLegacyResourcesMigrator( repositoryResources resources.RepositoryResourcesFactory, parsers resources.ParserFactory, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, signerFactory signature.SignerFactory, clients resources.ClientFactory, exportFn export.ExportFn, @@ -46,7 +47,7 @@ func NewLegacyResourcesMigrator( return &legacyResourcesMigrator{ repositoryResources: repositoryResources, parsers: parsers, - legacyMigrator: legacyMigrator, + dashboardAccess: dashboardAccess, signerFactory: signerFactory, clients: clients, exportFn: exportFn, @@ -94,7 +95,7 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea reader := newLegacyResourceMigrator( rw, - m.legacyMigrator, + m.dashboardAccess, parser, repositoryResources, progress, @@ -113,21 +114,21 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea } type legacyResourceResourceMigrator struct { - repo repository.ReaderWriter - legacy legacy.LegacyMigrator - parser resources.Parser - progress jobs.JobProgressRecorder - namespace string - kind schema.GroupResource - options provisioning.MigrateJobOptions - resources resources.RepositoryResources - signer signature.Signer - history map[string]string // UID >> file path + repo repository.ReaderWriter + dashboardAccess legacy.MigrationDashboardAccessor + parser resources.Parser + progress jobs.JobProgressRecorder + namespace string + kind schema.GroupResource + options provisioning.MigrateJobOptions + resources resources.RepositoryResources + signer signature.Signer + history map[string]string // UID >> file path } func newLegacyResourceMigrator( repo repository.ReaderWriter, - legacy legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, parser resources.Parser, resources resources.RepositoryResources, progress jobs.JobProgressRecorder, @@ -141,16 +142,16 @@ func newLegacyResourceMigrator( history = make(map[string]string) } return &legacyResourceResourceMigrator{ - repo: repo, - legacy: legacy, - parser: parser, - progress: progress, - options: options, - namespace: namespace, - kind: kind, - resources: resources, - signer: signer, - history: history, + repo: repo, + dashboardAccess: dashboardAccess, + parser: parser, + progress: progress, + options: options, + namespace: namespace, + kind: kind, + resources: resources, + signer: signer, + history: history, } } @@ -225,14 +226,21 @@ func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourc func (r *legacyResourceResourceMigrator) Migrate(ctx context.Context) error { r.progress.SetMessage(ctx, fmt.Sprintf("migrate %s resource", r.kind.Resource)) + + // Create a parquet migrator with this instance as the BulkResourceWriter + parquetClient := parquet.NewBulkResourceWriterClient(r) + migrator := unifiedmigrations.ProvideUnifiedMigratorParquet( + r.dashboardAccess, + parquetClient, + ) + opts := legacy.MigrateOptions{ Namespace: r.namespace, WithHistory: r.options.History, Resources: []schema.GroupResource{r.kind}, - Store: parquet.NewBulkResourceWriterClient(r), OnlyCount: true, // first get the count } - stats, err := r.legacy.Migrate(ctx, opts) + stats, err := migrator.Migrate(ctx, opts) if err != nil { return fmt.Errorf("unable to count legacy items %w", err) } @@ -248,7 +256,7 @@ func (r *legacyResourceResourceMigrator) Migrate(ctx context.Context) error { } opts.OnlyCount = false // this time actually write - _, err = r.legacy.Migrate(ctx, opts) + _, err = migrator.Migrate(ctx, opts) if err != nil { return fmt.Errorf("migrate legacy %s: %w", r.kind.Resource, err) } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go index 89077848f25..7f4318639c2 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go @@ -93,8 +93,8 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). Return(mockRepoResources, nil) - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, errors.New("legacy migrator error")) @@ -115,7 +115,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { migrator := NewLegacyResourcesMigrator( mockRepoResourcesFactory, mockParserFactory, - mockLegacyMigrator, + mockDashboardAccess, signerFactory, mockClientFactory, mockExportFn.Execute, @@ -136,7 +136,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockParserFactory.AssertExpectations(t) mockRepoResourcesFactory.AssertExpectations(t) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) mockExportFn.AssertExpectations(t) mockClientFactory.AssertExpectations(t) @@ -308,22 +308,18 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { History: true, }).Return(mockSigner, nil) - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + // Mock CountResources for the count phase + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + })).Return(&resourcepb.BulkResponse{}, nil).Once() + // Mock MigrateDashboards for the actual migration phase (dashboards resource) + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{ - Summary: []*resourcepb.BulkResponse_Summary{ - { - Group: "test.grafana.app", - Resource: "tests", - Count: 10, - History: 5, - }, - }, - }, nil).Once() // Migration phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{ + Count: 10, + Size: 5, + }, nil).Once() mockClients := resources.NewMockResourceClients(t) mockClientFactory := resources.NewMockClientFactory(t) @@ -339,7 +335,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { migrator := NewLegacyResourcesMigrator( mockRepoResourcesFactory, mockParserFactory, - mockLegacyMigrator, + mockDashboardAccess, mockSignerFactory, mockClientFactory, mockExportFn.Execute, @@ -362,7 +358,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockParserFactory.AssertExpectations(t) mockRepoResourcesFactory.AssertExpectations(t) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) mockClientFactory.AssertExpectations(t) mockExportFn.AssertExpectations(t) progress.AssertExpectations(t) @@ -742,8 +738,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) { func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { t.Run("should fail when legacy migrate count fails", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, errors.New("count error")) @@ -752,7 +748,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, @@ -766,89 +762,91 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "unable to count legacy items") - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should fail when legacy migrate write fails", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + // For test-resources GroupResource, we don't know which method it will call, but since it's not dashboards/folders/librarypanels, + // the Migrate will fail trying to map the resource type. Let's make it dashboards for this test. + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, errors.New("write error")).Once() // Write phase + }), mock.Anything).Return(nil, errors.New("write error")).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "test-resources"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.Error(t, err) - require.Contains(t, err.Error(), "migrate legacy test-resources: write error") + require.Contains(t, err.Error(), "migrate legacy dashboards: write error") - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should successfully migrate resource", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should set total to history if history is greater than count", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{ Summary: []*resourcepb.BulkResponse_Summary{ { - Group: "test.grafana.app", - Resource: "tests", + Group: "dashboard.grafana.app", + Resource: "dashboards", Count: 1, History: 100, }, }, }, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() @@ -856,39 +854,39 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should set total to count if history is less than count", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{ Summary: []*resourcepb.BulkResponse_Summary{ { - Group: "test.grafana.app", - Resource: "tests", + Group: "dashboard.grafana.app", + Resource: "dashboards", Count: 200, History: 1, }, }, }, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() @@ -897,20 +895,20 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signer, ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) } diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index c30b43eeb60..d18fc1156a8 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -60,6 +60,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -109,7 +110,7 @@ type APIBuilder struct { jobHistoryConfig *JobHistoryConfig jobHistoryLoki *jobs.LokiJobHistory resourceLister resources.ResourceLister - legacyMigrator legacy.LegacyMigrator + dashboardAccess legacy.MigrationDashboardAccessor storageStatus dualwrite.Service unified resource.ResourceClient repoFactory repository.Factory @@ -135,7 +136,7 @@ func NewAPIBuilder( features featuremgmt.FeatureToggles, unified resource.ResourceClient, configProvider apiserver.RestConfigProvider, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, storageStatus dualwrite.Service, usageStats usagestats.Service, access authlib.AccessChecker, @@ -158,6 +159,7 @@ func NewAPIBuilder( clients = resources.NewClientFactory(configProvider) } parsers := resources.NewParserFactory(clients) + legacyMigrator := migrations.ProvideUnifiedMigrator(dashboardAccess, unified) resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) b := &APIBuilder{ @@ -170,7 +172,7 @@ func NewAPIBuilder( parsers: parsers, repositoryResources: resources.NewRepositoryResourcesFactory(parsers, clients, resourceLister), resourceLister: resourceLister, - legacyMigrator: legacyMigrator, + dashboardAccess: dashboardAccess, storageStatus: storageStatus, unified: unified, access: access, @@ -234,7 +236,7 @@ func RegisterAPIService( client resource.ResourceClient, // implements resource.RepositoryClient configProvider apiserver.RestConfigProvider, access authlib.AccessClient, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, storageStatus dualwrite.Service, usageStats usagestats.Service, tracer tracing.Tracer, @@ -258,7 +260,7 @@ func RegisterAPIService( features, client, configProvider, - legacyMigrator, storageStatus, + dashboardAccess, storageStatus, usageStats, access, tracer, @@ -722,7 +724,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH legacyResources := migrate.NewLegacyResourcesMigrator( b.repositoryResources, b.parsers, - b.legacyMigrator, + b.dashboardAccess, signerFactory, b.clients, export.ExportAll, @@ -1241,8 +1243,9 @@ func (b *APIBuilder) tryRunningOnlyUnifiedStorage() error { return nil } - // Count how many things exist - rsp, err := b.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + // Count how many things exist - create a migrator on-demand for this + legacyMigrator := migrations.ProvideUnifiedMigrator(b.dashboardAccess, b.unified) + rsp, err := legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ Namespace: "default", // FIXME! this works for single org, but need to check multi-org Resources: []schema.GroupResource{{ Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, diff --git a/pkg/registry/apis/provisioning/resources/object.go b/pkg/registry/apis/provisioning/resources/object.go index 4d08de5c05c..0a016786410 100644 --- a/pkg/registry/apis/provisioning/resources/object.go +++ b/pkg/registry/apis/provisioning/resources/object.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -30,9 +31,9 @@ type ResourceStore interface { } type ResourceListerFromSearch struct { - store ResourceStore - legacyMigrator legacy.LegacyMigrator - storageStatus dualwrite.Service + store ResourceStore + migrator migrations.UnifiedMigrator + storageStatus dualwrite.Service } func NewResourceLister(store ResourceStore) ResourceLister { @@ -42,13 +43,13 @@ func NewResourceLister(store ResourceStore) ResourceLister { // FIXME: the logic about migration and storage should probably be separated from this func NewResourceListerForMigrations( store ResourceStore, - legacyMigrator legacy.LegacyMigrator, + migrator migrations.UnifiedMigrator, storageStatus dualwrite.Service, ) ResourceLister { return &ResourceListerFromSearch{ - store: store, - legacyMigrator: legacyMigrator, - storageStatus: storageStatus, + store: store, + migrator: migrator, + storageStatus: storageStatus, } } @@ -133,8 +134,8 @@ func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, reposit } // Get the stats based on what a migration could support - if o.storageStatus != nil && o.legacyMigrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { - rsp, err := o.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + if o.storageStatus != nil && o.migrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { + rsp, err := o.migrator.Migrate(ctx, legacy.MigrateOptions{ Namespace: namespace, Resources: []schema.GroupResource{{ Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 5f997875985..e80bcb720c9 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -48,7 +48,6 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/services/team/teamapi" "github.com/grafana/grafana/pkg/services/updatemanager" - unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" ) func ProvideBackgroundServiceRegistry( @@ -74,7 +73,6 @@ func ProvideBackgroundServiceRegistry( dashboardServiceImpl *service.DashboardServiceImpl, secretsGarbageCollectionWorker *secretsgarbagecollectionworker.Worker, fixedRolesLoader *accesscontrol.FixedRolesLoader, - unifiedStorageMigrationProvider unifiedmigrations.UnifiedStorageMigrationProvider, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ serviceaccounts.Service, @@ -91,7 +89,6 @@ func ProvideBackgroundServiceRegistry( notifications, rendering, tokenService, - unifiedStorageMigrationProvider, provisioning, grafanaUpdateChecker, pluginsUpdateChecker, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 2fe6db16b31..0d4bb10c0b5 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -128,6 +128,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/preference/prefimpl" promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration" + "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/publicdashboards" publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" @@ -238,7 +239,9 @@ var wireBasicSet = wire.NewSet( uss.ProvideService, wire.Bind(new(usagestats.Service), new(*uss.UsageStats)), validator.ProvideService, - legacy.ProvideLegacyMigrator, + provisioning.ProvideStubProvisioningService, + legacy.ProvideMigratorDashboardAccessor, + unifiedmigrations.ProvideUnifiedMigrator, pluginsintegration.WireSet, pluginDashboards.ProvideFileStoreManager, wire.Bind(new(pluginDashboards.FileStore), new(*pluginDashboards.FileStoreManager)), @@ -466,8 +469,7 @@ var wireBasicSet = wire.NewSet( // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, - unifiedmigrations.ProvideUnifiedStorageMigrationProvider, - wire.Bind(new(unifiedmigrations.UnifiedStorageMigrationProvider), new(*unifiedmigrations.UnifiedStorageMigrationProviderImpl)), + unifiedmigrations.ProvideUnifiedStorageMigrationService, // Kubernetes API server grafanaapiserver.WireSet, apiregistry.WireSet, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 91a1e50a224..0cb27791c19 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -219,7 +219,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" - migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" + migrations3 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" "github.com/grafana/grafana/pkg/services/secrets/manager" migrator2 "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -255,13 +255,14 @@ import ( "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" database4 "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" - migrations3 "github.com/grafana/grafana/pkg/storage/unified/migrations" + migrations2 "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" @@ -526,7 +527,15 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg) + legacyDatabaseProvider := legacysql.NewDatabaseProvider(sqlStore) + stubProvisioningService, err := provisioning.ProvideStubProvisioningService(cfg) + if err != nil { + return nil, err + } + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err } @@ -716,8 +725,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) - secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + dataSourceSecretMigrationService := migrations3.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) + secretMigrationProviderImpl := migrations3.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) @@ -838,8 +847,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -892,7 +899,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) + provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, migrationDashboardAccessor, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) if err != nil { return nil, err } @@ -920,7 +927,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokenService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationService) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1167,7 +1174,15 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg) + legacyDatabaseProvider := legacysql.NewDatabaseProvider(sqlStore) + stubProvisioningService, err := provisioning.ProvideStubProvisioningService(cfg) + if err != nil { + return nil, err + } + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err } @@ -1359,8 +1374,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) - secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + dataSourceSecretMigrationService := migrations3.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) + secretMigrationProviderImpl := migrations3.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) @@ -1481,8 +1496,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -1535,7 +1548,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) + provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, migrationDashboardAccessor, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) if err != nil { return nil, err } @@ -1563,7 +1576,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokentestService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationServiceMock) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1759,7 +1772,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, migrations3.ProvideUnifiedStorageMigrationProvider, wire.Bind(new(migrations3.UnifiedStorageMigrationProvider), new(*migrations3.UnifiedStorageMigrationProviderImpl)), apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, provisioning.ProvideStubProvisioningService, legacy.ProvideMigratorDashboardAccessor, migrations2.ProvideUnifiedMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations3.ProvideDataSourceMigrationService, migrations3.ProvideSecretMigrationProvider, wire.Bind(new(migrations3.SecretMigrationProvider), new(*migrations3.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, migrations2.ProvideUnifiedStorageMigrationService, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index b2b134ee4a9..9f069f807fc 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -62,6 +62,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" search2 "github.com/grafana/grafana/pkg/storage/unified/search" @@ -98,6 +99,7 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(validations.DataSourceRequestURLValidator), new(*validations.OSSDataSourceRequestURLValidator)), provisioning.ProvideService, wire.Bind(new(provisioning.ProvisioningService), new(*provisioning.ProvisioningServiceImpl)), + legacysql.NewDatabaseProvider, backgroundsvcs.ProvideBackgroundServiceRegistry, wire.Bind(new(registry.BackgroundServiceRegistry), new(*backgroundsvcs.BackgroundServiceRegistry)), migrations.ProvideOSSMigrations, diff --git a/pkg/services/apiserver/appinstaller/storage.go b/pkg/services/apiserver/appinstaller/storage.go index 3ecc34cce58..98989508858 100644 --- a/pkg/services/apiserver/appinstaller/storage.go +++ b/pkg/services/apiserver/appinstaller/storage.go @@ -107,7 +107,7 @@ func NewDualWriter( if currentMode != mode { klog.Warningf("Requested DualWrite mode: %d, but using %d for %+v", mode, currentMode, gr) } - return dualwrite.NewDualWriter(gr, currentMode, legacy, storage) + return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage) } func getRequestInfo(gr schema.GroupResource, namespaceMapper request.NamespaceMapper) *k8srequest.RequestInfo { diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 98a9c08ed25..603c49f3cb2 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -379,7 +379,7 @@ func InstallAPIs( case grafanarest.Mode4, grafanarest.Mode5: return storage, nil default: - return dualwrite.NewDualWriter(gr, currentMode, legacy, storage) + return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage) } } } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 7d4ee3c0d5a..28da416536c 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -97,7 +97,7 @@ type service struct { authorizer *authorizer.GrafanaAuthorizer serverLockService builder.ServerLockService - storageStatus dualwrite.Service + dualWriter dualwrite.Service kvStore kvstore.KVStore pluginClient plugins.Client @@ -127,7 +127,7 @@ func ProvideService( datasources datasource.ScopedPluginDatasourceProvider, contextProvider datasource.PluginContextWrapper, pluginStore pluginstore.Store, - storageStatus dualwrite.Service, + dualWriter dualwrite.Service, unified resource.ResourceClient, secrets secret.InlineSecureValueSupport, restConfigProvider RestConfigProvider, @@ -158,7 +158,7 @@ func ProvideService( contextProvider: contextProvider, pluginStore: pluginStore, serverLockService: serverLockService, - storageStatus: storageStatus, + dualWriter: dualWriter, unified: unified, secrets: secrets, restConfigProvider: restConfigProvider, @@ -385,12 +385,17 @@ func (s *service) start(ctx context.Context) error { } // Install the API group+version for existing builders - err = builder.InstallAPIs(s.scheme, s.codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions, + err = builder.InstallAPIs(s.scheme, + s.codecs, + server, + serverConfig.RESTOptionsGetter, + builders, + o.StorageOptions, s.metrics, request.GetNamespaceMapper(s.cfg), kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"), s.serverLockService, - s.storageStatus, + s.dualWriter, optsregister, s.features, s.dualWriterMetrics, @@ -409,7 +414,7 @@ func (s *service) start(ctx context.Context) error { kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"), s.serverLockService, request.GetNamespaceMapper(s.cfg), - s.storageStatus, + s.dualWriter, s.dualWriterMetrics, s.builderMetrics, serverConfig.MergedResourceConfig, diff --git a/pkg/services/provisioning/stubs.go b/pkg/services/provisioning/stubs.go new file mode 100644 index 00000000000..b40e66aac5c --- /dev/null +++ b/pkg/services/provisioning/stubs.go @@ -0,0 +1,69 @@ +package provisioning + +import ( + "os" + "path/filepath" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/provisioning/dashboards" + "github.com/grafana/grafana/pkg/setting" +) + +type StubProvisioningService interface { + GetDashboardProvisionerResolvedPath(name string) string + GetAllowUIUpdatesFromConfig(name string) bool +} + +func ProvideStubProvisioningService(cfg *setting.Cfg) (StubProvisioningService, error) { + return NewStubProvisioning(cfg.ProvisioningPath) +} + +func NewStubProvisioning(path string) (StubProvisioningService, error) { + cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) + if err != nil { + return nil, err + } + stub := &stubProvisioning{ + path: make(map[string]string), + allowUIUpdates: make(map[string]bool), + log: log.New("provisioning.stub"), + } + for _, cfg := range cfgs { + stub.path[cfg.Name] = cfg.Options["path"].(string) + stub.allowUIUpdates[cfg.Name] = cfg.AllowUIUpdates + } + return stub, nil +} + +type stubProvisioning struct { + path map[string]string // name > options.path + allowUIUpdates map[string]bool + log log.Logger +} + +func (s *stubProvisioning) GetAllowUIUpdatesFromConfig(name string) bool { + return s.allowUIUpdates[name] +} + +func (s *stubProvisioning) GetDashboardProvisionerResolvedPath(name string) string { + path := s.path[name] + if _, err := os.Stat(path); os.IsNotExist(err) { + s.log.Warn("Cannot read directory", "error", err) + } + + path, err := filepath.Abs(path) + if err != nil { + s.log.Warn("Could not create absolute path", "path", path, "error", err) + } + + path, err = filepath.EvalSymlinks(path) + if err != nil { + s.log.Warn("Failed to read content of symlinked path", "path", path, "error", err) + } + + if path == "" { + path = s.path[name] + s.log.Info("falling back to original path due to EvalSymlink/Abs failure") + } + return path +} diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go index b864e3a360d..777d791d4e5 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go @@ -71,7 +71,7 @@ func TestMode1_Create(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{}) @@ -154,7 +154,7 @@ func TestMode1_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) @@ -217,7 +217,7 @@ func TestMode1_List(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) _, err = dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -286,7 +286,7 @@ func TestMode1_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -361,7 +361,7 @@ func TestMode1_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{}) @@ -434,7 +434,7 @@ func TestMode1_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go index cd1e87b0b9a..a45dfa1285d 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go @@ -69,7 +69,7 @@ func TestMode2_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) @@ -154,7 +154,7 @@ func TestMode2_Get(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) @@ -229,7 +229,7 @@ func TestMode2_List(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -331,7 +331,7 @@ func TestMode2_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -401,7 +401,7 @@ func TestMode2_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) @@ -471,7 +471,7 @@ func TestMode2_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go index 3076d8a9409..d2485efb762 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go @@ -75,7 +75,7 @@ func TestMode3_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) @@ -134,7 +134,7 @@ func TestMode3_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) @@ -185,7 +185,7 @@ func TestMode3_List(t *testing.T) { tt.setupStorageFn(us.Mock, &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) res, err := dw.List(context.Background(), &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) @@ -276,7 +276,7 @@ func TestMode3_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -346,7 +346,7 @@ func TestMode3_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) @@ -416,7 +416,7 @@ func TestMode3_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/runtime.go b/pkg/storage/legacysql/dualwrite/runtime.go index 180beeb255a..b9c5355050c 100644 --- a/pkg/storage/legacysql/dualwrite/runtime.go +++ b/pkg/storage/legacysql/dualwrite/runtime.go @@ -14,37 +14,6 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) -func (m *service) NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, unified grafanarest.Storage) (grafanarest.Storage, error) { - status, err := m.Status(context.Background(), gr) - if err != nil { - return nil, err - } - - if m.enabled && status.Runtime { - // Dynamic storage behavior - return &runtimeDualWriter{ - service: m, - legacy: legacy, - unified: unified, - dualwrite: &dualWriter{legacy: legacy, unified: unified}, // not used for read - gr: gr, - }, nil - } - - if status.ReadUnified { - if status.WriteLegacy { - // Write both, read unified - return &dualWriter{legacy: legacy, unified: unified, readUnified: true}, nil - } - return unified, nil - } - if status.WriteUnified { - // Write both, read legacy - return &dualWriter{legacy: legacy, unified: unified}, nil - } - return legacy, nil -} - // The runtime dual writer implements the various modes we have described as: mode:1/2/3/4/5 // However the behavior can be configured at runtime rather than just at startup. // When a resource is marked as "migrating", all write requests will be 503 unavailable diff --git a/pkg/storage/legacysql/dualwrite/runtime_test.go b/pkg/storage/legacysql/dualwrite/runtime_test.go index b59801e3da8..01dd79647a7 100644 --- a/pkg/storage/legacysql/dualwrite/runtime_test.go +++ b/pkg/storage/legacysql/dualwrite/runtime_test.go @@ -77,7 +77,7 @@ func TestRuntime_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) dw, err := m.NewStorage(kind, ls, us) require.NoError(t, err) @@ -150,7 +150,7 @@ func TestRuntime_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) dw, err := m.NewStorage(kind, ls, us) require.NoError(t, err) @@ -235,7 +235,7 @@ func TestRuntime_CreateWhileMigrating(t *testing.T) { } // Shared provider across all tests - dual, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + dual, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) for _, tt := range tests { diff --git a/pkg/storage/legacysql/dualwrite/service.go b/pkg/storage/legacysql/dualwrite/service.go index 77ecc761de1..8ac88deb281 100644 --- a/pkg/storage/legacysql/dualwrite/service.go +++ b/pkg/storage/legacysql/dualwrite/service.go @@ -12,8 +12,28 @@ import ( "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" ) +// fakeMigrator is a no-op implementation of UnifiedStorageMigrationService +type fakeMigrator struct{} + +func (f *fakeMigrator) Run(ctx context.Context) error { + return nil +} + +var _ unifiedmigrations.UnifiedStorageMigrationService = (*fakeMigrator)(nil) + +func NewFakeMigrator() unifiedmigrations.UnifiedStorageMigrationService { + return &fakeMigrator{} +} + +func NewFakeConfig() *setting.Cfg { + return &setting.Cfg{ + UnifiedStorage: make(map[string]setting.UnifiedStorageConfig), + } +} + func ProvideStaticServiceForTests(cfg *setting.Cfg) Service { if cfg == nil { cfg = &setting.Cfg{} @@ -25,7 +45,14 @@ func ProvideService( features featuremgmt.FeatureToggles, kv kvstore.KVStore, cfg *setting.Cfg, + migrator unifiedmigrations.UnifiedStorageMigrationService, ) (Service, error) { + // Ensure migrations have run before starting dualwrite + err := migrator.Run(context.Background()) + if err != nil { + return nil, fmt.Errorf("unable to start dualwrite service due to migration error: %w", err) + } + //nolint:staticcheck // not yet migrated to OpenFeature enabled := features.IsEnabledGlobally(featuremgmt.FlagManagedDualWriter) || features.IsEnabledGlobally(featuremgmt.FlagProvisioning) // required for git provisioning @@ -64,6 +91,37 @@ type service struct { enabled bool } +func (m *service) NewStorage(gr schema.GroupResource, legacy rest.Storage, unified rest.Storage) (rest.Storage, error) { + status, err := m.Status(context.Background(), gr) + if err != nil { + return nil, err + } + + if m.enabled && status.Runtime { + // Dynamic storage behavior + return &runtimeDualWriter{ + service: m, + legacy: legacy, + unified: unified, + dualwrite: &dualWriter{legacy: legacy, unified: unified}, // not used for read + gr: gr, + }, nil + } + + if status.ReadUnified { + if status.WriteLegacy { + // Write both, read unified + return &dualWriter{legacy: legacy, unified: unified, readUnified: true}, nil + } + return unified, nil + } + if status.WriteUnified { + // Write both, read legacy + return &dualWriter{legacy: legacy, unified: unified}, nil + } + return legacy, nil +} + // Hardcoded list of resources that should be controlled by the database (eventually everything?) func (m *service) ShouldManage(gr schema.GroupResource) bool { if !m.enabled { diff --git a/pkg/storage/legacysql/dualwrite/service_test.go b/pkg/storage/legacysql/dualwrite/service_test.go index a083e6470be..f9fcb9f9b38 100644 --- a/pkg/storage/legacysql/dualwrite/service_test.go +++ b/pkg/storage/legacysql/dualwrite/service_test.go @@ -17,7 +17,7 @@ import ( func TestService(t *testing.T) { t.Run("dynamic", func(t *testing.T) { ctx := context.Background() - mode, err := ProvideService(featuremgmt.WithFeatures(), kvstore.NewFakeKVStore(), nil) + mode, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagProvisioning), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) gr := schema.GroupResource{Group: "ggg", Resource: "rrr"} @@ -122,7 +122,7 @@ func TestService(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - svc, err := ProvideService(tc.flags, kvstore.NewFakeKVStore(), &tc.cfg) + svc, err := ProvideService(tc.flags, kvstore.NewFakeKVStore(), &tc.cfg, NewFakeMigrator()) if tc.error != "" { require.ErrorContains(t, err, tc.error) require.Nil(t, svc, "expect a nil service when an error exts") diff --git a/pkg/storage/legacysql/dualwrite/static.go b/pkg/storage/legacysql/dualwrite/static.go index 3835cf80752..bd51b9b6ac6 100644 --- a/pkg/storage/legacysql/dualwrite/static.go +++ b/pkg/storage/legacysql/dualwrite/static.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -// NewDualWriter -- temporary shim -func NewDualWriter( +// NewStaticStorage -- temporary shim +func NewStaticStorage( gr schema.GroupResource, mode rest.DualWriterMode, legacy rest.Storage, diff --git a/pkg/storage/unified/migrations/contract/migrations.go b/pkg/storage/unified/migrations/contract/migrations.go new file mode 100644 index 00000000000..fe1c42cea21 --- /dev/null +++ b/pkg/storage/unified/migrations/contract/migrations.go @@ -0,0 +1,12 @@ +package contract + +import ( + "github.com/grafana/grafana/pkg/registry" +) + +// UnifiedStorageMigrationService provides unified storage migrations as a background service. +// This interface is defined in a separate package to avoid import cycles between +// the migrations implementation and packages that need to depend on it (like dualwrite). +type UnifiedStorageMigrationService interface { + registry.BackgroundService +} diff --git a/pkg/storage/unified/migrations/dashboard_folder_migration.go b/pkg/storage/unified/migrations/dashboard_folder_migration.go deleted file mode 100644 index 96c5a824c8a..00000000000 --- a/pkg/storage/unified/migrations/dashboard_folder_migration.go +++ /dev/null @@ -1,93 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - - "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/util/xorm" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - FoldersAndDashboardsMigrationID = "folders and dashboards migration" - UnifiedStorageDataMigrationSQL = "unified storage data migration" -) - -type dashboardAndFolderMigration struct { - migrator.MigrationBase - legacyMigrator legacy.LegacyMigrator - bulkStoreClient resource.ResourceClient -} - -var _ migrator.CodeMigration = (*dashboardAndFolderMigration)(nil) - -// SQL implements migrator.Migration interface. Returns a description string. -func (sp *dashboardAndFolderMigration) SQL(dialect migrator.Dialect) string { - return UnifiedStorageDataMigrationSQL -} - -func (sp *dashboardAndFolderMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { - ctx := context.Background() - logger := mg.Logger - - resources := []schema.GroupResource{ - { - Group: "folder.grafana.app", - Resource: "folders", - }, - { - Group: "dashboard.grafana.app", - Resource: "dashboards", - }, - } - - storageMigrator := newUnifiedStorageMigrator(sp.legacyMigrator, sp.bulkStoreClient, resources, "unified-storage-migration.folders-dashboards") - - orgs, err := sp.getAllOrgs(sess) - if err != nil { - logger.Error("failed to get organizations for folders and dashboards migration", "error", err) - return fmt.Errorf("failed to get organizations: %w", err) - } - - if len(orgs) == 0 { - logger.Info("No organizations found to migrate, skipping migration") - return nil - } - - logger.Info("Starting migration for all organizations", "org_count", len(orgs)) - - for _, org := range orgs { - namespace := types.OrgNamespaceFormatter(org.ID) - logger.Info("Migrating organization", "org_id", org.ID, "org_name", org.Name, "namespace", namespace) - - // Create a service identity context for this namespace to authenticate with unified storage - migrationCtx, _ := identity.WithServiceIdentityForSingleNamespace(ctx, namespace) - - if err := storageMigrator.executeMigration(migrationCtx, sess, mg, namespace); err != nil { - logger.Error("migration failed for organization", "org_id", org.ID, "org_name", org.Name, "error", err) - return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) - } - } - - logger.Info("Migration completed successfully for all organizations", "org_count", len(orgs)) - return nil -} - -type orgInfo struct { - ID int64 `xorm:"id"` - Name string `xorm:"name"` -} - -func (sp *dashboardAndFolderMigration) getAllOrgs(sess *xorm.Session) ([]orgInfo, error) { - var orgs []orgInfo - err := sess.Table("org").Cols("id", "name").Find(&orgs) - if err != nil { - return nil, err - } - return orgs, nil -} diff --git a/pkg/storage/unified/migrations/migrations.go b/pkg/storage/unified/migrations/migrations.go deleted file mode 100644 index a30777e308a..00000000000 --- a/pkg/storage/unified/migrations/migrations.go +++ /dev/null @@ -1,112 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - "os" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel" -) - -var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") -var logger = log.New("storage.unified.migrations") - -// UnifiedStorageMigrationProvider provides unified storage migrations as a background service -type UnifiedStorageMigrationProvider interface { - registry.BackgroundService -} - -type UnifiedStorageMigrationProviderImpl struct { - legacyMigrator legacy.LegacyMigrator - cfg *setting.Cfg - client resource.ResourceClient - sqlStore db.DB -} - -var _ UnifiedStorageMigrationProvider = (*UnifiedStorageMigrationProviderImpl)(nil) - -// ProvideUnifiedStorageMigrationProvider is a Wire provider that creates the migration service. -// The service implements registry.BackgroundService and runs migrations during server startup. -func ProvideUnifiedStorageMigrationProvider( - legacyMigrator legacy.LegacyMigrator, - cfg *setting.Cfg, - client resource.ResourceClient, - sqlStore db.DB, -) *UnifiedStorageMigrationProviderImpl { - return &UnifiedStorageMigrationProviderImpl{ - legacyMigrator: legacyMigrator, - cfg: cfg, - client: client, - sqlStore: sqlStore, - } -} - -// Run executes unified storage migrations as a background service. -// This blocks until migrations complete. If migrations fail, an error is returned -// which will prevent Grafana from starting. -func (p *UnifiedStorageMigrationProviderImpl) Run(ctx context.Context) error { - // skip migrations in test environments to prevent integration test timeouts. - if os.Getenv("GRAFANA_TEST_DB") != "" { - return nil - } - // skip migrations if disabled in config - if p.cfg.DisableDataMigrations { - logger.Info("Data migrations are disabled, skipping") - return nil - } - // TODO: Re-enable once migrations are ready - // return RegisterMigrations(p.legacyMigrator, p.cfg, p.client, p.sqlStore) - return nil -} - -// RegisterMigrations initializes and registers all unified storage migrations. -// This function is the entry point for all data migrations from legacy storage -// to unified storage. It returns an error if migrations fail, preventing Grafana -// from starting with inconsistent data. -func RegisterMigrations( - legacyMigrator legacy.LegacyMigrator, - cfg *setting.Cfg, - client resource.ResourceClient, - sqlStore db.DB, -) error { - ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") - defer span.End() - mg := migrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unified_storage") - mg.AddCreateMigration() - - if err := prometheus.Register(mg); err != nil { - logger.Warn("Failed to register migrator metrics", "error", err) - } - - // Add new migration registrations here for each resource type - registerDashboardAndFolderMigration(mg, legacyMigrator, client) - - // Run all registered migrations (blocking) - sec := cfg.Raw.Section("database") - if err := mg.RunMigrations(ctx, sec.Key("migration_locking").MustBool(true), sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { - return fmt.Errorf("unified storage data migration failed: %w", err) - } - - logger.Info("Unified storage migrations completed successfully") - return nil -} - -func registerDashboardAndFolderMigration( - mg *migrator.Migrator, - legacyMigrator legacy.LegacyMigrator, - bulkStoreClient resource.ResourceClient, -) { - migration := &dashboardAndFolderMigration{ - legacyMigrator: legacyMigrator, - bulkStoreClient: bulkStoreClient, - } - mg.AddMigration(FoldersAndDashboardsMigrationID, migration) -} diff --git a/pkg/storage/unified/migrations/migrator.go b/pkg/storage/unified/migrations/migrator.go index cab6ce38432..bdbb69f98a1 100644 --- a/pkg/storage/unified/migrations/migrator.go +++ b/pkg/storage/unified/migrations/migrator.go @@ -3,164 +3,200 @@ package migrations import ( "context" "fmt" - "time" - "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "google.golang.org/grpc/metadata" + + authlib "github.com/grafana/authlib/types" + + v1beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/util/xorm" - "k8s.io/apimachinery/pkg/runtime/schema" ) -// StorageMigrator defines the interface for executing unified storage migrations -type StorageMigrator interface { - executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error +// Read from legacy and write into unified storage +// +//go:generate mockery --name UnifiedMigrator --structname MockUnifiedMigrator --inpackage --filename migrator_mock.go --with-expecter +type UnifiedMigrator interface { + Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) } -type unifiedStorageMigrator struct { - migrator legacy.LegacyMigrator - bulkStoreClient resource.ResourceClient - resources []schema.GroupResource - log log.Logger +// unifiedMigration handles the migration of legacy resources to unified storage +type unifiedMigration struct { + legacy.MigrationDashboardAccessor + streamProvider streamProvider + log log.Logger } -func newUnifiedStorageMigrator(migrator legacy.LegacyMigrator, bulkStoreClient resource.ResourceClient, resources []schema.GroupResource, logPrefix string) StorageMigrator { - return &unifiedStorageMigrator{ - migrator: migrator, - bulkStoreClient: bulkStoreClient, - resources: resources, - log: log.New(logPrefix), - } +// streamProvider abstracts the different ways to create a bulk process stream +type streamProvider interface { + createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) } -func (m *unifiedStorageMigrator) executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error { - startTime := time.Now() - m.log.Info("Starting unified storage migration", "namespace", namespace, "resources", m.resources) - - opts := legacy.MigrateOptions{ - Namespace: namespace, - Store: m.bulkStoreClient, - LargeObjects: nil, // Not using large object support to avoid import cycles - Resources: m.resources, - WithHistory: true, // Migrate with full history - OnlyCount: false, - Progress: func(count int, msg string) { - m.log.Info("Migration progress", "count", count, "message", msg) - }, - } - - // Execute the migration via legacy migrator - response, err := m.migrator.Migrate(ctx, opts) - if err != nil { - m.log.Error("Migration failed", "error", err, "duration", time.Since(startTime)) - return fmt.Errorf("failed to migrate resources: %w", err) - } - - // Validate the migration results - if err := m.validateMigration(sess, response); err != nil { - m.log.Error("Migration validation failed", "error", err, "duration", time.Since(startTime)) - return fmt.Errorf("migration validation failed: %w", err) - } - - m.log.Info("Migration completed successfully", - "duration", time.Since(startTime), - "processed", response.Processed, - "summaries", len(response.Summary), - "rejected", len(response.Rejected)) - - return nil +// resourceClientStreamProvider creates streams using resource.ResourceClient +type resourceClientStreamProvider struct { + client resource.ResourceClient } -func (m *unifiedStorageMigrator) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { - // Check for rejected items - if len(response.Rejected) > 0 { - m.log.Warn("Migration had rejected items", "count", len(response.Rejected)) - for i, rejected := range response.Rejected { - if i < 10 { // Log first 10 rejected items - m.log.Warn("Rejected item", - "namespace", rejected.Key.Namespace, - "group", rejected.Key.Group, - "resource", rejected.Key.Resource, - "name", rejected.Key.Name, - "reason", rejected.Error) - } +func (r *resourceClientStreamProvider) createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) { + // Build collection settings for resource client + settings := resource.BulkSettings{ + RebuildCollection: true, + SkipValidation: true, + } + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: folders.GROUP, + Resource: folders.RESOURCE, + }) + case "dashboard.grafana.app/librarypanels": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.LIBRARY_PANEL_RESOURCE, + }) + case "dashboard.grafana.app/dashboards": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.DASHBOARD_RESOURCE, + }) } - // Rejections are not fatal - they may be expected for invalid data + } + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + return r.client.BulkProcess(ctx) +} + +// bulkStoreClientStreamProvider creates streams using resourcepb.BulkStoreClient +type bulkStoreClientStreamProvider struct { + client resourcepb.BulkStoreClient +} + +func (b *bulkStoreClientStreamProvider) createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) { + // Build collection settings for resource client + settings := resource.BulkSettings{ + RebuildCollection: true, + SkipValidation: true, + } + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: folders.GROUP, + Resource: folders.RESOURCE, + }) + case "dashboard.grafana.app/librarypanels": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.LIBRARY_PANEL_RESOURCE, + }) + case "dashboard.grafana.app/dashboards": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.DASHBOARD_RESOURCE, + }) + } + } + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + return b.client.BulkProcess(ctx) +} + +// This can migrate Folders, Dashboards and LibraryPanels +func ProvideUnifiedMigrator( + dashboardAccess legacy.MigrationDashboardAccessor, + client resource.ResourceClient, +) UnifiedMigrator { + return newUnifiedMigrator( + dashboardAccess, + &resourceClientStreamProvider{client: client}, + log.New("storage.unified.migrator"), + ) +} + +func ProvideUnifiedMigratorParquet( + dashboardAccess legacy.MigrationDashboardAccessor, + client resourcepb.BulkStoreClient, +) UnifiedMigrator { + return newUnifiedMigrator( + dashboardAccess, + &bulkStoreClientStreamProvider{client: client}, + log.New("storage.unified.migrator.parquet"), + ) +} + +func newUnifiedMigrator( + dashboardAccess legacy.MigrationDashboardAccessor, + streamProvider streamProvider, + log log.Logger, +) UnifiedMigrator { + return &unifiedMigration{ + MigrationDashboardAccessor: dashboardAccess, + streamProvider: streamProvider, + log: log, + } +} + +// migrate function -- works for a single kind +type migratorFunc = func(ctx context.Context, orgId int64, opts legacy.MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*legacy.BlobStoreInfo, error) + +func (m *unifiedMigration) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { + info, err := authlib.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + if opts.Progress == nil { + opts.Progress = func(count int, msg string) {} // noop } - // Validate counts for each resource type - for _, summary := range response.Summary { - legacyCount, err := m.getLegacyCount(sess, summary.Group, summary.Resource, summary.Namespace) + if len(opts.Resources) < 1 { + return nil, fmt.Errorf("missing resource selector") + } + + if opts.OnlyCount { + return m.CountResources(ctx, opts) + } + + stream, err := m.streamProvider.createStream(ctx, opts) + if err != nil { + return nil, err + } + + migratorFuncs := []migratorFunc{} + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + migratorFuncs = append(migratorFuncs, m.MigrateFolders) + case "dashboard.grafana.app/librarypanels": + migratorFuncs = append(migratorFuncs, m.MigrateLibraryPanels) + case "dashboard.grafana.app/dashboards": + migratorFuncs = append(migratorFuncs, m.MigrateDashboards) + default: + return nil, fmt.Errorf("unsupported resource: %s", res) + } + } + + // Execute migrations + blobStore := legacy.BlobStoreInfo{} + m.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) + for _, fn := range migratorFuncs { + blobs, err := fn(ctx, info.OrgID, opts, stream) if err != nil { - return fmt.Errorf("failed to get legacy count for %s/%s: %w", summary.Group, summary.Resource, err) + m.log.Error("error migrating legacy resources", "error", err, "namespace", opts.Namespace) + return nil, err } - - // Account for rejected items in validation - expectedCount := summary.Count + int64(len(response.Rejected)) - - m.log.Info("Count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace, - "legacy_count", legacyCount, - "unified_count", summary.Count, - "rejected", len(response.Rejected), - "history", summary.History) - - // Validate that we migrated all items (allowing for rejected items) - if legacyCount > expectedCount { - return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", - summary.Resource, summary.Group, summary.Namespace, - legacyCount, summary.Count, len(response.Rejected)) + if blobs != nil { + blobStore.Count += blobs.Count + blobStore.Size += blobs.Size } } - - return nil -} - -func (m *unifiedStorageMigrator) getLegacyCount(sess *xorm.Session, group, resourceType, namespace string) (int64, error) { - // Parse namespace to get org ID - orgID, err := ParseOrgIDFromNamespace(namespace) - if err != nil { - return 0, fmt.Errorf("invalid namespace %s: %w", namespace, err) - } - - // Map group/resource to legacy table - tableName, whereClause := m.getLegacyTableInfo(group, resourceType) - if tableName == "" { - return 0, fmt.Errorf("unknown resource type: %s.%s", resourceType, group) - } - - // Count items in legacy table using Table() before Count() - count, err := sess.Table(tableName).Where(whereClause, orgID).Count() - if err != nil { - return 0, fmt.Errorf("failed to count %s: %w", tableName, err) - } - - return count, nil -} - -func (m *unifiedStorageMigrator) getLegacyTableInfo(group, resource string) (table string, whereClause string) { - // Map unified storage group/resource to legacy tables - switch { - case group == "dashboard.grafana.app" && resource == "dashboards": - return "dashboard", "org_id = ? and is_folder = false" - case group == "folder.grafana.app" && resource == "folders": - return "dashboard", "org_id = ? and is_folder = true" - case group == "playlist.grafana.app" && resource == "playlists": - return "playlist", "org_id = ?" - default: - return "", "" - } -} - -func ParseOrgIDFromNamespace(namespace string) (int64, error) { - // Use authlib to properly parse all namespace formats including "default" for org 1 - info, err := types.ParseNamespace(namespace) - if err != nil { - return 0, fmt.Errorf("failed to parse namespace: %w", err) - } - return info.OrgID, nil + m.log.Info("finished migrating legacy resources", "blobStore", blobStore) + return stream.CloseAndRecv() } diff --git a/pkg/storage/unified/migrations/migrator_mock.go b/pkg/storage/unified/migrations/migrator_mock.go new file mode 100644 index 00000000000..3b460ca6e5b --- /dev/null +++ b/pkg/storage/unified/migrations/migrator_mock.go @@ -0,0 +1,98 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package migrations + +import ( + context "context" + + legacy "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + mock "github.com/stretchr/testify/mock" + + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +// MockUnifiedMigrator is an autogenerated mock type for the UnifiedMigrator type +type MockUnifiedMigrator struct { + mock.Mock +} + +type MockUnifiedMigrator_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUnifiedMigrator) EXPECT() *MockUnifiedMigrator_Expecter { + return &MockUnifiedMigrator_Expecter{mock: &_m.Mock} +} + +// Migrate provides a mock function with given fields: ctx, opts +func (_m *MockUnifiedMigrator) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Migrate") + } + + var r0 *resourcepb.BulkResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, legacy.MigrateOptions) (*resourcepb.BulkResponse, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, legacy.MigrateOptions) *resourcepb.BulkResponse); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.BulkResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, legacy.MigrateOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUnifiedMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate' +type MockUnifiedMigrator_Migrate_Call struct { + *mock.Call +} + +// Migrate is a helper method to define mock.On call +// - ctx context.Context +// - opts legacy.MigrateOptions +func (_e *MockUnifiedMigrator_Expecter) Migrate(ctx interface{}, opts interface{}) *MockUnifiedMigrator_Migrate_Call { + return &MockUnifiedMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, opts)} +} + +func (_c *MockUnifiedMigrator_Migrate_Call) Run(run func(ctx context.Context, opts legacy.MigrateOptions)) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(legacy.MigrateOptions)) + }) + return _c +} + +func (_c *MockUnifiedMigrator_Migrate_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUnifiedMigrator_Migrate_Call) RunAndReturn(run func(context.Context, legacy.MigrateOptions) (*resourcepb.BulkResponse, error)) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUnifiedMigrator creates a new instance of MockUnifiedMigrator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUnifiedMigrator(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUnifiedMigrator { + mock := &MockUnifiedMigrator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go new file mode 100644 index 00000000000..663bf030f67 --- /dev/null +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -0,0 +1,275 @@ +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/xorm" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ValidationFunc is a function that validates migration results. +// It receives the database session, migration response, and logger for reporting. +// Return an error if validation fails, nil if validation passes or is skipped. +type ValidationFunc func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error + +// ResourceMigration handles migration of specific resource types from legacy to unified storage. +// It implements migrator.CodeMigration and provides a generic, extensible way to migrate any +// resource type by: +// +// 1. Iterating through all organizations +// 2. For each org, delegating to LegacyMigrator to read from legacy and write to unified storage +// 3. Validating migration results using the provided validation function (if any) +// +// To add a new resource type migration, simply create a new ResourceMigration instance in +// service.go with the appropriate schema.GroupResource specifications and optional validation function. +type ResourceMigration struct { + migrator.MigrationBase + migrator UnifiedMigrator + resources []schema.GroupResource + migrationID string + validationFunc ValidationFunc // Optional: custom validation logic for this migration + log log.Logger +} + +// NewResourceMigration creates a new migration for the specified resources. +// This is the primary way to register new resource migrations. +// +// Parameters: +// - legacyMigrator: handles reading from legacy storage and writing to unified storage +// - resources: list of GroupResource to migrate +// - migrationID: unique identifier for this migration +// - validationFunc: optional validation function to verify migration results. +// If nil, no validation will be performed. +// +// Example with legacy table count validation: +// +// NewResourceMigration( +// migrator, +// []schema.GroupResource{{Group: "playlist.grafana.app", Resource: "playlists"}}, +// "playlists", +// NewLegacyTableCountValidator(map[string]LegacyTableInfo{ +// "playlist.grafana.app/playlists": {Table: "playlist", WhereClause: "org_id = ?"}, +// }), +// ) +// +// Example without validation: +// +// NewResourceMigration(migrator, resources, "new-resource", nil) +func NewResourceMigration( + migrator UnifiedMigrator, + resources []schema.GroupResource, + migrationID string, + validationFunc ValidationFunc, +) *ResourceMigration { + return &ResourceMigration{ + migrator: migrator, + resources: resources, + migrationID: migrationID, + validationFunc: validationFunc, + log: log.New("storage.unified.resource_migration." + migrationID), + } +} + +var _ migrator.CodeMigration = (*ResourceMigration)(nil) + +// SQL implements migrator.Migration interface. Returns a description string. +func (m *ResourceMigration) SQL(_ migrator.Dialect) string { + return fmt.Sprintf("unified storage data migration: %s", m.migrationID) +} + +// Exec implements migrator.CodeMigration interface. Executes the migration across all organizations. +func (m *ResourceMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + ctx := context.Background() + + orgs, err := m.getAllOrgs(sess) + if err != nil { + m.log.Error("failed to get organizations", "error", err) + return fmt.Errorf("failed to get organizations: %w", err) + } + + if len(orgs) == 0 { + m.log.Info("No organizations found to migrate, skipping migration") + return nil + } + + m.log.Info("Starting migration for all organizations", "org_count", len(orgs), "resources", m.resources) + + for _, org := range orgs { + if err := m.migrateOrg(ctx, sess, org); err != nil { + return err + } + } + + m.log.Info("Migration completed successfully for all organizations", "org_count", len(orgs)) + return nil +} + +// migrateOrg handles migration for a single organization +func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, org orgInfo) error { + namespace := types.OrgNamespaceFormatter(org.ID) + m.log.Info("Migrating organization", "org_id", org.ID, "org_name", org.Name, "namespace", namespace) + + // Create a service identity context for this namespace to authenticate with unified storage + migrationCtx, _ := identity.WithServiceIdentityForSingleNamespace(ctx, namespace) + + startTime := time.Now() + + opts := legacy.MigrateOptions{ + Namespace: namespace, + Resources: m.resources, + WithHistory: true, // Migrate with full history + Progress: func(count int, msg string) { + m.log.Info("Migration progress", "org_id", org.ID, "count", count, "message", msg) + }, + } + + // Execute the migration via legacy migrator + response, err := m.migrator.Migrate(migrationCtx, opts) + if err != nil { + m.log.Error("Migration failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) + } + + // Validate the migration results + if err := m.validateMigration(sess, response); err != nil { + m.log.Error("Migration validation failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("migration validation failed for org %d (%s): %w", org.ID, org.Name, err) + } + + m.log.Info("Migration completed for organization", + "org_id", org.ID, + "duration", time.Since(startTime), + "processed", response.Processed, + "summaries", len(response.Summary), + "rejected", len(response.Rejected)) + + return nil +} + +// validateMigration calls the custom validation function if provided +func (m *ResourceMigration) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { + if m.validationFunc == nil { + m.log.Debug("No validation function provided, skipping validation") + return nil + } + + return m.validationFunc(sess, response, m.log) +} + +// LegacyTableInfo defines how to map a unified storage resource to its legacy table +type LegacyTableInfo struct { + Table string // Legacy table name (e.g., "dashboard", "playlist") + WhereClause string // WHERE clause template with org_id parameter (e.g., "org_id = ? and is_folder = false") +} + +// NewLegacyTableCountValidator creates a ValidationFunc that validates migration by comparing +// counts between legacy tables and unified storage. +// +// This is a helper for the common case of validating that all items from legacy tables +// were successfully migrated to unified storage. +// +// Parameters: +// - legacyTableMap: maps "group/resource" keys to LegacyTableInfo for validation. +// Only resources with mappings will be validated. +// +// Example: +// +// validator := NewLegacyTableCountValidator(map[string]LegacyTableInfo{ +// "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, +// "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, +// }) +func NewLegacyTableCountValidator(legacyTableMap map[string]LegacyTableInfo) ValidationFunc { + return func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + // Check for rejected items + if len(response.Rejected) > 0 { + log.Warn("Migration had rejected items", "count", len(response.Rejected)) + for i, rejected := range response.Rejected { + if i < 10 { // Log first 10 rejected items + log.Warn("Rejected item", + "namespace", rejected.Key.Namespace, + "group", rejected.Key.Group, + "resource", rejected.Key.Resource, + "name", rejected.Key.Name, + "reason", rejected.Error) + } + } + // Rejections are not fatal - they may be expected for invalid data + } + + // Validate counts for each resource type + for _, summary := range response.Summary { + key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) + tableInfo, ok := legacyTableMap[key] + if !ok { + log.Debug("No legacy table mapping for resource, skipping count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace) + continue + } + + // Get legacy count + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() + if err != nil { + return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) + } + + // Account for rejected items in validation + expectedCount := summary.Count + int64(len(response.Rejected)) + + log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) + + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, summary.Count, len(response.Rejected)) + } + } + + return nil + } +} + +func ParseOrgIDFromNamespace(namespace string) (int64, error) { + // Use authlib to properly parse all namespace formats including "default" for org 1 + info, err := types.ParseNamespace(namespace) + if err != nil { + return 0, fmt.Errorf("failed to parse namespace: %w", err) + } + return info.OrgID, nil +} + +// orgInfo represents basic organization information +type orgInfo struct { + ID int64 `xorm:"id"` + Name string `xorm:"name"` +} + +// getAllOrgs retrieves all organizations from the database +func (m *ResourceMigration) getAllOrgs(sess *xorm.Session) ([]orgInfo, error) { + var orgs []orgInfo + err := sess.Table("org").Cols("id", "name").Find(&orgs) + if err != nil { + return nil, err + } + return orgs, nil +} diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go new file mode 100644 index 00000000000..5bcb2fdf8c2 --- /dev/null +++ b/pkg/storage/unified/migrations/service.go @@ -0,0 +1,118 @@ +package migrations + +import ( + "context" + "fmt" + "os" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/log" + sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") +var logger = log.New("storage.unified.migrations") + +type UnifiedStorageMigrationServiceImpl struct { + migrator UnifiedMigrator + cfg *setting.Cfg + sqlStore db.DB + kv kvstore.KVStore +} + +var _ contract.UnifiedStorageMigrationService = (*UnifiedStorageMigrationServiceImpl)(nil) + +// ProvideUnifiedStorageMigrationService is a Wire provider that creates the migration service. +// The service implements registry.BackgroundService and runs migrations during server startup. +func ProvideUnifiedStorageMigrationService( + migrator UnifiedMigrator, + cfg *setting.Cfg, + sqlStore db.DB, + kv kvstore.KVStore, +) contract.UnifiedStorageMigrationService { + return &UnifiedStorageMigrationServiceImpl{ + migrator: migrator, + cfg: cfg, + sqlStore: sqlStore, + kv: kv, + } +} + +// Run executes unified storage migrations as a background service. +// This blocks until migrations complete. If migrations fail, an error is returned +// which will prevent Grafana from starting. +func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { + // TODO: temporary skip migrations in test environments to prevent integration test timeouts. + if os.Getenv("GRAFANA_TEST_DB") != "" { + return nil + } + + // skip migrations if disabled in config + if p.cfg.DisableDataMigrations { + logger.Info("Data migrations are disabled, skipping") + return nil + } + + // TODO: Re-enable once migrations are ready + // TODO: add guarantee that this only runs once + // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore) + return nil +} + +// RegisterMigrations initializes and registers all unified storage migrations. +// This function is the entry point for all data migrations from legacy storage +// to unified storage. It returns an error if migrations fail, preventing Grafana +// from starting with inconsistent data. +func RegisterMigrations( + migrator UnifiedMigrator, + cfg *setting.Cfg, + sqlStore db.DB, +) error { + ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") + defer span.End() + mg := sqlstoremigrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unifiedstorage") + mg.AddCreateMigration() + + if err := prometheus.Register(mg); err != nil { + logger.Warn("Failed to register migrator metrics", "error", err) + } + + // Register resource migrations + // To add a new resource type, simply add another migration here with the appropriate resources + registerResourceMigrations(mg, migrator) + + // Run all registered migrations (blocking) + sec := cfg.Raw.Section("database") + if err := mg.RunMigrations(ctx, + sec.Key("migration_locking").MustBool(true), + sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { + return fmt.Errorf("unified storage data migration failed: %w", err) + } + + logger.Info("Unified storage migrations completed successfully") + return nil +} + +// registerResourceMigrations registers all unified storage resource migrations. +// Add new resource types here by creating additional ResourceMigration instances. +func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator) { + dashboardsAndFolders := NewResourceMigration( + migrator, + []schema.GroupResource{ + {Group: "folder.grafana.app", Resource: "folders"}, + {Group: "dashboard.grafana.app", Resource: "dashboards"}, + }, + "folders-dashboards", + NewLegacyTableCountValidator(map[string]LegacyTableInfo{ + "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, + "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, + }), + ) + mg.AddMigration("folders and dashboards migration", dashboardsAndFolders) +} From 534ed3421b75505c6a6f376e18c062a7b34833d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 20 Nov 2025 17:09:49 +0100 Subject: [PATCH 012/423] Fix search by both tags and folders. (#114246) * Fix search by both tags and folders. * Move // nolint:gocyclo to the new method. * Revert unnecessary change. --- pkg/registry/apis/dashboard/search.go | 118 ++--- pkg/registry/apis/dashboard/search_test.go | 491 +++++++++++++++++---- 2 files changed, 470 insertions(+), 139 deletions(-) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index ec2f6053b95..0f1b83a7e18 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -3,6 +3,7 @@ package dashboard import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -272,7 +273,8 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) { const rootFolder = "general" -// nolint:gocyclo +var errEmptyResults = fmt.Errorf("empty results") + func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -289,6 +291,51 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } + searchRequest, err := convertHttpSearchRequestToResourceSearchRequest(queryParams, user, func() ([]string, error) { + return s.getDashboardsUIDsSharedWithUser(ctx, user) + }) + if err != nil { + if errors.Is(err, errEmptyResults) { + s.write(w, dashboardv0alpha1.SearchResults{ + Hits: []dashboardv0alpha1.DashboardHit{}, + }) + } else { + errhttp.Write(ctx, err, w) + } + return + } + + result, err := s.client.Search(ctx, searchRequest) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + if result != nil { + s.log.Debug("search result hits and cost", "total_hits", result.TotalHits, "query_cost", result.QueryCost) + } + + parsedResults, err := dashboardsearch.ParseResults(result, searchRequest.Offset) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + if len(searchRequest.SortBy) == 0 { + // default sort by resource descending ( folders then dashboards ) then title + sort.Slice(parsedResults.Hits, func(i, j int) bool { + // Just sorting by resource for now. The rest should be sorted by search score already + return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource + }) + } + + s.write(w, parsedResults) +} + +// convertHttpSearchRequestToResourceSearchRequest create ResourceSearchRequest from query parameters. +// Supplied function is used to get dashboards shared with user. +// nolint:gocyclo +func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, user identity.Requester, getDashboardsUIDsSharedWithUser func() ([]string, error)) (*resourcepb.ResourceSearchRequest, error) { // get limit and offset from query params limit := 50 offset := 0 @@ -339,6 +386,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { hasDash := len(types) == 0 || slices.Contains(types, "dashboard") hasFolder := len(types) == 0 || slices.Contains(types, "folder") // If both types are present, we need to search both dashboards and folders, by default is nothing is set we also search both. + var err error if (hasDash && hasFolder) || (!hasDash && !hasFolder) { searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE) if err == nil { @@ -352,8 +400,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE) } if err != nil { - errhttp.Write(ctx, err, w) - return + return nil, err } // Add sorting @@ -384,20 +431,20 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { // The tags filter if tags, ok := queryParams["tag"]; ok { - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: "tags", Operator: "=", Values: tags, - }} + }) } // The libraryPanel filter if libraryPanel, ok := queryParams["libraryPanel"]; ok { - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: libraryPanel, - }} + }) } // The names filter @@ -406,17 +453,13 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { // Add the folder constraint. Note this does not do recursive search folder := queryParams.Get("folder") if folder == foldermodel.SharedWithMeFolderUID { - dashboardUIDs, err := s.getDashboardsUIDsSharedWithUser(ctx, user) + dashboardUIDs, err := getDashboardsUIDsSharedWithUser() if err != nil { - errhttp.Write(ctx, err, w) - return + return nil, err } if len(dashboardUIDs) == 0 { - s.write(w, dashboardv0alpha1.SearchResults{ - Hits: []dashboardv0alpha1.DashboardHit{}, - }) - return + return nil, errEmptyResults } // hijacks the "name" query param to only search for shared dashboard UIDs names = append(names, dashboardUIDs...) @@ -424,50 +467,21 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { if folder == rootFolder { folder = "" // root folder is empty in the search index } - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: "folder", Operator: "=", Values: []string{folder}, - }} - } - - if len(names) > 0 { - if searchRequest.Options.Fields == nil { - searchRequest.Options.Fields = []*resourcepb.Requirement{} - } - namesFilter := []*resourcepb.Requirement{{ - Key: "name", - Operator: "in", - Values: names, - }} - searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) - } - - result, err := s.client.Search(ctx, searchRequest) - if err != nil { - errhttp.Write(ctx, err, w) - return - } - - if result != nil { - s.log.Debug("search result hits and cost", "total_hits", result.TotalHits, "query_cost", result.QueryCost) - } - - parsedResults, err := dashboardsearch.ParseResults(result, searchRequest.Offset) - if err != nil { - errhttp.Write(ctx, err, w) - return - } - - if len(searchRequest.SortBy) == 0 { - // default sort by resource descending ( folders then dashboards ) then title - sort.Slice(parsedResults.Hits, func(i, j int) bool { - // Just sorting by resource for now. The rest should be sorted by search score already - return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource }) } - s.write(w, parsedResults) + if len(names) > 0 { + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ + Key: "name", + Operator: "in", + Values: names, + }) + } + return searchRequest, nil } func (s *SearchHandler) write(w http.ResponseWriter, obj any) { diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index b392cb1606c..ab7f05e7d00 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http/httptest" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -282,93 +284,6 @@ func TestSearchHandlerPagination(t *testing.T) { } func TestSearchHandler(t *testing.T) { - t.Run("Multiple comma separated fields will be appended to default dashboard search fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search?field=field1&field=field2&field=field3", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id", "field1", "field2", "field3"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - - t.Run("Single field will be appended to default dashboard search fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search?field=field1", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id", "field1"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - - t.Run("Passing no fields will search using default dashboard fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - t.Run("Sort - default sort by resource", func(t *testing.T) { rows := make([]*resourcepb.ResourceTableRow, len(mockResults)) for i, r := range mockResults { @@ -689,6 +604,408 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { }) } +func TestConvertHttpSearchRequestToResourceSearchRequest(t *testing.T) { + testUser := &user.SignedInUser{ + Namespace: "test-namespace", + OrgID: 1, + } + + dashboardKey := &resourcepb.ResourceKey{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Namespace: "test-namespace", + } + folderKey := &resourcepb.ResourceKey{ + Group: "folder.grafana.app", + Resource: "folders", + Namespace: "test-namespace", + } + defaultFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id"} + + tests := map[string]struct { + queryString string + sharedDashboards []string + sharedDashboardsError error + expected *resourcepb.ResourceSearchRequest + expectedError error + }{ + "default values with no query params": { + queryString: "", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "custom limit and offset": { + queryString: "limit=100&offset=50", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 100, + Offset: 50, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "pagination with page parameter": { + queryString: "limit=25&page=3", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 25, + Offset: 50, + Page: 3, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "query string and explain": { + queryString: "query=test-query&explain=true", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "test-query", + Limit: 50, + Offset: 0, + Page: 1, + Explain: true, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "additional fields": { + queryString: "field=custom1&field=custom2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: append(defaultFields, "custom1", "custom2"), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "view permission": { + queryString: "permission=view", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_VIEW), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "edit permission": { + queryString: "permission=Edit", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_EDIT), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "admin permission": { + queryString: "permission=ADMIN", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_ADMIN), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "type dashboard only": { + queryString: "type=dashboard", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + }, + }, + "type folder only": { + queryString: "type=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: folderKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + }, + }, + "both types should include federated": { + queryString: "type=dashboard&type=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "sort ascending": { + queryString: "sort=title", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + SortBy: []*resourcepb.ResourceSearchRequest_Sort{{Field: "title", Desc: false}}, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "sort descending": { + queryString: "sort=-title", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + SortBy: []*resourcepb.ResourceSearchRequest_Sort{{Field: "title", Desc: true}}, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "facet fields": { + queryString: "facet=tags&facet=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ + "tags": {Field: "tags", Limit: 50}, + "folder": {Field: "folder", Limit: 50}, + }, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "tag filter": { + queryString: "tag=tag1&tag=tag2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "tags", Operator: "=", Values: []string{"tag1", "tag2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "folder filter": { + queryString: "folder=my-folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "folder", Operator: "=", Values: []string{"my-folder"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "tag and folder filter together": { + queryString: "tag=tag1&tag=tag2&folder=my-folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"tag1", "tag2"}}, + {Key: "folder", Operator: "=", Values: []string{"my-folder"}}, + }, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "root folder should be converted to empty string": { + queryString: "folder=general", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "folder", Operator: "=", Values: []string{""}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "shared with me folder with dashboards": { + queryString: "folder=sharedwithme", + sharedDashboards: []string{"dash1", "dash2", "dash3"}, + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "name", Operator: "in", Values: []string{"dash1", "dash2", "dash3"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "shared with me folder without dashboards returns error": { + queryString: "folder=sharedwithme", + sharedDashboards: []string{}, + expectedError: errEmptyResults, + }, + "name filter": { + queryString: "name=name1&name=name2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "name", Operator: "in", Values: []string{"name1", "name2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "comprehensive filter with query, tags, folder, and name": { + queryString: "query=search-term&tag=monitoring&tag=prod&folder=my-folder&name=dash1&name=dash2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"monitoring", "prod"}}, + {Key: "folder", Operator: "=", Values: []string{"my-folder"}}, + {Key: "name", Operator: "in", Values: []string{"dash1", "dash2"}}, + }, + }, + Query: "search-term", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "libraryPanel filter": { + queryString: "libraryPanel=panel1&libraryPanel=panel2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "reference.LibraryPanel", Operator: "=", Values: []string{"panel1", "panel2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "libraryPanel and tag filter together": { + queryString: "libraryPanel=panel1&tag=monitoring&tag=prod", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"monitoring", "prod"}}, + {Key: "reference.LibraryPanel", Operator: "=", Values: []string{"panel1"}}, + }, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + queryParams, err := url.ParseQuery(tt.queryString) + require.NoError(t, err) + + getDashboardsFunc := func() ([]string, error) { + if tt.sharedDashboardsError != nil { + return nil, tt.sharedDashboardsError + } + return tt.sharedDashboards, nil + } + + result, err := convertHttpSearchRequestToResourceSearchRequest(queryParams, testUser, getDashboardsFunc) + + if tt.expectedError != nil { + assert.ErrorIs(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, tt.expected, result) + }) + } +} + // MockClient implements the ResourceIndexClient interface for testing type MockClient struct { resourcepb.ResourceIndexClient From 4d25381716585a2a9b2d6bfa8f904bef22bf9421 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 20 Nov 2025 13:51:59 -0300 Subject: [PATCH 013/423] DashboardLibrary: Template dashboards public preview (#114230) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 12 +- pkg/services/featuremgmt/registry.go | 18 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 12 +- pkg/services/featuremgmt/toggles_gen.json | 260 ++++++++++-------- 6 files changed, 172 insertions(+), 133 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 6b76c32fd28..a4eca4f9ee7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -90,6 +90,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. | | `sqlExpressions` | Enables SQL Expressions, which can execute SQL queries against data source results. | | `queryLibrary` | Enables Saved queries (query library) feature | +| `dashboardTemplates` | Enables a flow to get started with a new dashboard from a template | | `enableSCIM` | Enables SCIM support for user and group management | | `alertRuleRestore` | Enables the alert rule restore feature | | `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index acb3b1796de..a7d2378f765 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -502,14 +502,18 @@ export interface FeatureToggles { */ queryLibrary?: boolean; /** - * Enable dashboard library experiments that are production ready + * Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page */ dashboardLibrary?: boolean; /** - * Enable suggested dashboards when creating new dashboards + * Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page */ suggestedDashboards?: boolean; /** + * Enables a flow to get started with a new dashboard from a template + */ + dashboardTemplates?: boolean; + /** * Sets the logs table as default visualisation in logs explore */ logsExploreTableDefaultVisualization?: boolean; @@ -1157,10 +1161,6 @@ export interface FeatureToggles { */ panelTimeSettings?: boolean; /** - * Enable template dashboards - */ - dashboardTemplates?: boolean; - /** * Enables app platform API for annotations * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e6587428f18..c56f46a7193 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -825,18 +825,25 @@ var ( }, { Name: "dashboardLibrary", - Description: "Enable dashboard library experiments that are production ready", + Description: "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, FrontendOnly: false, }, { Name: "suggestedDashboards", - Description: "Enable suggested dashboards when creating new dashboards", + Description: "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, FrontendOnly: false, }, + { + Name: "dashboardTemplates", + Description: "Enables a flow to get started with a new dashboard from a template", + Stage: FeatureStagePublicPreview, + Owner: grafanaSharingSquad, + FrontendOnly: false, + }, { Name: "logsExploreTableDefaultVisualization", Description: "Sets the logs table as default visualisation in logs explore", @@ -1904,13 +1911,6 @@ var ( RequiresRestart: false, HideFromDocs: false, }, - { - Name: "dashboardTemplates", - Description: "Enable template dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaSharingSquad, - FrontendOnly: false, - }, { Name: "kubernetesAnnotations", Description: "Enables app platform API for annotations", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 24f9769ea4f..9465d6992c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -115,6 +115,7 @@ grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,fa queryLibrary,preview,@grafana/sharing-squad,false,false,false dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false +dashboardTemplates,preview,@grafana/sharing-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false @@ -258,7 +259,6 @@ pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,f newPanelPadding,experimental,@grafana/dashboards-squad,false,false,false onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false -dashboardTemplates,experimental,@grafana/sharing-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index fdc48861894..059ed11e535 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -348,13 +348,17 @@ const ( FlagQueryLibrary = "queryLibrary" // FlagDashboardLibrary - // Enable dashboard library experiments that are production ready + // Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page FlagDashboardLibrary = "dashboardLibrary" // FlagSuggestedDashboards - // Enable suggested dashboards when creating new dashboards + // Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page FlagSuggestedDashboards = "suggestedDashboards" + // FlagDashboardTemplates + // Enables a flow to get started with a new dashboard from a template + FlagDashboardTemplates = "dashboardTemplates" + // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" @@ -750,10 +754,6 @@ const ( // Enables a new panel time settings drawer FlagPanelTimeSettings = "panelTimeSettings" - // FlagDashboardTemplates - // Enable template dashboards - FlagDashboardTemplates = "dashboardTemplates" - // FlagKubernetesAnnotations // Enables app platform API for annotations FlagKubernetesAnnotations = "kubernetesAnnotations" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 48eba662c0a..2298b91d854 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -8,7 +8,7 @@ "name": "addFieldFromCalculationStatFunctions", "resourceVersion": "1762442825881", "creationTimestamp": "2023-11-03T14:39:58Z", - "deletionTimestamp": "2025-11-12T14:07:50Z", + "deletionTimestamp": "2025-11-17T15:58:43Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -26,7 +26,7 @@ "name": "adhocFiltersInTooltips", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-29T17:53:43Z", - "deletionTimestamp": "2025-11-11T09:49:48Z", + "deletionTimestamp": "2025-11-12T10:05:30Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -70,7 +70,8 @@ "metadata": { "name": "alertEnrichmentConditional", "resourceVersion": "1757418974334", - "creationTimestamp": "2025-09-09T11:56:14Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable conditional alert enrichment steps.", @@ -84,7 +85,8 @@ "metadata": { "name": "alertEnrichmentMultiStep", "resourceVersion": "1757418224384", - "creationTimestamp": "2025-09-09T11:43:44Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Allow multiple steps per enrichment.", @@ -249,7 +251,8 @@ "metadata": { "name": "alertingEnrichmentAssistantInvestigations", "resourceVersion": "1757606567075", - "creationTimestamp": "2025-09-11T16:02:47Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable Assistant Investigations enrichment type.", @@ -278,7 +281,8 @@ "metadata": { "name": "alertingEnrichmentPerRule", "resourceVersion": "1756206837948", - "creationTimestamp": "2025-08-28T08:30:28Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable enrichment per rule in the alerting UI.", @@ -319,7 +323,8 @@ "metadata": { "name": "alertingImportAlertmanagerUI", "resourceVersion": "1754585847887", - "creationTimestamp": "2025-08-13T15:28:43Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the UI to see imported Alertmanager configuration", @@ -572,7 +577,8 @@ "metadata": { "name": "alertingTriage", "resourceVersion": "1763541314825", - "creationTimestamp": "2025-09-01T09:33:33Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" } @@ -603,7 +609,7 @@ "metadata": { "name": "alertingUIUseBackendFilters", "resourceVersion": "1762966218072", - "creationTimestamp": "2025-11-12T16:50:18Z" + "creationTimestamp": "2025-11-13T14:52:14Z" }, "spec": { "description": "Enables the UI to use certain backend-side filters", @@ -616,8 +622,8 @@ "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", "resourceVersion": "1759339813575", - "creationTimestamp": "2025-10-01T17:28:42Z", - "deletionTimestamp": "2025-10-01T17:29:29Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-01 17:30:13.575464 +0000 UTC" } @@ -759,7 +765,7 @@ "metadata": { "name": "awsDatasourcesHttpProxy", "resourceVersion": "1762964349996", - "creationTimestamp": "2025-11-12T16:15:47Z", + "creationTimestamp": "2025-11-12T18:51:23Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 16:19:09.996919 +0000 UTC" } @@ -840,7 +846,8 @@ "metadata": { "name": "azureResourcePickerUpdates", "resourceVersion": "1754910058337", - "creationTimestamp": "2025-09-02T10:02:01Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the updated Azure Monitor resource picker", @@ -893,7 +900,8 @@ "metadata": { "name": "cdnPluginsLoadFirst", "resourceVersion": "1758882341746", - "creationTimestamp": "2025-09-26T10:25:41Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Prioritize loading plugins from the CDN before other sources", @@ -920,7 +928,8 @@ "metadata": { "name": "cdnPluginsUrls", "resourceVersion": "1759489886228", - "creationTimestamp": "2025-10-03T11:11:26Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable loading plugins via declarative URLs", @@ -1013,7 +1022,7 @@ "name": "correlations", "resourceVersion": "1762442825881", "creationTimestamp": "2022-09-16T13:14:27Z", - "deletionTimestamp": "2025-11-12T13:11:31Z", + "deletionTimestamp": "2025-11-13T09:21:46Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1067,7 +1076,7 @@ "name": "dashboardDsAdHocFiltering", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-23T08:12:25Z", - "deletionTimestamp": "2025-09-27T19:59:33Z", + "deletionTimestamp": "2025-11-10T17:17:49Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -1096,14 +1105,15 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1762521182817", - "creationTimestamp": "2025-09-26T16:02:12Z", + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 13:13:02.817210943 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" } }, "spec": { - "description": "Enable dashboard library experiments that are production ready", + "description": "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", "stage": "experimental", "codeowner": "@grafana/sharing-squad" } @@ -1180,12 +1190,15 @@ { "metadata": { "name": "dashboardTemplates", - "resourceVersion": "1761575312733", - "creationTimestamp": "2025-10-27T14:28:32Z" + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-10-28T20:05:32Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" + } }, "spec": { - "description": "Enable template dashboards", - "stage": "experimental", + "description": "Enables a flow to get started with a new dashboard from a template", + "stage": "preview", "codeowner": "@grafana/sharing-squad" } }, @@ -1193,7 +1206,8 @@ "metadata": { "name": "dashboardUndoRedo", "resourceVersion": "1757940426210", - "creationTimestamp": "2025-09-15T12:47:06Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enables undo/redo in dynamic dashboards", @@ -1341,8 +1355,8 @@ "metadata": { "name": "dskitBackgroundServices", "resourceVersion": "1757339637779", - "creationTimestamp": "2025-09-03T12:20:24Z", - "deletionTimestamp": "2025-09-17T12:19:32Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-08 13:53:57.77994 +0000 UTC" } @@ -1361,7 +1375,7 @@ "name": "editPanelCSVDragAndDrop", "resourceVersion": "1762783224740", "creationTimestamp": "2023-01-24T09:43:44Z", - "deletionTimestamp": "2025-11-10T16:31:10Z", + "deletionTimestamp": "2025-11-12T14:47:44Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-10 14:00:24.740459 +0000 UTC" } @@ -1420,7 +1434,7 @@ "metadata": { "name": "enableDashboardEmptyExtensions", "resourceVersion": "1759194774156", - "creationTimestamp": "2025-09-30T01:12:54Z" + "creationTimestamp": "2025-10-13T07:03:13Z" }, "spec": { "description": "Set this to true to enable all dashboard empty state extensions registered by plugins.", @@ -1477,7 +1491,7 @@ "name": "enablePluginImporter", "resourceVersion": "1753448760331", "creationTimestamp": "2025-07-16T04:42:28Z", - "deletionTimestamp": "2025-10-20T05:42:33Z" + "deletionTimestamp": "2025-10-23T04:18:23Z" }, "spec": { "description": "Set this to true to use the new PluginImporter functionality", @@ -1570,7 +1584,7 @@ "name": "expressionParser", "resourceVersion": "1753448760331", "creationTimestamp": "2024-02-17T00:59:11Z", - "deletionTimestamp": "2025-08-26T13:21:24Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enable new expression parser", @@ -1584,7 +1598,7 @@ "name": "extensionSidebar", "resourceVersion": "1753448760331", "creationTimestamp": "2025-04-03T10:16:35Z", - "deletionTimestamp": "2025-09-01T10:14:17Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables the extension sidebar", @@ -1624,7 +1638,7 @@ "name": "extractFieldsNameDeduplication", "resourceVersion": "1762442825881", "creationTimestamp": "2023-11-02T15:47:42Z", - "deletionTimestamp": "2025-11-11T16:00:57Z", + "deletionTimestamp": "2025-11-12T10:08:13Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1653,7 +1667,8 @@ "metadata": { "name": "favoriteDatasources", "resourceVersion": "1754648387873", - "creationTimestamp": "2025-08-08T13:28:17Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable favorite datasources", @@ -1680,7 +1695,7 @@ "name": "featureToggleAdminPage", "resourceVersion": "1758022099771", "creationTimestamp": "2023-07-18T20:43:32Z", - "deletionTimestamp": "2025-09-29T13:36:16Z", + "deletionTimestamp": "2025-08-29T14:46:39Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-16 11:28:19.771156 +0000 UTC" } @@ -1724,8 +1739,8 @@ "metadata": { "name": "filterOutBotsFromFrontendLogs", "resourceVersion": "1758000919535", - "creationTimestamp": "2025-09-16T05:35:19Z", - "deletionTimestamp": "2025-10-13T11:09:22Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Filter out bots from collecting data for Frontend Observability", @@ -1755,7 +1770,7 @@ "name": "formatString", "resourceVersion": "1762442825881", "creationTimestamp": "2023-10-13T18:17:12Z", - "deletionTimestamp": "2025-11-12T00:00:00Z", + "deletionTimestamp": "2025-11-17T13:06:30Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1812,7 +1827,8 @@ "metadata": { "name": "grafanaAssistantInProfilesDrilldown", "resourceVersion": "1754572610001", - "creationTimestamp": "2025-08-19T07:54:00Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-08-07 13:16:50.001205 +0000 UTC" } @@ -1857,8 +1873,8 @@ "metadata": { "name": "grafanaPathfinder", "resourceVersion": "1760434668782", - "creationTimestamp": "2025-08-20T12:30:29Z", - "deletionTimestamp": "2025-10-21T16:10:16Z", + "creationTimestamp": "2025-10-14T10:40:40Z", + "deletionTimestamp": "2025-10-22T08:06:21Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-14 09:37:48.782577 +0000 UTC" } @@ -1888,7 +1904,8 @@ "metadata": { "name": "graphiteBackendMode", "resourceVersion": "1755870507537", - "creationTimestamp": "2025-09-01T15:13:47Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the Graphite data source full backend mode", @@ -1928,7 +1945,7 @@ "name": "groupToNestedTableTransformation", "resourceVersion": "1762442825881", "creationTimestamp": "2024-02-07T14:28:26Z", - "deletionTimestamp": "2025-11-12T14:33:44Z", + "deletionTimestamp": "2025-11-17T11:57:22Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -2046,7 +2063,7 @@ "metadata": { "name": "interactiveLearning", "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-21T16:10:16Z" + "creationTimestamp": "2025-10-22T08:06:21Z" }, "spec": { "description": "Enables the interactive learning app", @@ -2072,7 +2089,7 @@ "name": "inviteUserExperimental", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-07T19:09:59Z", - "deletionTimestamp": "2025-11-14T10:29:28Z" + "deletionTimestamp": "2025-11-14T15:33:26Z" }, "spec": { "description": "Renders invite user button along the app", @@ -2086,7 +2103,7 @@ "metadata": { "name": "jaegerEnableGrpcEndpoint", "resourceVersion": "1760451551713", - "creationTimestamp": "2025-10-14T14:19:11Z" + "creationTimestamp": "2025-10-31T18:19:16Z" }, "spec": { "description": "Enable querying trace data through Jaeger's gRPC endpoint (HTTP)", @@ -2164,7 +2181,8 @@ "metadata": { "name": "kubernetesAlertingRules", "resourceVersion": "1754340669702", - "creationTimestamp": "2025-08-04T20:51:09Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Adds support for Kubernetes alerting and recording rules", @@ -2177,7 +2195,7 @@ "metadata": { "name": "kubernetesAnnotations", "resourceVersion": "1761142826172", - "creationTimestamp": "2025-10-22T14:20:26Z" + "creationTimestamp": "2025-11-06T18:22:20Z" }, "spec": { "description": "Enables app platform API for annotations", @@ -2190,7 +2208,8 @@ "metadata": { "name": "kubernetesAuthZHandlerRedirect", "resourceVersion": "1758820248165", - "creationTimestamp": "2025-09-25T17:10:48Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", @@ -2246,7 +2265,8 @@ "metadata": { "name": "kubernetesAuthzResourcePermissionApis", "resourceVersion": "1754668670559", - "creationTimestamp": "2025-08-11T08:54:36Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Registers AuthZ resource permission /apis endpoints", @@ -2259,7 +2279,7 @@ "metadata": { "name": "kubernetesAuthzZanzanaSync", "resourceVersion": "1758887751768", - "creationTimestamp": "2025-09-26T09:35:02Z", + "creationTimestamp": "2025-10-13T19:37:13Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC" } @@ -2275,7 +2295,8 @@ "metadata": { "name": "kubernetesCorrelations", "resourceVersion": "1757513374180", - "creationTimestamp": "2025-09-10T14:09:34Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Adds support for Kubernetes correlations", @@ -2330,7 +2351,7 @@ "metadata": { "name": "kubernetesLogsDrilldown", "resourceVersion": "1760632282014", - "creationTimestamp": "2025-10-07T19:26:08Z", + "creationTimestamp": "2025-10-16T21:31:42Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-16 16:31:22.014483 +0000 UTC" } @@ -2346,7 +2367,7 @@ "metadata": { "name": "kubernetesQueryCaching", "resourceVersion": "1760972620939", - "creationTimestamp": "2025-10-20T15:03:40Z" + "creationTimestamp": "2025-10-20T16:11:25Z" }, "spec": { "description": "Adds support for Kubernetes querycaching", @@ -2359,7 +2380,8 @@ "metadata": { "name": "kubernetesShortURLs", "resourceVersion": "1756914263808", - "creationTimestamp": "2025-08-04T12:12:12Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" } @@ -2388,7 +2410,8 @@ "metadata": { "name": "kubernetesStars", "resourceVersion": "1759149842036", - "creationTimestamp": "2025-09-29T12:44:02Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Routes stars requests from /api to the /apis endpoint", @@ -2428,7 +2451,7 @@ "name": "localizationForPlugins", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-31T04:38:38Z", - "deletionTimestamp": "2025-09-29T07:10:59Z" + "deletionTimestamp": "2025-08-29T14:46:39Z" }, "spec": { "description": "Enables localization for plugins", @@ -2665,7 +2688,7 @@ "name": "multiTenantFrontend", "resourceVersion": "1753448760331", "creationTimestamp": "2025-04-25T09:24:25Z", - "deletionTimestamp": "2025-09-08T17:06:39Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Register MT frontend", @@ -2702,7 +2725,8 @@ "metadata": { "name": "newClickhouseConfigPageDesign", "resourceVersion": "1754075145003", - "creationTimestamp": "2025-08-05T13:37:28Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables new design for the Clickhouse data source configuration page", @@ -2716,7 +2740,7 @@ "name": "newDashboardSharingComponent", "resourceVersion": "1753448760331", "creationTimestamp": "2024-05-03T15:02:18Z", - "deletionTimestamp": "2025-09-12T17:27:39Z" + "deletionTimestamp": "2025-08-29T14:46:39Z" }, "spec": { "description": "Enables the new sharing drawer design", @@ -2756,7 +2780,7 @@ "metadata": { "name": "newGauge", "resourceVersion": "1760700645318", - "creationTimestamp": "2025-10-17T11:30:45Z" + "creationTimestamp": "2025-10-20T16:33:19Z" }, "spec": { "description": "Enable new gauge visualization", @@ -2783,7 +2807,7 @@ "metadata": { "name": "newLogContext", "resourceVersion": "1754044501326", - "creationTimestamp": "2025-08-01T11:30:17Z" + "creationTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "New Log Context component", @@ -2827,7 +2851,7 @@ "metadata": { "name": "newPanelPadding", "resourceVersion": "1760780310038", - "creationTimestamp": "2025-10-18T09:38:30Z" + "creationTimestamp": "2025-11-12T15:40:46Z" }, "spec": { "description": "Increases panel padding globally", @@ -2853,7 +2877,7 @@ "metadata": { "name": "newVizSuggestions", "resourceVersion": "1762456851857", - "creationTimestamp": "2025-11-06T19:20:51Z" + "creationTimestamp": "2025-11-12T19:26:29Z" }, "spec": { "description": "Enable new visualization suggestions", @@ -2893,7 +2917,7 @@ "metadata": { "name": "onlyStoreActionSets", "resourceVersion": "1759844046154", - "creationTimestamp": "2025-10-07T13:24:26Z", + "creationTimestamp": "2025-10-20T15:02:56Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-07 13:34:06.15476 +0000 UTC" } @@ -2938,7 +2962,7 @@ "name": "panelMonitoring", "resourceVersion": "1753448760331", "creationTimestamp": "2023-10-09T05:19:08Z", - "deletionTimestamp": "2025-11-06T15:46:51Z" + "deletionTimestamp": "2025-11-07T19:04:42Z" }, "spec": { "description": "Enables panel monitoring through logs and measurements", @@ -2966,7 +2990,7 @@ "metadata": { "name": "panelTimeSettings", "resourceVersion": "1761555646368", - "creationTimestamp": "2025-10-27T09:00:46Z" + "creationTimestamp": "2025-10-29T08:06:23Z" }, "spec": { "description": "Enables a new panel time settings drawer", @@ -3028,7 +3052,7 @@ "name": "pinNavItems", "resourceVersion": "1762958248290", "creationTimestamp": "2024-06-10T11:40:03Z", - "deletionTimestamp": "2025-11-13T14:52:10Z", + "deletionTimestamp": "2025-11-17T12:12:47Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" } @@ -3058,7 +3082,7 @@ "name": "pluginAssetProvider", "resourceVersion": "1753448760331", "creationTimestamp": "2025-07-17T15:20:35Z", - "deletionTimestamp": "2025-10-03T16:13:14Z" + "deletionTimestamp": "2025-10-10T09:35:22Z" }, "spec": { "description": "Allows decoupled core plugins to load from the Grafana CDN", @@ -3073,7 +3097,8 @@ "metadata": { "name": "pluginContainers", "resourceVersion": "1756911074581", - "creationTimestamp": "2025-09-03T14:51:14Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables running plugins in containers", @@ -3087,7 +3112,7 @@ "metadata": { "name": "pluginInstallAPISync", "resourceVersion": "1760543624249", - "creationTimestamp": "2025-10-15T15:53:44Z" + "creationTimestamp": "2025-10-24T12:09:26Z" }, "spec": { "description": "Enable syncing plugin installations to the installs API", @@ -3113,7 +3138,7 @@ "metadata": { "name": "pluginStoreServiceLoading", "resourceVersion": "1761144346944", - "creationTimestamp": "2025-10-17T14:52:48Z", + "creationTimestamp": "2025-10-17T20:01:43Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-22 14:45:46.944669 +0000 UTC" } @@ -3142,7 +3167,7 @@ "name": "pluginsFrontendSandbox", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-05T08:51:36Z", - "deletionTimestamp": "2025-11-05T10:06:22Z" + "deletionTimestamp": "2025-11-06T10:06:53Z" }, "spec": { "description": "Enables the plugins frontend sandbox", @@ -3155,7 +3180,7 @@ "name": "pluginsSkipHostEnvVars", "resourceVersion": "1753448760331", "creationTimestamp": "2023-11-15T17:09:14Z", - "deletionTimestamp": "2025-11-04T16:51:13Z" + "deletionTimestamp": "2025-11-13T15:31:57Z" }, "spec": { "description": "Disables passing host environment variable to plugin processes", @@ -3207,7 +3232,7 @@ "name": "preinstallAutoUpdate", "resourceVersion": "1753448760331", "creationTimestamp": "2024-11-07T12:14:25Z", - "deletionTimestamp": "2025-11-07T10:10:01Z" + "deletionTimestamp": "2025-11-10T14:06:30Z" }, "spec": { "description": "Enables automatic updates for pre-installed plugins", @@ -3233,7 +3258,7 @@ "metadata": { "name": "preventPanelChromeOverflow", "resourceVersion": "1760704390127", - "creationTimestamp": "2025-10-17T12:33:10Z" + "creationTimestamp": "2025-10-17T14:40:08Z" }, "spec": { "description": "Restrict PanelChrome contents with overflow: hidden;", @@ -3248,7 +3273,7 @@ "name": "promQLScope", "resourceVersion": "1753448760331", "creationTimestamp": "2024-01-29T20:22:17Z", - "deletionTimestamp": "2025-10-05T00:24:17Z" + "deletionTimestamp": "2025-10-10T14:53:18Z" }, "spec": { "description": "In-development feature that will allow injection of labels into prometheus queries.", @@ -3277,7 +3302,7 @@ "name": "prometheusCodeModeMetricNamesSearch", "resourceVersion": "1753448760331", "creationTimestamp": "2024-04-04T20:38:23Z", - "deletionTimestamp": "2025-08-27T13:11:58Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names", @@ -3303,7 +3328,8 @@ "metadata": { "name": "prometheusTypeMigration", "resourceVersion": "1757089774247", - "creationTimestamp": "2025-08-25T21:53:16Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-05 16:29:34.247055837 +0000 UTC" } @@ -3360,7 +3386,8 @@ "metadata": { "name": "queryCacheRequestDeduplication", "resourceVersion": "1757521912495", - "creationTimestamp": "2025-09-10T16:31:52Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable request deduplication when query caching is enabled. Requests issuing the same query will be deduplicated, only the first request to arrive will be executed and the response will be shared with requests arriving while there is a request in-flight", @@ -3456,7 +3483,7 @@ "name": "recordedQueriesMulti", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-14T12:34:22Z", - "deletionTimestamp": "2025-11-07T17:31:39Z" + "deletionTimestamp": "2025-11-10T20:31:43Z" }, "spec": { "description": "Enables writing multiple items from a single query within Recorded Queries", @@ -3579,7 +3606,8 @@ "metadata": { "name": "restrictedPluginApis", "resourceVersion": "1753776783657", - "creationTimestamp": "2025-09-01T09:57:00Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-07-29 08:13:03.657209 +0000 UTC" } @@ -3623,8 +3651,8 @@ "metadata": { "name": "savedQueries", "resourceVersion": "1756920131554", - "creationTimestamp": "2025-08-25T21:22:09Z", - "deletionTimestamp": "2025-09-18T12:07:31Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 17:22:11.554759 +0000 UTC" } @@ -3705,7 +3733,8 @@ "metadata": { "name": "secretsManagementAppPlatformUI", "resourceVersion": "1756816818369", - "creationTimestamp": "2025-09-02T12:40:18Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable the secrets management app platform UI", @@ -3747,7 +3776,7 @@ "name": "skipTokenRotationIfRecent", "resourceVersion": "1753448760331", "creationTimestamp": "2025-06-03T06:59:40Z", - "deletionTimestamp": "2025-10-22T10:29:12Z" + "deletionTimestamp": "2025-10-23T08:02:41Z" }, "spec": { "description": "Skip token rotation if it was already rotated less than 5 seconds ago", @@ -3762,7 +3791,7 @@ "name": "sqlDatasourceDatabaseSelection", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-06T16:28:52Z", - "deletionTimestamp": "2025-08-12T13:22:30Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables previous SQL data source dataset dropdown behavior", @@ -3832,7 +3861,8 @@ "metadata": { "name": "starsFromAPIServer", "resourceVersion": "1762958248290", - "creationTimestamp": "2025-09-19T10:00:55Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" } @@ -3860,11 +3890,14 @@ { "metadata": { "name": "suggestedDashboards", - "resourceVersion": "1762521182817", - "creationTimestamp": "2025-11-07T13:13:02Z" + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-11-07T13:38:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" + } }, "spec": { - "description": "Enable suggested dashboards when creating new dashboards", + "description": "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", "stage": "experimental", "codeowner": "@grafana/sharing-squad" } @@ -3874,7 +3907,7 @@ "name": "tableNextGen", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-26T03:57:57Z", - "deletionTimestamp": "2025-08-26T21:25:16Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Allows access to the new react-data-grid based table component.", @@ -3913,7 +3946,8 @@ "metadata": { "name": "teamFolders", "resourceVersion": "1755099058649", - "creationTimestamp": "2025-08-13T16:41:00Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables team folders functionality", @@ -3927,7 +3961,7 @@ "name": "teamHttpHeadersMimir", "resourceVersion": "1753448760331", "creationTimestamp": "2025-01-13T10:42:47Z", - "deletionTimestamp": "2025-08-07T09:04:46Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", @@ -3967,7 +4001,7 @@ "name": "templateVariablesUsesCombobox", "resourceVersion": "1753448760331", "creationTimestamp": "2025-01-31T09:53:13Z", - "deletionTimestamp": "2025-11-12T14:40:39Z" + "deletionTimestamp": "2025-11-13T03:31:18Z" }, "spec": { "description": "Use new **Combobox** component for template variables", @@ -3995,7 +4029,8 @@ "metadata": { "name": "tempoSearchBackendMigration", "resourceVersion": "1758029567165", - "creationTimestamp": "2025-09-16T11:18:16Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-16 13:32:47.165146 +0000 UTC" } @@ -4025,7 +4060,7 @@ "metadata": { "name": "timeRangePan", "resourceVersion": "1762290731154", - "creationTimestamp": "2025-10-24T19:49:53Z", + "creationTimestamp": "2025-11-05T01:39:46Z", "deletionTimestamp": "2025-11-06T17:39:31Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" @@ -4055,7 +4090,7 @@ "name": "tlsMemcached", "resourceVersion": "1753448760331", "creationTimestamp": "2024-05-09T19:12:08Z", - "deletionTimestamp": "2025-11-11T13:50:51Z" + "deletionTimestamp": "2025-11-12T15:49:28Z" }, "spec": { "description": "Use TLS-enabled memcached in the enterprise caching feature", @@ -4068,7 +4103,7 @@ "metadata": { "name": "transformationsEmptyPlaceholder", "resourceVersion": "1763373021129", - "creationTimestamp": "2025-11-11T13:14:25Z", + "creationTimestamp": "2025-11-17T13:57:05Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-17 09:50:21.129721 +0000 UTC" } @@ -4099,7 +4134,7 @@ "metadata": { "name": "ttlPluginInstanceManager", "resourceVersion": "1763462850634", - "creationTimestamp": "2025-11-18T10:47:30Z" + "creationTimestamp": "2025-11-18T11:17:23Z" }, "spec": { "description": "Enable TTL plugin instance manager", @@ -4181,7 +4216,7 @@ "name": "unifiedStorageHistoryPruner", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-17T10:36:38Z", - "deletionTimestamp": "2025-11-17T12:35:33Z" + "deletionTimestamp": "2025-11-17T19:47:37Z" }, "spec": { "description": "Enables the unified storage history pruner", @@ -4208,8 +4243,8 @@ "metadata": { "name": "unifiedStorageSearchAfterWriteExperimentalAPI", "resourceVersion": "1755089543487", - "creationTimestamp": "2025-08-13T14:05:15Z", - "deletionTimestamp": "2025-09-10T09:52:56Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-08-13 12:52:23.487521 +0000 UTC" } @@ -4266,8 +4301,8 @@ "metadata": { "name": "unifiedStorageUseFullNgram", "resourceVersion": "1758820248165", - "creationTimestamp": "2025-09-25T17:10:48Z", - "deletionTimestamp": "2025-10-22T12:24:39Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", @@ -4280,7 +4315,7 @@ "metadata": { "name": "unlimitedLayoutsNesting", "resourceVersion": "1760013838902", - "creationTimestamp": "2025-10-09T12:43:58Z" + "creationTimestamp": "2025-10-10T12:15:54Z" }, "spec": { "description": "Enables unlimited dashboard panel grouping", @@ -4293,7 +4328,8 @@ "metadata": { "name": "useKubernetesShortURLsAPI", "resourceVersion": "1756914263808", - "creationTimestamp": "2025-09-03T10:49:07Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" } @@ -4309,7 +4345,8 @@ "metadata": { "name": "useMultipleScopeNodesEndpoint", "resourceVersion": "1759237515008", - "creationTimestamp": "2025-09-30T13:05:15Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", @@ -4369,7 +4406,8 @@ "metadata": { "name": "vizActionsAuth", "resourceVersion": "1756904995830", - "creationTimestamp": "2025-08-08T18:59:18Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 13:09:55.830412 +0000 UTC" } @@ -4399,7 +4437,7 @@ "metadata": { "name": "zanzanaNoLegacyClient", "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-20T09:52:55Z", + "creationTimestamp": "2025-10-21T14:03:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-21 16:10:16.739546 +0000 UTC" } From 5ca3c27e4f89d3ad6e25ae249a94fd151d238e16 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 20 Nov 2025 14:17:53 -0300 Subject: [PATCH 014/423] Chore: Fix playlist flaky test (#114241) --- .../playlist/PlaylistNewPage.test.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/public/app/features/playlist/PlaylistNewPage.test.tsx b/public/app/features/playlist/PlaylistNewPage.test.tsx index 22a6bfb0117..fe33fada83c 100644 --- a/public/app/features/playlist/PlaylistNewPage.test.tsx +++ b/public/app/features/playlist/PlaylistNewPage.test.tsx @@ -24,7 +24,21 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ function getTestContext() { jest.clearAllMocks(); - const backendSrvMock = jest.spyOn(backendSrv, 'fetch').mockImplementation(() => of(createFetchResponse({}))); + + // Create separate spies for different HTTP methods + const postSpy = jest.fn(); + const otherSpy = jest.fn(); + + const backendSrvMock = jest.spyOn(backendSrv, 'fetch').mockImplementation((options) => { + if (options.method === 'POST') { + postSpy(options); + return of(createFetchResponse({})); + } + // Handle GET and other methods + otherSpy(options); + return of(createFetchResponse({ items: [] })); + }); + jest.spyOn(backendSrv, 'search').mockResolvedValue([]); const { rerender } = render( @@ -33,7 +47,7 @@ function getTestContext() { ); - return { rerender, backendSrvMock }; + return { rerender, backendSrvMock, postSpy, otherSpy }; } describe('PlaylistNewPage', () => { @@ -47,15 +61,17 @@ describe('PlaylistNewPage', () => { describe('when submitted', () => { it('then correct api should be called', async () => { - const { backendSrvMock } = getTestContext(); + const { postSpy } = getTestContext(); expect(locationService.getLocation().pathname).toEqual('/'); await userEvent.type(screen.getByRole('textbox', { name: selectors.pages.PlaylistForm.name }), 'A new name'); fireEvent.submit(screen.getByRole('button', { name: /save/i })); - await waitFor(() => expect(backendSrvMock).toHaveBeenCalledTimes(1)); - expect(backendSrvMock).toHaveBeenCalledWith( + await waitFor(() => expect(postSpy).toHaveBeenCalledTimes(1)); + + expect(postSpy).toHaveBeenCalledWith( expect.objectContaining({ + method: 'POST', body: expect.objectContaining({ spec: { title: 'A new name', From 496d6f8021eeadd78d87111aaed161e8e0da14be Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 20 Nov 2025 17:33:58 +0000 Subject: [PATCH 015/423] Frontend Service: Avoid double counting boot errors (#114250) avoid double counting boot errors --- pkg/services/frontend/index.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 2a8cebebef6..2eb4d82dce3 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -187,7 +187,15 @@ // Wrap in an IIFE to avoid polluting the global scope. Intentionally global-scope properties // are explicitly assigned to the `window` object. (() => { + // Grafana can only fail to load once + // However, it can fail to load in multiple different places + // To avoid double reporting the error, we use this boolean to check if we've already failed + let hasFailedToBoot = false; window.__grafana_load_failed = function(err) { + if (hasFailedToBoot) { + return; + } + hasFailedToBoot = true; console.error('Failed to load Grafana', err); document.querySelector('.fs-variant-loader').classList.add('fs-hidden'); document.querySelector('.fs-variant-error').classList.remove('fs-hidden'); @@ -356,7 +364,6 @@ document.head.appendChild(cssLink); } - window.__grafana_boot_data_promise = initGrafana() window.__grafana_boot_data_promise.catch((err) => { console.error("__grafana_boot_data_promise rejected", err); From 0b020c5e30fbc4b1f5585cb3107ed60f77c6ea8f Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:44:06 -0600 Subject: [PATCH 016/423] DOCS: add alias to SQL expressions doc (#114253) added alias to SQL expressions since the doc moved --- .../query-transform-data/sql-expressions/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md index 7cb91aaeb7f..ded072448ce 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md @@ -1,5 +1,6 @@ --- aliases: + - ../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/ labels: products: - cloud From 6f9c867660c4b76941fe090d01a86a2446719714 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 20 Nov 2025 13:49:46 -0500 Subject: [PATCH 017/423] Timeseries: More nuanced editing of linear threshold to avoid crashes (#112301) * Timeseries: More nuanced editing of linear threshold to avoid crashes * remove useEffect import * remove unused import * update error and invalid field handling to call out error case --- .../src/options/builder/axis.test.tsx | 33 ++++++++++ .../grafana-ui/src/options/builder/axis.tsx | 64 ++++++++++++++++--- public/locales/cs-CZ/grafana.json | 6 +- public/locales/de-DE/grafana.json | 6 +- public/locales/en-US/grafana.json | 8 ++- public/locales/es-ES/grafana.json | 6 +- public/locales/fr-FR/grafana.json | 6 +- public/locales/hu-HU/grafana.json | 6 +- public/locales/id-ID/grafana.json | 6 +- public/locales/it-IT/grafana.json | 6 +- public/locales/ja-JP/grafana.json | 6 +- public/locales/ko-KR/grafana.json | 6 +- public/locales/nl-NL/grafana.json | 6 +- public/locales/pl-PL/grafana.json | 6 +- public/locales/pt-BR/grafana.json | 6 +- public/locales/pt-PT/grafana.json | 6 +- public/locales/ru-RU/grafana.json | 6 +- public/locales/sv-SE/grafana.json | 6 +- public/locales/tr-TR/grafana.json | 6 +- public/locales/zh-Hans/grafana.json | 6 +- public/locales/zh-Hant/grafana.json | 6 +- 21 files changed, 168 insertions(+), 45 deletions(-) create mode 100644 packages/grafana-ui/src/options/builder/axis.test.tsx diff --git a/packages/grafana-ui/src/options/builder/axis.test.tsx b/packages/grafana-ui/src/options/builder/axis.test.tsx new file mode 100644 index 00000000000..0b749ec6f63 --- /dev/null +++ b/packages/grafana-ui/src/options/builder/axis.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ScaleDistribution } from '@grafana/schema'; + +import { ScaleDistributionEditor } from './axis'; + +describe('ScaleDistributionEditor', () => { + describe('Symlog', () => { + it('linear threshold should not dispatch a change for 0', async () => { + const onChange = jest.fn(); + const origValue = { type: ScaleDistribution.Symlog, log: 10 }; + + render(); + + // so annoying that this doesn't work. + // const el = await screen.findByLabelText('Linear threshold'); + const el = screen.getByTestId('input-wrapper').querySelector('input')!; + + await userEvent.type(el, '0'); + expect(onChange).not.toHaveBeenCalled(); + + await userEvent.type(el, '.'); + expect(onChange).not.toHaveBeenCalled(); + + await userEvent.type(el, '5'); + expect(onChange).toHaveBeenCalledWith({ linearThreshold: 0.5, ...origValue }); + + await userEvent.clear(el); + expect(onChange).toHaveBeenCalledWith(origValue); + }); + }); +}); diff --git a/packages/grafana-ui/src/options/builder/axis.tsx b/packages/grafana-ui/src/options/builder/axis.tsx index d3b6a74ee07..83237cb310c 100644 --- a/packages/grafana-ui/src/options/builder/axis.tsx +++ b/packages/grafana-ui/src/options/builder/axis.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import { FieldConfigEditorBuilder, FieldType, @@ -126,12 +128,31 @@ const LOG_DISTRIBUTION_OPTIONS: Array> = [ }, ]; +const isValidLinearThreshold = (value: number): string | undefined => { + if (Number.isNaN(value)) { + return t('grafana-ui.axis-builder.linear-threshold.warning.nan', 'Linear threshold must be a number'); + } + if (value === 0) { + return t('grafana-ui.axis-builder.linear-threshold.warning.zero', 'Linear threshold cannot be zero'); + } + return; +}; + /** * @internal */ -export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps) => { +export const ScaleDistributionEditor = ({ + value, + onChange, +}: Pick, 'value' | 'onChange'>) => { const type = value?.type ?? ScaleDistribution.Linear; const log = value?.log ?? 2; + + const [localLinearThreshold, setLocalLinearThreshold] = useState( + value?.linearThreshold != null ? String(value.linearThreshold) : '' + ); + const [linearThresholdWarning, setLinearThresholdWarning] = useState(); + const DISTRIBUTION_OPTIONS: Array> = [ { label: t('grafana-ui.builder.axis.scale-distribution-editor.distribution-options.label-linear', 'Linear'), @@ -161,7 +182,7 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps }} /> {(type === ScaleDistribution.Log || type === ScaleDistribution.Symlog) && ( - + { + if (ev.currentTarget.value) { + setLinearThresholdWarning(isValidLinearThreshold(Number(ev.currentTarget.value))); + } + }} onChange={(v) => { - onChange({ - ...value, - linearThreshold: Number(v.currentTarget.value), - }); + setLocalLinearThreshold(v.currentTarget.value); + if (v.currentTarget.value === '') { + const newValue = { ...value }; + delete newValue.linearThreshold; + onChange(newValue); + setLinearThresholdWarning(undefined); + return; + } + + const asNumber = Number(v.currentTarget.value); + if (isValidLinearThreshold(asNumber) == null) { + setLinearThresholdWarning(undefined); + onChange({ + ...value, + linearThreshold: asNumber, + }); + } }} /> diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 70cca883b8b..cc3b5e994c4 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Ukládání <1>" }, "axis-builder": { - "linear-threshold": "Lineární práh", + "linear-threshold": { + "label": "Lineární práh" + }, "log-base": "Základ protokolu" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Body" } } -} \ No newline at end of file +} diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index d491c81700b..a4eb397e4f7 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -8589,7 +8589,9 @@ "saving": "<1> wird gespeichert" }, "axis-builder": { - "linear-threshold": "Linearer Schwellenwert", + "linear-threshold": { + "label": "Linearer Schwellenwert" + }, "log-base": "Logarithmische Basis" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punkte" } } -} \ No newline at end of file +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d29fe101bd6..ba66da11bbf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8589,7 +8589,13 @@ "saving": "Saving <1>" }, "axis-builder": { - "linear-threshold": "Linear threshold", + "linear-threshold": { + "label": "Linear threshold", + "warning": { + "nan": "Linear threshold must be a number", + "zero": "Linear threshold cannot be zero" + } + }, "log-base": "Log base" }, "builder": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index adae8f28bf6..933d56ff352 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Guardando <1>" }, "axis-builder": { - "linear-threshold": "Umbral lineal", + "linear-threshold": { + "label": "Umbral lineal" + }, "log-base": "Base de registro" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Puntos" } } -} \ No newline at end of file +} diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e0648accd63..9bedeb3235c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Enregistrement en cours <1>" }, "axis-builder": { - "linear-threshold": "Seuil linéaire", + "linear-threshold": { + "label": "Seuil linéaire" + }, "log-base": "Base du journal" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Points" } } -} \ No newline at end of file +} diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 80e9b74c7a7..bccbc9c4437 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Mentés: <1>" }, "axis-builder": { - "linear-threshold": "Lineáris küszöbérték", + "linear-threshold": { + "label": "Lineáris küszöbérték" + }, "log-base": "Naplóbázis" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontok" } } -} \ No newline at end of file +} diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 1d0d348f979..e2653a0bbee 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -8553,7 +8553,9 @@ "saving": "Menyimpan <1>" }, "axis-builder": { - "linear-threshold": "Ambang linear", + "linear-threshold": { + "label": "Ambang linear" + }, "log-base": "Basis log" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "Poin" } } -} \ No newline at end of file +} diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 31e179978f2..4d9670ae838 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Salvataggio in corso di <1>" }, "axis-builder": { - "linear-threshold": "Soglia lineare", + "linear-threshold": { + "label": "Soglia lineare" + }, "log-base": "Base logaritmica" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punti" } } -} \ No newline at end of file +} diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 22a4029f62d..ba8d530e4a0 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -8553,7 +8553,9 @@ "saving": "<1>を保存しています" }, "axis-builder": { - "linear-threshold": "線形のしきい値", + "linear-threshold": { + "label": "線形のしきい値" + }, "log-base": "ログベース" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "ポイント" } } -} \ No newline at end of file +} diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 3174cc0be90..debe88cc4ac 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -8553,7 +8553,9 @@ "saving": "<1> 저장 중" }, "axis-builder": { - "linear-threshold": "선형 임계값", + "linear-threshold": { + "label": "선형 임계값" + }, "log-base": "로그 베이스" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "포인트" } } -} \ No newline at end of file +} diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index ebef81b6199..46a2279ecfb 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Opslaan <1>" }, "axis-builder": { - "linear-threshold": "Lineaire drempel", + "linear-threshold": { + "label": "Lineaire drempel" + }, "log-base": "Logboekbase" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punten" } } -} \ No newline at end of file +} diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 4ea5e15c281..977a67b7d6c 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Zapisywanie <1>" }, "axis-builder": { - "linear-threshold": "Próg liniowy", + "linear-threshold": { + "label": "Próg liniowy" + }, "log-base": "Baza logów" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Punkty" } } -} \ No newline at end of file +} diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c6b347e785f..efeee889595 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Salvando <1>" }, "axis-builder": { - "linear-threshold": "Limite linear", + "linear-threshold": { + "label": "Limite linear" + }, "log-base": "Base de log" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontos" } } -} \ No newline at end of file +} diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 26db09f82dd..fe3265ca211 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -8589,7 +8589,9 @@ "saving": "A guardar <1>" }, "axis-builder": { - "linear-threshold": "Limite linear", + "linear-threshold": { + "label": "Limite linear" + }, "log-base": "Base de registo" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontos" } } -} \ No newline at end of file +} diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index dec4add29ca..e4f9fe26d4f 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Сохранение <1>" }, "axis-builder": { - "linear-threshold": "Линейный порог", + "linear-threshold": { + "label": "Линейный порог" + }, "log-base": "База журналов" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Точки" } } -} \ No newline at end of file +} diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 65178bb1cc1..aee0dd73037 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Sparar <1>" }, "axis-builder": { - "linear-threshold": "Lineär tröskel", + "linear-threshold": { + "label": "Lineär tröskel" + }, "log-base": "Loggbas" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Poäng" } } -} \ No newline at end of file +} diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 9674e80d4de..d70e3969044 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "<1> kaydediliyor" }, "axis-builder": { - "linear-threshold": "Doğrusal eşik", + "linear-threshold": { + "label": "Doğrusal eşik" + }, "log-base": "Günlük tabanı" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Noktalar" } } -} \ No newline at end of file +} diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index d0302660f99..72c6cd12396 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -8553,7 +8553,9 @@ "saving": "正在保存<1>" }, "axis-builder": { - "linear-threshold": "线性阈值", + "linear-threshold": { + "label": "线性阈值" + }, "log-base": "记录基础" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "点" } } -} \ No newline at end of file +} diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index ad6c511d1f8..46758e840b5 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -8553,7 +8553,9 @@ "saving": "正在儲存<1>" }, "axis-builder": { - "linear-threshold": "線性臨界值", + "linear-threshold": { + "label": "線性臨界值" + }, "log-base": "日誌基礎" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "點" } } -} \ No newline at end of file +} From ef00dd494061de0764aad659e3d0174117004938 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Thu, 20 Nov 2025 17:14:27 -0400 Subject: [PATCH 018/423] Dashboard: New experimental time range zoom shortcuts (#114190) * feat(toggle): new feature toggle for time range zoom keyboard shortcuts * feat(keyboard-shortcuts): handle `t +` and `t -` key combinations * test(keyboard-shortcuts): validate time range zoom with `t +`, and `t -` * chore(i18n): update keyboard shortcut translations * refactor(time-range): no-op when timespan is zero instead of defaulting --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../core/components/help/HelpModal.test.tsx | 70 +++++++++ public/app/core/components/help/HelpModal.tsx | 45 ++++-- public/app/core/services/keybindingSrv.ts | 18 ++- .../scene/keyboardShortcuts.test.ts | 143 ++++++++++++++++++ .../scene/keyboardShortcuts.ts | 57 ++++++- public/locales/en-US/grafana.json | 1 + 10 files changed, 337 insertions(+), 22 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a7d2378f765..e5c59000c8d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -669,6 +669,10 @@ export interface FeatureToggles { */ timeRangePan?: boolean; /** + * Enables new keyboard shortcuts for time range zoom operations + */ + newTimeRangeZoomShortcuts?: boolean; + /** * Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c56f46a7193..28ff9e1809e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1099,6 +1099,13 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, + { + Name: "newTimeRangeZoomShortcuts", + Description: "Enables new keyboard shortcuts for time range zoom operations", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, + }, { Name: "azureMonitorDisableLogLimit", Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9465d6992c3..4ee6e57a9ed 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -152,6 +152,7 @@ pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false timeRangePan,experimental,@grafana/dataviz-squad,false,false,true +newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2298b91d854..bb0e03295a0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2873,6 +2873,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "newTimeRangeZoomShortcuts", + "resourceVersion": "1763646782694", + "creationTimestamp": "2025-11-20T13:53:02Z" + }, + "spec": { + "description": "Enables new keyboard shortcuts for time range zoom operations", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, { "metadata": { "name": "newVizSuggestions", diff --git a/public/app/core/components/help/HelpModal.test.tsx b/public/app/core/components/help/HelpModal.test.tsx index d343c5560b0..a410f1cd4d6 100644 --- a/public/app/core/components/help/HelpModal.test.tsx +++ b/public/app/core/components/help/HelpModal.test.tsx @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react'; import { useAssistant } from '@grafana/assistant'; +import { config } from '@grafana/runtime'; import { useShortcuts } from './HelpModal'; @@ -148,4 +149,73 @@ describe('useShortcuts', () => { const assistantShortcut = globalCategory!.shortcuts.find((shortcut) => shortcut.keys.includes('ctrl + .')); expect(assistantShortcut).toBeDefined(); }); + + describe('time range zoom shortcuts with feature toggle', () => { + beforeEach(() => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + }); + + it('should show new zoom shortcuts when feature toggle is enabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const zoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const zoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(zoomInShortcut).toBeDefined(); + expect(zoomInShortcut!.isNew).toBe(true); + expect(zoomOutShortcut).toBeDefined(); + expect(zoomOutShortcut!.isNew).toBe(true); + }); + + it('should show legacy t z shortcut when feature toggle is disabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const legacyZoomShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('z')); + const newZoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const newZoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(legacyZoomShortcut).toBeDefined(); + expect(newZoomInShortcut).toBeUndefined(); + expect(newZoomOutShortcut).toBeUndefined(); + }); + + it('should not show isNew badge on legacy shortcuts', () => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const legacyZoomShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('z')); + + expect(legacyZoomShortcut!.isNew).toBeUndefined(); + }); + + it('should show isNew badge on new shortcuts when feature toggle is enabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const zoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const zoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(zoomInShortcut!.isNew).toBe(true); + expect(zoomOutShortcut!.isNew).toBe(true); + }); + }); }); diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 4017443b252..82f39fac28f 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -2,9 +2,10 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { useAssistant } from '@grafana/assistant'; -import { GrafanaTheme2 } from '@grafana/data'; +import { FeatureState, GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Grid, Modal, useStyles2, Text } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { Grid, Modal, useStyles2, Text, FeatureBadge } from '@grafana/ui'; import { getModKey } from 'app/core/utils/browser'; export interface HelpModalProps { @@ -36,7 +37,7 @@ export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { - {shortcuts.map(({ keys, description }) => ( + {shortcuts.map(({ keys, description, isNew }) => ( {keys.map((key) => ( @@ -44,9 +45,12 @@ export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { ))} - - {description} - +
+ + {description} + + {isNew && } +
))} @@ -104,10 +108,25 @@ export const useShortcuts = () => { { category: t('help-modal.shortcuts-category.time-range', 'Time range'), shortcuts: [ - { - keys: ['t', 'z'], - description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), - }, + ...(config.featureToggles.newTimeRangeZoomShortcuts + ? [ + { + keys: ['t', '+'], + description: t('help-modal.shortcuts-description.zoom-in-time-range', 'Zoom in time range'), + isNew: true, + }, + { + keys: ['t', '-'], + description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), + isNew: true, + }, + ] + : [ + { + keys: ['t', 'z'], + description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), + }, + ]), { keys: ['t', '←'], description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'), @@ -277,6 +296,12 @@ function getStyles(theme: GrafanaTheme2) { whiteSpace: 'nowrap', minWidth: 83, // To match column widths with the widest }), + descriptionWrapper: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.75), + flexWrap: 'nowrap', + }), shortcutTableKey: css({ display: 'inline-block', textAlign: 'center', diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 865f461ce84..ea232baffcc 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -1,6 +1,6 @@ import { toggleAssistant, isAssistantAvailable } from '@grafana/assistant'; import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from '@grafana/data'; -import { LocationService } from '@grafana/runtime'; +import { LocationService, config } from '@grafana/runtime'; import { appEvents } from 'app/core/app_events'; import { getExploreUrl } from 'app/core/utils/explore'; import { toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; @@ -231,9 +231,19 @@ export class KeybindingSrv { appEvents.publish(new AbsoluteTimeEvent({ updateUrl })); }); - this.bind('t z', () => { - appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); - }); + if (config.featureToggles.newTimeRangeZoomShortcuts) { + this.bind('t +', () => { + appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); + }); + + this.bind('t -', () => { + appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); + }); + } else { + this.bind('t z', () => { + appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); + }); + } this.bind('ctrl+z', () => { appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts index 856f8df3193..9a70858f39b 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts @@ -1,4 +1,5 @@ import { LegacyGraphHoverClearEvent } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { behaviors, sceneGraph, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; @@ -253,4 +254,146 @@ describe('setupKeyboardShortcuts', () => { expect(drBinding).toBeDefined(); }); }); + + describe('time range zoom shortcuts with feature toggle', () => { + describe('when newTimeRangeZoomShortcuts is enabled', () => { + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + jest.clearAllMocks(); + }); + + it('should setup t + zoom in shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + expect(tPlusBinding).toBeDefined(); + }); + + it('should setup t - zoom out shortcut with keypress type', () => { + setupKeyboardShortcuts(mockScene); + + const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); + expect(tMinusBinding).toBeDefined(); + expect(tMinusBinding![0].type).toBe('keypress'); + }); + + it('should not setup t z shortcut when feature toggle is on', () => { + setupKeyboardShortcuts(mockScene); + + const tzBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't z'); + expect(tzBinding).toBeUndefined(); + }); + }); + + describe('when newTimeRangeZoomShortcuts is disabled', () => { + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + jest.clearAllMocks(); + }); + + it('should setup legacy t z shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tzBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't z'); + expect(tzBinding).toBeDefined(); + }); + + it('should not setup new zoom shortcuts when feature toggle is off', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); + + expect(tPlusBinding).toBeUndefined(); + expect(tMinusBinding).toBeUndefined(); + }); + }); + + describe('zoom handler logic', () => { + let mockTimeRange: ReturnType; + + function createMockTimeRange() { + return { + state: { + value: { + from: { valueOf: () => new Date('2024-01-01 12:00:00').getTime() }, + to: { valueOf: () => new Date('2024-01-01 18:00:00').getTime() }, // 6 hour span + raw: { from: 'now-6h', to: 'now' }, + }, + }, + onTimeRangeChange: jest.fn(), + } satisfies { + state: { + value: { + from: { valueOf: () => number }; + to: { valueOf: () => number }; + raw: { from: string; to: string }; + }; + }; + onTimeRangeChange: jest.Mock; + }; + } + + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + mockTimeRange = createMockTimeRange(); + + (sceneGraph.getTimeRange as jest.Mock).mockReturnValue(mockTimeRange); + jest.clearAllMocks(); + }); + + it('should zoom in (scale 0.5) when t + is pressed', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + handler(); + + // Scale 0.5 should result in 3 hour span (half of 6) + expect(mockTimeRange.onTimeRangeChange).toHaveBeenCalledWith( + expect.objectContaining({ + from: expect.any(Object), + to: expect.any(Object), + raw: expect.any(Object), + }) + ); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newSpan = call.to.valueOf() - call.from.valueOf(); + expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds + }); + + it('should keep center point when zooming in', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + const originalCenter = (mockTimeRange.state.value.from.valueOf() + mockTimeRange.state.value.to.valueOf()) / 2; + + handler(); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newCenter = (call.from.valueOf() + call.to.valueOf()) / 2; + + expect(newCenter).toBe(originalCenter); + }); + + it('should do nothing when timespan is zero', () => { + mockTimeRange.state.value.from.valueOf = () => new Date('2024-01-01 12:00:00').getTime(); + mockTimeRange.state.value.to.valueOf = () => new Date('2024-01-01 12:00:00').getTime(); // Same time + + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + handler(); + + // Should not call onTimeRangeChange when timespan is 0 + expect(mockTimeRange.onTimeRangeChange).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index b6ebf75bf44..1113b8a8a8c 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -1,4 +1,4 @@ -import { locationUtil, SetPanelAttentionEvent, LegacyGraphHoverClearEvent } from '@grafana/data'; +import { locationUtil, SetPanelAttentionEvent, LegacyGraphHoverClearEvent, dateTime } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { behaviors, sceneGraph, VizPanel } from '@grafana/scenes'; import { appEvents } from 'app/core/app_events'; @@ -130,13 +130,29 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { onTrigger: () => sceneGraph.getTimeRange(scene).onRefresh(), }); - // Zoom out - keybindings.addBinding({ - key: 't z', - onTrigger: () => { - handleZoomOut(scene); - }, - }); + if (config.featureToggles.newTimeRangeZoomShortcuts) { + keybindings.addBinding({ + key: 't +', + onTrigger: () => { + handleZoom(scene, 0.5); + }, + }); + + keybindings.addBinding({ + key: 't -', + type: 'keypress', // NOTE: Because some browsers/OS identify minus symbol differently. + onTrigger: () => { + handleZoomOut(scene); + }, + }); + } else { + keybindings.addBinding({ + key: 't z', + onTrigger: () => { + handleZoomOut(scene); + }, + }); + } keybindings.addBinding({ key: 'ctrl+z', @@ -266,6 +282,31 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { }; } +function handleZoom(scene: DashboardScene, scale: number) { + const timeRange = sceneGraph.getTimeRange(scene); + const currentRange = timeRange.state.value; + const timespan = currentRange.to.valueOf() - currentRange.from.valueOf(); + + if (timespan === 0) { + return; + } + + const center = currentRange.to.valueOf() - timespan / 2; + const newTimespan = timespan * scale; + + const to = center + newTimespan / 2; + const from = center - newTimespan / 2; + + timeRange.onTimeRangeChange({ + from: dateTime(from), + to: dateTime(to), + raw: { + from: dateTime(from), + to: dateTime(to), + }, + }); +} + function handleZoomOut(scene: DashboardScene) { const timePicker = dashboardSceneGraph.getTimePicker(scene); timePicker?.onZoom(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ba66da11bbf..9539dbefc30 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9297,6 +9297,7 @@ "toggle-panel-edit": "Toggle panel edit view", "toggle-panel-fullscreen": "Toggle panel fullscreen view", "toggle-panel-legend": "Toggle panel legend", + "zoom-in-time-range": "Zoom in time range", "zoom-out-time-range": "Zoom out time range" }, "title": "Shortcuts" From 07d975134d27f1c6bf44e326511bbef1bd5af26b Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:39:57 +0000 Subject: [PATCH 019/423] I18n: Download translations from Crowdin (#114278) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 17 +++++++++++++++-- public/locales/de-DE/grafana.json | 17 +++++++++++++++-- public/locales/es-ES/grafana.json | 17 +++++++++++++++-- public/locales/fr-FR/grafana.json | 17 +++++++++++++++-- public/locales/hu-HU/grafana.json | 17 +++++++++++++++-- public/locales/id-ID/grafana.json | 17 +++++++++++++++-- public/locales/it-IT/grafana.json | 17 +++++++++++++++-- public/locales/ja-JP/grafana.json | 17 +++++++++++++++-- public/locales/ko-KR/grafana.json | 17 +++++++++++++++-- public/locales/nl-NL/grafana.json | 17 +++++++++++++++-- public/locales/pl-PL/grafana.json | 17 +++++++++++++++-- public/locales/pt-BR/grafana.json | 17 +++++++++++++++-- public/locales/pt-PT/grafana.json | 17 +++++++++++++++-- public/locales/ru-RU/grafana.json | 17 +++++++++++++++-- public/locales/sv-SE/grafana.json | 17 +++++++++++++++-- public/locales/tr-TR/grafana.json | 17 +++++++++++++++-- public/locales/zh-Hans/grafana.json | 17 +++++++++++++++-- public/locales/zh-Hant/grafana.json | 17 +++++++++++++++-- 18 files changed, 270 insertions(+), 36 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index cc3b5e994c4..01d21c3f657 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineární práh" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Základ protokolu" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Použít pravou osu y" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Přepnout zobrazení úprav panelu", "toggle-panel-fullscreen": "Přepnout zobrazení panelu na celou obrazovku", "toggle-panel-legend": "Přepnout legendu panelu", + "zoom-in-time-range": "", "zoom-out-time-range": "Oddálit časový rozsah" }, "title": "Zkratky" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Kopírovat do schránky", "copy-to-clipboard-and-close": "Kopírovat do schránky a zavřít", "description-name-to-easily-identify-the-token": "Název pro snadnou identifikaci tokenu", + "description-no-expiration-disabled": "", "description-token": "Zkopírujte token teď, protože ho znovu neuvidíte. Ztráta tokenu vyžaduje vytvoření nového tokenu.", "generate-token": "Generování tokenu", "label-display-name": "Zobrazované jméno", "label-expiration": "Expirace", "label-expiration-date": "Datum vypršení platnosti", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Body" } } -} +} \ No newline at end of file diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index a4eb397e4f7..9a3b21b70aa 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Linearer Schwellenwert" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Logarithmische Basis" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Rechte y-Achse verwenden" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Fenster bearbeiten-Ansicht umschalten", "toggle-panel-fullscreen": "Vollbildansicht des Fensters umschalten", "toggle-panel-legend": "Legende des Fensters umschalten", + "zoom-in-time-range": "", "zoom-out-time-range": "Zeitbereich verkleinern" }, "title": "Shortcuts" @@ -12540,11 +12550,14 @@ "copy-clipboard": "In die Zwischenablage kopieren", "copy-to-clipboard-and-close": "In Zwischenablage kopieren und schließen", "description-name-to-easily-identify-the-token": "Name zur einfachen Identifizierung des Tokens", + "description-no-expiration-disabled": "", "description-token": "Kopieren Sie den Token jetzt, denn Sie werden ihn später nicht mehr sehen können. Wenn Sie einen Token verlieren, müssen Sie einen neuen Token erstellen.", "generate-token": "Token generieren", "label-display-name": "Anzeigename", "label-expiration": "Ablauf", "label-expiration-date": "Ablaufdatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punkte" } } -} +} \ No newline at end of file diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 933d56ff352..b46f38a8442 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Umbral lineal" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de registro" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usar eje y derecho" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Cambiar la vista de edición del panel", "toggle-panel-fullscreen": "Cambiar la vista de pantalla completa del panel", "toggle-panel-legend": "Cambiar leyenda del panel", + "zoom-in-time-range": "", "zoom-out-time-range": "Reducir el intervalo de tiempo" }, "title": "Accesos directos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar al portapapeles", "copy-to-clipboard-and-close": "Copiar al portapapeles y cerrar", "description-name-to-easily-identify-the-token": "Nombre para identificar fácilmente el token", + "description-no-expiration-disabled": "", "description-token": "Copia el token ahora, ya que no lo volverás a ver. Si lo pierdes, deberás crear uno nuevo.", "generate-token": "Generar token", "label-display-name": "Nombre para mostrar", "label-expiration": "Caducidad", "label-expiration-date": "Fecha de vencimiento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Puntos" } } -} +} \ No newline at end of file diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 9bedeb3235c..428c251e152 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Seuil linéaire" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base du journal" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Utiliser l'axe y droit" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Activer/Désactiver la vue Édition du panneau", "toggle-panel-fullscreen": "Activer/Désactiver la vue Plein écran", "toggle-panel-legend": "Activer/désactiver la légende du panneau", + "zoom-in-time-range": "", "zoom-out-time-range": "Dézoomer la plage de temps" }, "title": "Raccourcis" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copier dans le presse-papiers", "copy-to-clipboard-and-close": "Copier dans le presse-papiers et fermer", "description-name-to-easily-identify-the-token": "Nom pour identifier facilement le jeton", + "description-no-expiration-disabled": "", "description-token": "Copiez le jeton maintenant, car vous ne pourrez plus le revoir. Si vous perdez un jeton, vous devrez 'en créer un nouveau.", "generate-token": "Générer un jeton", "label-display-name": "Pseudo", "label-expiration": "Expiration", "label-expiration-date": "Date d’expiration", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Jeton" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Points" } } -} +} \ No newline at end of file diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index bccbc9c4437..ac46a842a13 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineáris küszöbérték" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Naplóbázis" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Jobb y tengely használata" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Panelszerkesztési nézet ki- és bekapcsolása", "toggle-panel-fullscreen": "Panel teljes képernyős nézetének ki- és bekapcsolása", "toggle-panel-legend": "Panel jelmagyarázatának ki- és bekapcsolása", + "zoom-in-time-range": "", "zoom-out-time-range": "Időtartomány kicsinyítése" }, "title": "Billentyűparancsok" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Másolás vágólapra", "copy-to-clipboard-and-close": "Másolás vágólapra és bezárás", "description-name-to-easily-identify-the-token": "Név a token egyszerű azonosításához", + "description-no-expiration-disabled": "", "description-token": "Másolja át most a tokent, mert később nem lesz látható. A token elvesztése esetén új tokent kell létrehozni.", "generate-token": "Token generálása", "label-display-name": "Megjelenített név", "label-expiration": "Lejárat", "label-expiration-date": "Lejárati dátum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontok" } } -} +} \ No newline at end of file diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index e2653a0bbee..b00d4be59e5 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Ambang linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Basis log" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "Gunakan sumbu y kanan" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "Alihkan tampilan edit panel", "toggle-panel-fullscreen": "Alihkan tampilan layar penuh panel", "toggle-panel-legend": "Alihkan legenda panel", + "zoom-in-time-range": "", "zoom-out-time-range": "Perkecil rentang waktu" }, "title": "Pintasan" @@ -12485,11 +12495,14 @@ "copy-clipboard": "Salin ke papan klip", "copy-to-clipboard-and-close": "Salin ke papan klip dan tutup", "description-name-to-easily-identify-the-token": "Nama untuk mengidentifikasi token dengan mudah", + "description-no-expiration-disabled": "", "description-token": "Salin token sekarang karena Anda tidak akan dapat melihatnya lagi. Kehilangan token membutuhkan pembuatan token baru.", "generate-token": "Buat token", "label-display-name": "Nama tampilan", "label-expiration": "Kedaluwarsa", "label-expiration-date": "Tanggal kedaluwarsa", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "Poin" } } -} +} \ No newline at end of file diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 4d9670ae838..ab7d67dc580 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Soglia lineare" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base logaritmica" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usa l'asse y destro" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Attiva/disattiva la vista di modifica del pannello", "toggle-panel-fullscreen": "Attiva/disattiva la vista a schermo intero del pannello", "toggle-panel-legend": "Attiva/disattiva la legenda del pannello", + "zoom-in-time-range": "", "zoom-out-time-range": "Riduci l'intervallo di tempo" }, "title": "Scelte rapide" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copia negli appunti", "copy-to-clipboard-and-close": "Copia negli appunti e chiudi", "description-name-to-easily-identify-the-token": "Nome per identificare facilmente il token", + "description-no-expiration-disabled": "", "description-token": "Copia il token ora perché non potrai più visualizzarlo. La perdita di un token richiede la creazione di uno nuovo.", "generate-token": "Genera token", "label-display-name": "Visualizza nome", "label-expiration": "Scadenza", "label-expiration-date": "Data di scadenza", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punti" } } -} +} \ No newline at end of file diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index ba8d530e4a0..7a09ed72d4c 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "線形のしきい値" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "ログベース" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "右y軸を使用" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "パネル編集ビューを切り替え", "toggle-panel-fullscreen": "パネルの全画面表示を切り替え", "toggle-panel-legend": "パネルの凡例を切り替え", + "zoom-in-time-range": "", "zoom-out-time-range": "時間範囲をズームアウト" }, "title": "ショートカット" @@ -12485,11 +12495,14 @@ "copy-clipboard": "クリップボードにコピー", "copy-to-clipboard-and-close": "クリップボードにコピーして閉じる", "description-name-to-easily-identify-the-token": "トークンを簡単に識別するための名前", + "description-no-expiration-disabled": "", "description-token": "トークンはもう表示できなくなるため、今すぐコピーしてください。トークンを失うと、新しいトークンを作成する必要があります。", "generate-token": "トークンを生成", "label-display-name": "表示名", "label-expiration": "有効期限", "label-expiration-date": "有効期日", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "トークン" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "ポイント" } } -} +} \ No newline at end of file diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index debe88cc4ac..56dd9df87fb 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "선형 임계값" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "로그 베이스" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "오른쪽 y축 사용" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "패널 편집 보기 토글", "toggle-panel-fullscreen": "패널 전체 화면 보기 토글", "toggle-panel-legend": "패널 범례 토글", + "zoom-in-time-range": "", "zoom-out-time-range": "시간 범위 확대" }, "title": "단축키" @@ -12485,11 +12495,14 @@ "copy-clipboard": "클립보드로 복사", "copy-to-clipboard-and-close": "클립보드로 복사하고 닫기", "description-name-to-easily-identify-the-token": "토큰을 쉽게 식별할 수 있는 이름", + "description-no-expiration-disabled": "", "description-token": "토큰이 다시 표시되지 않으므로 지금 복사해 두세요. 토큰을 분실하면 새 토큰을 생성해야 합니다.", "generate-token": "토큰 생성", "label-display-name": "표시 이름", "label-expiration": "만료", "label-expiration-date": "만료일", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "토큰" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "포인트" } } -} +} \ No newline at end of file diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 46a2279ecfb..854505274f8 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineaire drempel" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Logboekbase" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Rechter y-as gebruiken" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Paneelbewerkingsweergave in-/uitschakelen", "toggle-panel-fullscreen": "Paneelweergave volledig scherm in-/uitschakelen", "toggle-panel-legend": "Paneellegenda in-/uitschakelen", + "zoom-in-time-range": "", "zoom-out-time-range": "Tijdsbereik uitzoomen" }, "title": "Snelkoppelingen" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Kopiëren naar klembord", "copy-to-clipboard-and-close": "Kopiëren naar klembord en sluiten", "description-name-to-easily-identify-the-token": "Naam om het token gemakkelijk te identificeren", + "description-no-expiration-disabled": "", "description-token": "Kopieer het token nu, want hierna kun je het niet meer zien. Als je een token verliest, moet je een nieuw token maken.", "generate-token": "Token genereren", "label-display-name": "Weergavenaam", "label-expiration": "Vervaldatum", "label-expiration-date": "Vervaldatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punten" } } -} +} \ No newline at end of file diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 977a67b7d6c..f3d06bb2edb 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Próg liniowy" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Baza logów" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Użyj prawej półosi OY" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Przełącz widok edycji panelu", "toggle-panel-fullscreen": "Przełącz widok pełnoekranowy panelu", "toggle-panel-legend": "Przełącz legendę panelu", + "zoom-in-time-range": "", "zoom-out-time-range": "Oddal zakres czasu" }, "title": "Skróty" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Kopiuj do schowka", "copy-to-clipboard-and-close": "Kopiuj do schowka i zamknij", "description-name-to-easily-identify-the-token": "Nazwa ułatwiająca identyfikację tokena", + "description-no-expiration-disabled": "", "description-token": "Skopiuj token teraz, ponieważ później nie będzie go można ponownie wyświetlić. Utrata tokena wymaga utworzenia nowego.", "generate-token": "Wygeneruj token", "label-display-name": "Nazwa wyświetlana", "label-expiration": "Wygaśnięcie", "label-expiration-date": "Data ważności", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Punkty" } } -} +} \ No newline at end of file diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index efeee889595..a46882c87c8 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Limite linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de log" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usar o eixo y à direita" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Alternar visualização de edição de painel", "toggle-panel-fullscreen": "Alternar visualização de painel em tela cheia", "toggle-panel-legend": "Alternar legenda de painel", + "zoom-in-time-range": "", "zoom-out-time-range": "Diminuir o intervalo de tempo" }, "title": "Atalhos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar para a área de transferência", "copy-to-clipboard-and-close": "Copiar para a área de transferência e fechar", "description-name-to-easily-identify-the-token": "Nome para identificar facilmente o token", + "description-no-expiration-disabled": "", "description-token": "Copie o token agora, já que você não será capaz de vê-lo novamente. Perder um token requer criar um novo.", "generate-token": "Gerar token", "label-display-name": "Nome de exibição", "label-expiration": "Validade", "label-expiration-date": "Data de vencimento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontos" } } -} +} \ No newline at end of file diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index fe3265ca211..81009be83be 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Limite linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de registo" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Utilizar o eixo y direito" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Alternar vista de edição do painel", "toggle-panel-fullscreen": "Alternar vista de ecrã inteiro do painel", "toggle-panel-legend": "Alternar legenda do painel", + "zoom-in-time-range": "", "zoom-out-time-range": "Diminuir o zoom do intervalo de tempo" }, "title": "Atalhos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar para a área de transferência", "copy-to-clipboard-and-close": "Copiar para a área de transferência e fechar", "description-name-to-easily-identify-the-token": "Nome para identificar facilmente o token", + "description-no-expiration-disabled": "", "description-token": "Copie o token agora, pois não poderá vê-lo novamente. Perder um token requer a criação de um novo.", "generate-token": "Gerar token", "label-display-name": "Nome de exibição", "label-expiration": "Validade", "label-expiration-date": "Data de vencimento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontos" } } -} +} \ No newline at end of file diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index e4f9fe26d4f..a1c58056eea 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Линейный порог" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "База журналов" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Использовать правую ось y" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Включение/выключение представления редактирования панели", "toggle-panel-fullscreen": "Включение/выключение полноэкранного представления панели", "toggle-panel-legend": "Включение/выключение условных обозначений панели", + "zoom-in-time-range": "", "zoom-out-time-range": "Уменьшение масштаба временного диапазона" }, "title": "Сочетания клавиш" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Копировать в буфер обмена", "copy-to-clipboard-and-close": "Копировать в буфер обмена и закрыть", "description-name-to-easily-identify-the-token": "Название для удобной идентификации токена", + "description-no-expiration-disabled": "", "description-token": "Скопируйте токен сейчас, так как вы не сможете увидеть его снова. При потере токена требуется создать новый.", "generate-token": "Создать токен", "label-display-name": "Отображаемое имя", "label-expiration": "Срок действия", "label-expiration-date": "Срок действия", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Токен" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Точки" } } -} +} \ No newline at end of file diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index aee0dd73037..ba91a108866 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineär tröskel" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Loggbas" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Använd höger y-axel" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Växla panelredigeringsvy", "toggle-panel-fullscreen": "Växla panelens helskärmsvy", "toggle-panel-legend": "Växla panelförklaring", + "zoom-in-time-range": "", "zoom-out-time-range": "Zooma ut tidsintervall" }, "title": "Genvägar" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Kopiera till urklippet", "copy-to-clipboard-and-close": "Kopiera till urklippet och stäng", "description-name-to-easily-identify-the-token": "Namn för att enkelt identifiera token", + "description-no-expiration-disabled": "", "description-token": "Kopiera token nu eftersom du inte kommer att kunna visa den igen. Om du förlorar denna token behöver du skapa en ny.", "generate-token": "Generera token", "label-display-name": "Visningsnamn", "label-expiration": "Utgång", "label-expiration-date": "Utgångsdatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Poäng" } } -} +} \ No newline at end of file diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index d70e3969044..6515d038e6a 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Doğrusal eşik" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Günlük tabanı" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Sağ Y eksenini kullan" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Panel düzenleme görünümünü aç/kapat", "toggle-panel-fullscreen": "Panel tam ekran görünümünü aç/kapat", "toggle-panel-legend": "Panel açıklamasını aç/kapat", + "zoom-in-time-range": "", "zoom-out-time-range": "Zaman aralığını büyüt" }, "title": "Kısayollar" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Panoya kopyala", "copy-to-clipboard-and-close": "Panoya kopyala ve kapat", "description-name-to-easily-identify-the-token": "Belirteci kolayca tanımlamak için ad", + "description-no-expiration-disabled": "", "description-token": "Bu noktada belirteci kopyalayın çünkü tekrar göremeyeceksiniz. Belirtecin kaybedilmesi yeni bir belirteç oluşturulmasını gerektirir.", "generate-token": "Belirteç oluştur", "label-display-name": "Görünen ad", "label-expiration": "Son", "label-expiration-date": "Son kullanma tarihi", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Belirteç" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Noktalar" } } -} +} \ No newline at end of file diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 72c6cd12396..925a737732c 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "线性阈值" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "记录基础" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "使用右侧 Y 轴" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "切换面板编辑视图", "toggle-panel-fullscreen": "切换面板全屏视图", "toggle-panel-legend": "切换面板图例", + "zoom-in-time-range": "", "zoom-out-time-range": "缩放时间范围" }, "title": "快捷键" @@ -12485,11 +12495,14 @@ "copy-clipboard": "复制到剪贴板", "copy-to-clipboard-and-close": "复制到剪贴板并关闭", "description-name-to-easily-identify-the-token": "用于轻松识别令牌的名称", + "description-no-expiration-disabled": "", "description-token": "现在就复制令牌,因为您将无法再次看到它。丢失令牌后需要创建一个新令牌。", "generate-token": "生成令牌", "label-display-name": "显示名称", "label-expiration": "到期", "label-expiration-date": "到期日期", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "令牌" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "点" } } -} +} \ No newline at end of file diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 46758e840b5..c128f8b4360 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "線性臨界值" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "日誌基礎" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "使用右 y 軸" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "切換面板編輯檢視", "toggle-panel-fullscreen": "切換面板全螢幕檢視", "toggle-panel-legend": "切換面板圖例", + "zoom-in-time-range": "", "zoom-out-time-range": "縮小時間範圍" }, "title": "捷徑" @@ -12485,11 +12495,14 @@ "copy-clipboard": "複製至剪貼簿", "copy-to-clipboard-and-close": "複製至剪貼簿並關閉", "description-name-to-easily-identify-the-token": "輕鬆識別權杖的名稱", + "description-no-expiration-disabled": "", "description-token": "請立即複製權杖,因為您將無法再次看到它。若您遺失權杖,則需要建立新的權杖。", "generate-token": "產生權杖", "label-display-name": "顯示名稱", "label-expiration": "過期", "label-expiration-date": "過期日期", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "權杖" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "點" } } -} +} \ No newline at end of file From 6f68b96097821e37dea3f29e22ecd81705a88e6f Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 21 Nov 2025 08:56:17 +0100 Subject: [PATCH 020/423] Alerting: Fix minInterval interpolation when creating rules from panel (#114238) Interpolate minInterval query parameter before query conversion --- .../features/alerting/unified/utils/rule-form.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 3c000d6cab7..077934e21e1 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -21,6 +21,7 @@ import { getQueryRunnerFor, } from 'app/features/dashboard-scene/utils/utils'; import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; +import { getTemplateSrv } from 'app/features/templating/template_srv'; import { LokiQuery } from 'app/plugins/datasource/loki/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { @@ -744,6 +745,9 @@ export const panelToRuleFormValues = async ( return undefined; } + // Interpolate interval to replace dashboard variables + const interpolatedInterval = panel.interval ? panel.replaceVariables(panel.interval, undefined) : undefined; + const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time)); const queries = await dataQueriesToGrafanaQueries( targets, @@ -751,7 +755,7 @@ export const panelToRuleFormValues = async ( panel.scopedVars || {}, panel.datasource ?? undefined, panel.maxDataPoints ?? undefined, - panel.interval ?? undefined + interpolatedInterval ); // if no alerting capable queries are found, can't create a rule if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) { @@ -825,13 +829,19 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise

Date: Fri, 21 Nov 2025 09:04:04 +0100 Subject: [PATCH 021/423] fix: handle empty provisioning folder gracefully in stub provisioning (#114273) fix: provisioning folder empty case --- pkg/services/provisioning/stubs.go | 14 +- pkg/services/provisioning/stubs_test.go | 449 ++++++++++++++++++ .../dashboards/multiple.yaml | 24 + .../stub-configs/dashboards/single.yaml | 10 + 4 files changed, 492 insertions(+), 5 deletions(-) create mode 100644 pkg/services/provisioning/stubs_test.go create mode 100644 pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml create mode 100644 pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml diff --git a/pkg/services/provisioning/stubs.go b/pkg/services/provisioning/stubs.go index b40e66aac5c..8e8b2078fbc 100644 --- a/pkg/services/provisioning/stubs.go +++ b/pkg/services/provisioning/stubs.go @@ -19,15 +19,19 @@ func ProvideStubProvisioningService(cfg *setting.Cfg) (StubProvisioningService, } func NewStubProvisioning(path string) (StubProvisioningService, error) { - cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) - if err != nil { - return nil, err - } + logger := log.New("provisioning.stub") stub := &stubProvisioning{ path: make(map[string]string), allowUIUpdates: make(map[string]bool), - log: log.New("provisioning.stub"), + log: logger, } + + cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) + if err != nil { + logger.Warn("can't read dashboard provisioning files from directory", "path", filepath.Join(path, "dashboards"), "error", err) + return stub, nil + } + for _, cfg := range cfgs { stub.path[cfg.Name] = cfg.Options["path"].(string) stub.allowUIUpdates[cfg.Name] = cfg.AllowUIUpdates diff --git a/pkg/services/provisioning/stubs_test.go b/pkg/services/provisioning/stubs_test.go new file mode 100644 index 00000000000..374434d9f59 --- /dev/null +++ b/pkg/services/provisioning/stubs_test.go @@ -0,0 +1,449 @@ +package provisioning + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" +) + +func TestNewStubProvisioning(t *testing.T) { + t.Run("should handle non-existent directory gracefully", func(t *testing.T) { + // This tests the bug fix - should not fail when directory doesn't exist + stub, err := NewStubProvisioning("/non-existent-path") + + require.NoError(t, err, "should not return error for non-existent directory") + require.NotNil(t, stub, "should return valid stub") + + // Should return resolved empty path for non-existent configs + // Note: filepath.Abs("") returns current directory + path := stub.GetDashboardProvisionerResolvedPath("any-name") + assert.NotEmpty(t, path, "returns resolved empty path") + + assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name")) + }) + + t.Run("should handle empty directory", func(t *testing.T) { + testPath := "./testdata/stub-configs-empty" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Empty directory should result in empty maps + // Note: filepath.Abs("") returns current directory + path := stub.GetDashboardProvisionerResolvedPath("any-name") + assert.NotEmpty(t, path, "returns resolved empty path") + + assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name")) + }) + + t.Run("should successfully read single config", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Should have the test-dashboard config + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard") + assert.True(t, allowUpdates, "test-dashboard should allow UI updates") + }) + + t.Run("should successfully read multiple configs", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Check editable-dashboard (allowUiUpdates: true) + allowUpdates := stub.GetAllowUIUpdatesFromConfig("editable-dashboard") + assert.True(t, allowUpdates, "editable-dashboard should allow UI updates") + + // Check readonly-dashboard (allowUiUpdates: false) + allowUpdates = stub.GetAllowUIUpdatesFromConfig("readonly-dashboard") + assert.False(t, allowUpdates, "readonly-dashboard should not allow UI updates") + + // Check default-dashboard (allowUiUpdates not specified, should default to false) + allowUpdates = stub.GetAllowUIUpdatesFromConfig("default-dashboard") + assert.False(t, allowUpdates, "default-dashboard should default to not allowing UI updates") + + // Verify paths are set correctly + path := stub.GetDashboardProvisionerResolvedPath("editable-dashboard") + assert.NotEmpty(t, path) + + path = stub.GetDashboardProvisionerResolvedPath("readonly-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle invalid YAML gracefully", func(t *testing.T) { + // Create a temporary directory with invalid YAML + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Write invalid YAML + invalidYAML := `this is not valid yaml: [[[` + err = os.WriteFile(filepath.Join(dashboardsDir, "invalid.yaml"), []byte(invalidYAML), 0644) + require.NoError(t, err) + + // Should handle gracefully by returning empty stub + _, err = NewStubProvisioning(tmpDir) + require.NoError(t, err) + }) +} + +func TestGetAllowUIUpdatesFromConfig(t *testing.T) { + t.Run("should return true for config with allowUiUpdates: true", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("editable-dashboard") + assert.True(t, result) + }) + + t.Run("should return false for config with allowUiUpdates: false", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("readonly-dashboard") + assert.False(t, result) + }) + + t.Run("should return false for non-existent config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("non-existent-config") + assert.False(t, result, "should return false for non-existent config") + }) + + t.Run("should return false when initialized with non-existent directory", func(t *testing.T) { + stub, err := NewStubProvisioning("/non-existent-path") + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("any-name") + assert.False(t, result) + }) +} + +func TestGetDashboardProvisionerResolvedPath(t *testing.T) { + t.Run("should resolve valid path", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle non-existent config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("non-existent") + // Returns resolved empty path (current directory) for non-existent config + assert.NotEmpty(t, path, "returns resolved path even for non-existent config") + }) + + t.Run("should handle non-existent path in config", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + // The path in config is /tmp/test-dashboards which likely doesn't exist + // Should still return a path (with warning logged) + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should resolve relative paths to absolute", func(t *testing.T) { + // Create a temporary directory with a config that has a relative path + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create a subdirectory for the dashboard path + dashPath := filepath.Join(tmpDir, "my-dashboards") + err = os.MkdirAll(dashPath, 0750) + require.NoError(t, err) + + // Write config with absolute path (relative paths in options are not resolved correctly by the stub) + configContent := `apiVersion: 1 +providers: +- name: 'relative-path' + orgId: 1 + type: file + options: + path: ` + dashPath + ` +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("relative-path") + // Should be absolute path + assert.True(t, filepath.IsAbs(path), "path should be absolute") + assert.Contains(t, path, "my-dashboards") + }) + + t.Run("should handle symlinks", func(t *testing.T) { + // Create a temporary directory structure with symlink + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create actual directory + actualDir := filepath.Join(tmpDir, "actual-dashboards") + err = os.MkdirAll(actualDir, 0750) + require.NoError(t, err) + + // Create symlink + symlinkPath := filepath.Join(tmpDir, "linked-dashboards") + err = os.Symlink(actualDir, symlinkPath) + if err != nil { + t.Skip("Cannot create symlinks on this system") + } + + // Write config with symlink path + configContent := `apiVersion: 1 +providers: +- name: 'symlink-path' + orgId: 1 + type: file + options: + path: ` + symlinkPath + ` +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("symlink-path") + // Should resolve symlink to actual path + assert.NotEmpty(t, path) + + // EvalSymlinks resolves to the real path, which might include /private prefix on macOS + // Just verify it contains the actual directory name + assert.Contains(t, path, "actual-dashboards", "should resolve symlink to actual directory") + }) + + t.Run("should fallback to original path on EvalSymlinks failure", func(t *testing.T) { + // This is harder to test directly, but we can verify the fallback logic exists + // by checking that even with a broken symlink, we get a path back + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create config with a path that will fail EvalSymlinks + configContent := `apiVersion: 1 +providers: +- name: 'broken-path' + orgId: 1 + type: file + options: + path: /tmp/test-stub-dashboards +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("broken-path") + assert.NotEmpty(t, path, "should return fallback path even if EvalSymlinks fails") + }) +} + +func TestProvideStubProvisioningService(t *testing.T) { + t.Run("should create stub from config", func(t *testing.T) { + cfg := &setting.Cfg{ + ProvisioningPath: "./testdata/stub-configs", + } + + stub, err := ProvideStubProvisioningService(cfg) + require.NoError(t, err) + require.NotNil(t, stub) + + // Verify it works + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle non-existent provisioning path", func(t *testing.T) { + cfg := &setting.Cfg{ + ProvisioningPath: "/non-existent-provisioning-path", + } + + stub, err := ProvideStubProvisioningService(cfg) + require.NoError(t, err, "should not fail with non-existent path") + require.NotNil(t, stub) + }) +} + +func TestStubProvisioningEdgeCases(t *testing.T) { + t.Run("should handle empty config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + // Empty config name returns resolved empty path (current directory) + path := stub.GetDashboardProvisionerResolvedPath("") + assert.NotEmpty(t, path) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("") + assert.False(t, allowUpdates) + }) + + t.Run("should handle special characters in config name", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + configContent := `apiVersion: 1 +providers: +- name: 'test-dashboard-with-special-chars-123' + orgId: 1 + type: file + allowUiUpdates: true + options: + path: /tmp/test +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard-with-special-chars-123") + assert.True(t, allowUpdates) + }) + + t.Run("should handle paths with spaces", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create directory with spaces + pathWithSpaces := filepath.Join(tmpDir, "my dashboard folder") + err = os.MkdirAll(pathWithSpaces, 0750) + require.NoError(t, err) + + configContent := `apiVersion: 1 +providers: +- name: 'space-test' + orgId: 1 + type: file + options: + path: "` + pathWithSpaces + `" +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("space-test") + assert.NotEmpty(t, path) + assert.Contains(t, path, "dashboard folder") + }) + + t.Run("should handle multiple YAML files in directory", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create first config file + config1 := `apiVersion: 1 +providers: +- name: 'config1' + orgId: 1 + type: file + allowUiUpdates: true + options: + path: /tmp/config1 +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config1.yaml"), []byte(config1), 0644) + require.NoError(t, err) + + // Create second config file + config2 := `apiVersion: 1 +providers: +- name: 'config2' + orgId: 1 + type: file + allowUiUpdates: false + options: + path: /tmp/config2 +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config2.yaml"), []byte(config2), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + // Both configs should be loaded + assert.True(t, stub.GetAllowUIUpdatesFromConfig("config1")) + assert.False(t, stub.GetAllowUIUpdatesFromConfig("config2")) + + path1 := stub.GetDashboardProvisionerResolvedPath("config1") + path2 := stub.GetDashboardProvisionerResolvedPath("config2") + assert.NotEmpty(t, path1) + assert.NotEmpty(t, path2) + assert.NotEqual(t, path1, path2) + }) + + t.Run("should ignore non-YAML files", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create YAML config + yamlConfig := `apiVersion: 1 +providers: +- name: 'yaml-config' + orgId: 1 + type: file + options: + path: /tmp/test +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(yamlConfig), 0644) + require.NoError(t, err) + + // Create non-YAML files + err = os.WriteFile(filepath.Join(dashboardsDir, "readme.txt"), []byte("test"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(dashboardsDir, "config.json"), []byte("{}"), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + // Should only load the YAML config + path := stub.GetDashboardProvisionerResolvedPath("yaml-config") + assert.NotEmpty(t, path) + }) +} diff --git a/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml b/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml new file mode 100644 index 00000000000..e36fc293380 --- /dev/null +++ b/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml @@ -0,0 +1,24 @@ +apiVersion: 1 + +providers: +- name: 'editable-dashboard' + orgId: 1 + folder: 'test-folder' + type: file + allowUiUpdates: true + options: + path: /tmp/editable-dashboards + +- name: 'readonly-dashboard' + orgId: 2 + folder: 'readonly-folder' + type: file + allowUiUpdates: false + options: + path: /tmp/readonly-dashboards + +- name: 'default-dashboard' + orgId: 1 + type: file + options: + path: /tmp/default-dashboards diff --git a/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml b/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml new file mode 100644 index 00000000000..cfbf0a3f1c5 --- /dev/null +++ b/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: +- name: 'test-dashboard' + orgId: 1 + folder: '' + type: file + allowUiUpdates: true + options: + path: /tmp/test-dashboards From e7377a88423a8bb49e96839eb4596fd1dd3cfdb9 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:31:22 +0100 Subject: [PATCH 022/423] Alerting: Update docs for ash AI helper button (#114229) Update docs for ash AI helper button --- .../view-alert-state-history.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/sources/alerting/monitor-status/view-alert-state-history.md b/docs/sources/alerting/monitor-status/view-alert-state-history.md index 553ef0a8fac..156fd6c9f09 100644 --- a/docs/sources/alerting/monitor-status/view-alert-state-history.md +++ b/docs/sources/alerting/monitor-status/view-alert-state-history.md @@ -73,6 +73,37 @@ To access the History page, complete the following steps. {{< figure src="/media/docs/alerting/alerting-alert-history-tab.png" max-width="750px" alt="Alert History tab in Grafana Alerting" >}} +## Use Grafana Assistant to analyze alert state history + +{{< admonition type="note" >}} +This feature is available in Grafana Cloud when Grafana Assistant is enabled. +{{< /admonition >}} + +The **Analyze with Assistant** button provides AI-powered analysis of your alert history to help you understand and troubleshoot alert patterns. Located in the top-right corner of the History page event list, this button uses Grafana Assistant to analyze the events displayed in your current view. + +When you click the AI Triage button, the Grafana Assistant analyzes: + +- Alert state transitions over the selected time range +- Alert instance patterns and frequency +- Common labels and characteristics of firing alerts +- Temporal patterns in alert behavior + +The AI assistant can help you: + +- Identify root causes of alert storms +- Detect patterns in alert firing behavior +- Understand correlations between different alert instances +- Get suggestions for improving alert configurations + +To use the Analyze with Assistant feature: + +1. Navigate to the History page as described above. +2. Filter the events to focus on the alerts you want to analyze using labels, states, or time range. +3. Click the **Analyze with Assistant** button in the top-right corner of the event list. +4. Review the AI-generated analysis and recommendations. + +The AI analysis is based on the currently displayed events, so filtering your view to specific alerts or time periods will result in more focused insights. + ## View from the State history view Use the State history view to get insight into how your individual alert instances behave over time. From 42babc7ce78e7f38dc8d70ef0a4184ea702822d5 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 21 Nov 2025 12:14:50 +0200 Subject: [PATCH 023/423] Select: Fix width bug when `maxVisibleValues` is set (#113913) * fix * fix --- packages/grafana-ui/src/components/Select/ValueContainer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/Select/ValueContainer.tsx b/packages/grafana-ui/src/components/Select/ValueContainer.tsx index aa5cec83c19..6eb8326ae5d 100644 --- a/packages/grafana-ui/src/components/Select/ValueContainer.tsx +++ b/packages/grafana-ui/src/components/Select/ValueContainer.tsx @@ -26,6 +26,7 @@ class UnthemedValueContainer Date: Fri, 21 Nov 2025 04:40:29 -0600 Subject: [PATCH 024/423] Loki: fix status in body not matching http status code (#114201) * fix: status in body not matching http status code --- pkg/tsdb/loki/api.go | 6 ++++- pkg/tsdb/loki/api_test.go | 49 ++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index 464a267391a..eb7f72e6570 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -199,6 +199,7 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery, responseOpts res := backend.DataResponse{ Error: err, ErrorSource: backend.ErrorSourceFromHTTPStatus(resp.StatusCode), + Status: backend.Status(resp.StatusCode), } lp = append(lp, "status", "error", "error", err, "statusSource", res.ErrorSource) api.log.Debug("Error received from Loki", lp...) @@ -214,6 +215,7 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery, responseOpts iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) res := converter.ReadPrometheusStyleResult(iter, converter.Options{}) + res.Status = backend.Status(resp.StatusCode) if res.Error != nil { span.RecordError(res.Error) @@ -305,7 +307,9 @@ func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) (RawLokiR } body, err = json.Marshal(lokiResponseErr) if err != nil { - return RawLokiResponse{}, err + return RawLokiResponse{ + Status: resp.StatusCode, + }, err } } diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 252608780e6..29b15e7fb18 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -250,6 +250,7 @@ func TestApiReturnValues(t *testing.T) { require.True(t, called) require.Equal(t, "gzip", encodedBytes.Encoding) require.Equal(t, []byte("{\"message\":\"foo\"}"), encodedBytes.Body) + require.Equal(t, 400, encodedBytes.Status) }) t.Run("Loki should return the error as is", func(t *testing.T) { @@ -280,6 +281,18 @@ func TestApiReturnValues(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "foo") }) + + t.Run("should set status for successful requests", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", []byte("{\"message\":\"foo\"}"), func(req *http.Request) { + called = true + }) + + res, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsVolume, QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + require.Equal(t, backend.Status(http.StatusOK), res.Status) + }) } func TestErrorSources(t *testing.T) { @@ -287,7 +300,7 @@ func TestErrorSources(t *testing.T) { t.Run("should set correct error source for downstream errors", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -296,11 +309,12 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusBadRequest), res.Status) }) t.Run("should set correct error source for plugin errors", func(t *testing.T) { called := false - api := makeMockedAPI(406, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusNotAcceptable, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -309,11 +323,12 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourcePlugin, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusNotAcceptable), res.Status) }) t.Run("should set correct error source for server errors", func(t *testing.T) { called := false - api := makeMockedAPI(500, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusInternalServerError, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -322,11 +337,26 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusInternalServerError), res.Status) + }) + + t.Run("should set correct error source for server timeout error", func(t *testing.T) { + called := false + api := makeMockedAPI(http.StatusGatewayTimeout, "application/json", errorResponse, func(req *http.Request) { + called = true + }) + + res, err := api.DataQuery(context.Background(), lokiQuery{QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + require.NotNil(t, res.Error) + require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusGatewayTimeout), res.Status) }) t.Run("should handle downstream HTTP errors", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -336,30 +366,33 @@ func TestErrorSources(t *testing.T) { require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) require.Contains(t, res.Error.Error(), "test error") + require.Equal(t, backend.Status(http.StatusBadRequest), res.Status) }) t.Run("should handle client errors in RawQuery", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) res, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") require.NoError(t, err) require.True(t, called) - require.Equal(t, 400, res.Status) + require.Equal(t, http.StatusBadRequest, res.Status) require.Contains(t, string(res.Body), "test error") }) t.Run("should handle server errors in RawQuery", func(t *testing.T) { called := false - api := makeMockedAPI(500, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusInternalServerError, "application/json", errorResponse, func(req *http.Request) { called = true }) - _, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") + res, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") require.Error(t, err) require.True(t, called) require.Contains(t, err.Error(), "test error") + // Status code of 0 gets mapped to InternalServerError (500) + require.Equal(t, 0, res.Status) }) } From e09905df35d0d163ae8597440ace156d8c636ce9 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 21 Nov 2025 11:41:03 +0100 Subject: [PATCH 025/423] SchemaV2: Add library panel repeat options to v2 schema during conversion (#114109) * Add library panel repeat options to v2 schema during conversion * use any instead of interface{} * change to common.Unstructured instead of byte[] for model field * Fix the tests and let the library panel behavior fetch repeat options in public and scripted dashboards * fix library panel differences between backend and frontend conversion --- .../pkg/migration/conversion/conversion.go | 12 +- .../migration/conversion/conversion_test.go | 27 ++- .../v1beta1.library-panel-repeat-options.json | 93 ++++++++++ ...library-panel-repeat-options.v0alpha1.json | 102 +++++++++++ ...library-panel-repeat-options.v2alpha1.json | 169 ++++++++++++++++++ ....library-panel-repeat-options.v2beta1.json | 169 ++++++++++++++++++ apps/dashboard/pkg/migration/conversion/v0.go | 8 +- .../pkg/migration/conversion/v0_test.go | 18 +- apps/dashboard/pkg/migration/conversion/v1.go | 8 +- .../pkg/migration/conversion/v1_test.go | 8 +- .../conversion/v1beta1_to_v2alpha1.go | 108 +++++++---- .../schemaversion/datasource_utils.go | 57 ++++++ .../pkg/migration/schemaversion/migrations.go | 3 + .../dashboard/pkg/migration/testutil/mocks.go | 99 ++++++++++ pkg/registry/apis/dashboard/datasources.go | 10 ++ .../scene/LibraryPanelBehavior.tsx | 17 +- .../serialization/serialization-test-utils.ts | 96 ++++++++++ .../transformSaveModelV1ToV2.test.ts | 23 ++- .../api/ResponseTransformersToBackend.test.ts | 8 +- 19 files changed, 962 insertions(+), 73 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json create mode 100644 public/app/features/dashboard-scene/serialization/serialization-test-utils.ts diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index 8d20dcd3837..7b614bc2656 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -11,11 +11,13 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, _ schemaversion.LibraryElementIndexProvider) error { +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), @@ -26,13 +28,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider) + return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider) + return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } @@ -46,13 +48,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider) + return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider) + return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index e7cbe2f70ef..5f334b98354 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -33,7 +33,8 @@ import ( func TestConversionMatrixExist(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) versions := []metav1.Object{ @@ -86,7 +87,8 @@ func TestDeepCopyValid(t *testing.T) { func TestDashboardConversionToAllVersions(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Set up conversion scheme @@ -246,7 +248,8 @@ func TestDashboardConversionToAllVersions(t *testing.T) { func TestMigratedDashboardsConversion(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Set up conversion scheme @@ -381,7 +384,8 @@ func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputD func TestConversionMetrics(t *testing.T) { // Initialize migration with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -509,7 +513,8 @@ func TestConversionMetrics(t *testing.T) { // TestConversionMetricsWrapper tests the withConversionMetrics wrapper function func TestConversionMetricsWrapper(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -678,7 +683,8 @@ func TestSchemaVersionExtraction(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Test the schema version extraction logic by creating a wrapper and checking the metrics labels dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -723,7 +729,8 @@ func TestSchemaVersionExtraction(t *testing.T) { // TestConversionLogging tests that conversion-level logging works correctly func TestConversionLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -815,7 +822,8 @@ func TestConversionLogging(t *testing.T) { // TestConversionLogLevels tests that appropriate log levels are used func TestConversionLogLevels(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) t.Run("log levels and structured fields verification", func(t *testing.T) { @@ -887,7 +895,8 @@ func TestConversionLogLevels(t *testing.T) { // TestConversionLoggingFields tests that all expected fields are included in log messages func TestConversionLoggingFields(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) t.Run("verify all log fields are present", func(t *testing.T) { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json new file mode 100644 index 00000000000..ec440d2894b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json @@ -0,0 +1,93 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "title": "Library Panel Repeat Options Test Dashboard", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "tags": ["test", "library-panels", "repeat"], + "schemaVersion": 38, + "panels": [ + { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 0, + "w": 12, + "h": 8 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat" + } + }, + { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 8, + "w": 6, + "h": 4 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-v", + "name": "Library Panel with Vertical Repeat" + } + }, + { + "id": 3, + "title": "Library Panel Instance Override", + "type": "library-panel-ref", + "gridPos": { + "x": 6, + "y": 8, + "w": 12, + "h": 8 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat" + }, + "repeat": "instance-var", + "repeatDirection": "v", + "maxPerRow": 5 + }, + { + "id": 4, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 12, + "w": 6, + "h": 3 + }, + "libraryPanel": { + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat" + } + } + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [] + } +} + diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json new file mode 100644 index 00000000000..4c9f4fc2eaf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json @@ -0,0 +1,102 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": { + "list": [] + }, + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + }, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 2, + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + }, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 6, + "y": 8 + }, + "id": 3, + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + }, + "maxPerRow": 5, + "repeat": "instance-var", + "repeatDirection": "v", + "title": "Library Panel Instance Override", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 12 + }, + "id": 4, + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + }, + "title": "Library Panel without Repeat", + "type": "library-panel-ref" + } + ], + "schemaVersion": 38, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Library Panel Repeat Options Test Dashboard" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json new file mode 100644 index 00000000000..a614f32dda6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json @@ -0,0 +1,169 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "editable": true, + "elements": { + "panel-1": { + "kind": "LibraryPanel", + "spec": { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + } + } + }, + "panel-3": { + "kind": "LibraryPanel", + "spec": { + "id": 3, + "title": "Library Panel Instance Override", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-4": { + "kind": "LibraryPanel", + "spec": { + "id": 4, + "title": "Library Panel without Repeat", + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "repeat": { + "mode": "variable", + "value": "server", + "direction": "h", + "maxPerRow": 3 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "repeat": { + "mode": "variable", + "value": "datacenter", + "direction": "v" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "repeat": { + "mode": "variable", + "value": "instance-var", + "direction": "v", + "maxPerRow": 5 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Library Panel Repeat Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json new file mode 100644 index 00000000000..d06d12848e6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json @@ -0,0 +1,169 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "editable": true, + "elements": { + "panel-1": { + "kind": "LibraryPanel", + "spec": { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + } + } + }, + "panel-3": { + "kind": "LibraryPanel", + "spec": { + "id": 3, + "title": "Library Panel Instance Override", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-4": { + "kind": "LibraryPanel", + "spec": { + "id": 4, + "title": "Library Panel without Repeat", + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "repeat": { + "mode": "variable", + "value": "server", + "direction": "h", + "maxPerRow": 3 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "repeat": { + "mode": "variable", + "value": "datacenter", + "direction": "v" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "repeat": { + "mode": "variable", + "value": "instance-var", + "direction": "v", + "maxPerRow": 5 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Library Panel Repeat Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index e182a71a52a..826293d9564 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -25,7 +25,7 @@ func Convert_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope co return nil } -func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { out.Status = dashv2alpha1.DashboardStatus{ @@ -48,7 +48,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s return nil } - if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -72,7 +72,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s return nil } -func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { out.Status = dashv2beta1.DashboardStatus{ @@ -86,7 +86,7 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco } v2alpha1 := &dashv2alpha1.Dashboard{} - if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), diff --git a/apps/dashboard/pkg/migration/conversion/v0_test.go b/apps/dashboard/pkg/migration/conversion/v0_test.go index 08bf1c550c9..ca2210f01e8 100644 --- a/apps/dashboard/pkg/migration/conversion/v0_test.go +++ b/apps/dashboard/pkg/migration/conversion/v0_test.go @@ -109,9 +109,9 @@ func TestV0ConversionErrorHandling(t *testing.T) { case *dashv1.Dashboard: err = Convert_V0_to_V1beta1(tt.source, target, nil) case *dashv2alpha1.Dashboard: - err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider) + err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider, leProvider) case *dashv2beta1.Dashboard: - err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider) + err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider, leProvider) default: t.Fatalf("unexpected target type: %T", target) } @@ -192,7 +192,7 @@ func TestV0ConversionErrorPropagation(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) require.Error(t, err, "expected error to be returned on first step failure") require.NotNil(t, target.Status.Conversion) @@ -243,7 +243,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) require.NoError(t, err, "expected successful conversion") // Layout should be set even on success @@ -264,7 +264,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) require.NoError(t, err, "expected successful conversion") }) @@ -293,7 +293,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V0_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") @@ -327,7 +327,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V0_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") @@ -357,7 +357,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on second step, status should be set @@ -383,7 +383,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on third step, status should be set diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index 48fdbf93b36..040a9a78741 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -25,8 +25,8 @@ func Convert_V1beta1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope co return nil } -func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { - if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider); err != nil { +func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), @@ -60,9 +60,9 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa return nil } -func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v2alpha1 := &dashv2alpha1.Dashboard{} - if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), diff --git a/apps/dashboard/pkg/migration/conversion/v1_test.go b/apps/dashboard/pkg/migration/conversion/v1_test.go index c0a9d2e2e6b..4525e974e01 100644 --- a/apps/dashboard/pkg/migration/conversion/v1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1_test.go @@ -37,7 +37,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V1beta1_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V1beta1_to_V2alpha1 doesn't return error") @@ -64,7 +64,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on first step, status should be set with correct StoredVersion @@ -91,7 +91,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on second step, status should be set with correct StoredVersion @@ -117,7 +117,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // Should succeed if dashboard is valid if err == nil { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 21877cb6361..d20855a9509 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -80,7 +80,7 @@ func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsIndexProvider schem return ctx, &nsInfo, nil } -func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv2alpha1.APIVERSION out.Kind = in.Kind @@ -94,10 +94,10 @@ func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha return fmt.Errorf("failed to prepare conversion context: %w", err) } - return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider) + return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider, leIndexProvider) } -func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { // Parse the unstructured spec into a dashboard JSON structure dashboardJSON, ok := in.Object["dashboard"] if !ok { @@ -161,7 +161,7 @@ func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *das out.Links = transformLinks(dashboard) // Transform panels to elements and layout - elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider) + elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider, leIndexProvider) if err != nil { return fmt.Errorf("failed to transform panels: %w", err) } @@ -387,7 +387,7 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa // Panel transformation constants const GRID_ROW_HEIGHT = 1 -func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { panels, ok := dashboard["panels"].([]interface{}) if !ok { // Return empty elements and default grid layout @@ -415,13 +415,13 @@ func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[strin } if hasRowPanels { - return convertToRowsLayout(ctx, panels, dsIndexProvider) + return convertToRowsLayout(ctx, panels, dsIndexProvider, leIndexProvider) } - return convertToGridLayout(ctx, panels, dsIndexProvider) + return convertToGridLayout(ctx, panels, dsIndexProvider, leIndexProvider) } -func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { elements := make(map[string]dashv2alpha1.DashboardElement) items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels)) @@ -437,7 +437,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi } elements[elementName] = element - items = append(items, buildGridItemKind(panelMap, elementName, nil)) + items = append(items, buildGridItemKind(ctx, panelMap, elementName, nil, leIndexProvider)) } layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ @@ -452,7 +452,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi return elements, layout, nil } -func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { elements := make(map[string]dashv2alpha1.DashboardElement) rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0) @@ -491,7 +491,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi element, name, err := buildElement(ctx, collapsedPanelMap, dsIndexProvider) if err == nil { elements[name] = element - rowElements = append(rowElements, buildGridItemKind(collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)))) + rowElements = append(rowElements, buildGridItemKind(ctx, collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)), leIndexProvider)) } } } @@ -512,7 +512,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi if currentRow.Spec.Layout.GridLayoutKind != nil { currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append( currentRow.Spec.Layout.GridLayoutKind.Spec.Items, - buildGridItemKind(panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY))), + buildGridItemKind(ctx, panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY)), leIndexProvider), ) } } else { @@ -521,7 +521,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi // The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row. legacyRowY = -1 gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{ - buildGridItemKind(panelMap, elementName, int64Ptr(0)), + buildGridItemKind(ctx, panelMap, elementName, int64Ptr(0), leIndexProvider), } hideHeader := true @@ -645,7 +645,7 @@ func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsInde return panelKind, nil } -func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOverride *int64) dashv2alpha1.DashboardGridLayoutItemKind { +func buildGridItemKind(ctx context.Context, panelMap map[string]interface{}, elementName string, yOverride *int64, leIndexProvider schemaversion.LibraryElementIndexProvider) dashv2alpha1.DashboardGridLayoutItemKind { // Default grid position (matches frontend PanelModel defaults: w=6, h=3) x, y, width, height := int64(0), int64(0), int64(6), int64(3) @@ -677,34 +677,78 @@ func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOve } // Handle repeat options - if repeat := schemaversion.GetStringValue(panelMap, "repeat"); repeat != "" { - repeatOptions := &dashv2alpha1.DashboardRepeatOptions{ - Mode: "variable", - Value: repeat, - } + // First check if repeat options are set on the panel itself (dashboard instance level) + repeatOptions := getRepeatOptionsFromPanel(panelMap) - if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" { - switch repeatDirection { - case "h": - direction := dashv2alpha1.DashboardRepeatOptionsDirectionH - repeatOptions.Direction = &direction - case "v": - direction := dashv2alpha1.DashboardRepeatOptionsDirectionV - repeatOptions.Direction = &direction + // If no repeat options on the panel and it's a library panel, try to get them from the library panel definition + if repeatOptions == nil { + if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok { + libraryPanelUID := schemaversion.GetStringValue(libraryPanel, "uid") + if libraryPanelUID != "" && leIndexProvider != nil { + repeatOptions = getRepeatOptionsFromLibraryPanel(ctx, libraryPanelUID, leIndexProvider) } } + } - if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 { - maxPerRowInt64 := int64(maxPerRow) - repeatOptions.MaxPerRow = &maxPerRowInt64 - } - + if repeatOptions != nil { item.Spec.Repeat = repeatOptions } return item } +// getRepeatOptionsFromPanel extracts repeat options from a panel map (dashboard instance level) +func getRepeatOptionsFromPanel(panelMap map[string]any) *dashv2alpha1.DashboardRepeatOptions { + repeat := schemaversion.GetStringValue(panelMap, "repeat") + if repeat == "" { + return nil + } + + repeatOptions := &dashv2alpha1.DashboardRepeatOptions{ + Mode: "variable", + Value: repeat, + } + + if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" { + switch repeatDirection { + case "h": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionH + repeatOptions.Direction = &direction + case "v": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionV + repeatOptions.Direction = &direction + } + } + + if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 { + maxPerRowInt64 := int64(maxPerRow) + repeatOptions.MaxPerRow = &maxPerRowInt64 + } + + return repeatOptions +} + +// getRepeatOptionsFromLibraryPanel retrieves repeat options from a library panel by UID +func getRepeatOptionsFromLibraryPanel(ctx context.Context, libraryPanelUID string, leIndexProvider schemaversion.LibraryElementIndexProvider) *dashv2alpha1.DashboardRepeatOptions { + libraryElements := leIndexProvider.GetLibraryElementInfo(ctx) + + // Find the library panel by UID + var libraryPanelModel map[string]any + for _, elem := range libraryElements { + if elem.UID == libraryPanelUID { + libraryPanelModel = elem.Model.Object + break + } + } + + if libraryPanelModel == nil { + return nil + } + + // Extract repeat options from the library panel model + return getRepeatOptionsFromPanel(libraryPanelModel) +} + func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind { collapsed := getBoolField(rowPanelMap, "collapsed", false) title := schemaversion.GetStringValue(rowPanelMap, "title") diff --git a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go index cc4e388bca3..18bc2fc17c4 100644 --- a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go @@ -216,3 +216,60 @@ func MigrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool, return nil } + +// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. +// This prevents multiple DB queries during operations that may call GetLibraryElementInfo() +// multiple times (e.g., dashboard conversions with many library panel lookups). +// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton +// while still refreshing periodically. +// +// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. +type cachedLibraryElementProvider struct { + provider LibraryElementIndexProvider + mu sync.RWMutex + elements []LibraryElementInfo + cachedAt time.Time + cacheTTL time.Duration +} + +// GetLibraryElementInfo returns the cached library elements if they're still valid (< 10s old), otherwise rebuilds the cache. +// Uses RWMutex for efficient concurrent reads when cache is valid. +func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { + // Fast path: check if cache is still valid using read lock + p.mu.RLock() + if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { + elements := p.elements + p.mu.RUnlock() + return elements + } + p.mu.RUnlock() + + // Slow path: cache expired or not yet built, acquire write lock + p.mu.Lock() + defer p.mu.Unlock() + + // Double-check: another goroutine might have refreshed the cache + // while we were waiting for the write lock + if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { + return p.elements + } + + // Rebuild the cache + p.elements = p.provider.GetLibraryElementInfo(ctx) + p.cachedAt = time.Now() + return p.elements +} + +// WrapLibraryElementProviderWithCache wraps a provider to cache library elements with a 10-second TTL. +// Useful for conversions or migrations that may call GetLibraryElementInfo() multiple times. +// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton +// at the top level of dependency injection while still refreshing periodically. +func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider) LibraryElementIndexProvider { + if provider == nil { + return nil + } + return &cachedLibraryElementProvider{ + provider: provider, + cacheTTL: 10 * time.Second, + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index 3e55b25b55c..ce786fa0dc0 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -3,6 +3,8 @@ package schemaversion import ( "context" "strconv" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( @@ -34,6 +36,7 @@ type LibraryElementInfo struct { Type string Description string FolderUID string + Model common.Unstructured // JSON model of the library element, used to extract repeat options during migration } type LibraryElementIndexProvider interface { diff --git a/apps/dashboard/pkg/migration/testutil/mocks.go b/apps/dashboard/pkg/migration/testutil/mocks.go index 6c8b4709137..6781dcecfa1 100644 --- a/apps/dashboard/pkg/migration/testutil/mocks.go +++ b/apps/dashboard/pkg/migration/testutil/mocks.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // EmptyLibraryElementProvider provides an empty library element list for tests @@ -187,3 +188,101 @@ func (p *ConfigurableDataSourceProvider) getDevDashboardDataSources() []schemave }, } } + +// TestLibraryElementProvider provides library elements with models for testing repeat options migration +type TestLibraryElementProvider struct { + elements []schemaversion.LibraryElementInfo +} + +// NewTestLibraryElementProvider creates a new test library element provider with sample library panels +func NewTestLibraryElementProvider() *TestLibraryElementProvider { + // Create library panel models with repeat options + libPanelWithRepeatH := map[string]any{ + "id": 1, + "type": "timeseries", + "title": "Library Panel with Horizontal Repeat", + "repeat": "server", + "repeatDirection": "h", + "maxPerRow": 3, + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 12, + "h": 8, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + libPanelWithRepeatV := map[string]any{ + "id": 2, + "type": "stat", + "title": "Library Panel with Vertical Repeat", + "repeat": "datacenter", + "repeatDirection": "v", + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 6, + "h": 4, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + libPanelWithoutRepeat := map[string]any{ + "id": 3, + "type": "text", + "title": "Library Panel without Repeat", + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 6, + "h": 3, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + // Convert models to Unstructured + modelWithRepeatH := v0alpha1.Unstructured{Object: libPanelWithRepeatH} + modelWithRepeatV := v0alpha1.Unstructured{Object: libPanelWithRepeatV} + modelWithoutRepeat := v0alpha1.Unstructured{Object: libPanelWithoutRepeat} + + return &TestLibraryElementProvider{ + elements: []schemaversion.LibraryElementInfo{ + { + UID: "lib-panel-repeat-h", + Name: "Library Panel with Horizontal Repeat", + Kind: 1, // Panel element + Type: "timeseries", + Description: "A library panel with horizontal repeat options", + FolderUID: "", + Model: modelWithRepeatH, + }, + { + UID: "lib-panel-repeat-v", + Name: "Library Panel with Vertical Repeat", + Kind: 1, // Panel element + Type: "stat", + Description: "A library panel with vertical repeat options", + FolderUID: "", + Model: modelWithRepeatV, + }, + { + UID: "lib-panel-no-repeat", + Name: "Library Panel without Repeat", + Kind: 1, // Panel element + Type: "text", + Description: "A library panel without repeat options", + FolderUID: "", + Model: modelWithoutRepeat, + }, + }, + } +} + +// GetLibraryElementInfo returns the test library elements +func (p *TestLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + return p.elements +} diff --git a/pkg/registry/apis/dashboard/datasources.go b/pkg/registry/apis/dashboard/datasources.go index 742dc76aad8..003a2c0de23 100644 --- a/pkg/registry/apis/dashboard/datasources.go +++ b/pkg/registry/apis/dashboard/datasources.go @@ -2,8 +2,10 @@ package dashboard import ( "context" + "encoding/json" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" @@ -112,6 +114,13 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context) } for _, elem := range result.Elements { + var modelUnstructured v0alpha1.Unstructured + if len(elem.Model) > 0 { + var modelObj map[string]any + if err := json.Unmarshal(elem.Model, &modelObj); err == nil { + modelUnstructured.Object = modelObj + } + } info = append(info, schemaversion.LibraryElementInfo{ UID: elem.UID, Name: elem.Name, @@ -119,6 +128,7 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context) Type: elem.Type, Description: elem.Description, FolderUID: elem.FolderUID, + Model: modelUnstructured, }) } diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 74b78bbc323..86b83bd0ed8 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -8,7 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; -import { getPanelIdForVizPanel } from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelLinksBehavior } from './PanelMenuBehavior'; @@ -99,8 +99,19 @@ export class LibraryPanelBehavior extends SceneObjectBase +): DashboardV2Spec { + // Deep clone the spec to avoid mutating the original + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const normalized = JSON.parse(JSON.stringify(backendSpec)) as DashboardV2Spec; + + // Create a map of panel ID to whether it has explicit repeat, original width, and if it's a library panel + const panelHasExplicitRepeat = new Map(); + const panelOriginalWidth = new Map(); + const panelIsLibraryPanel = new Map(); + inputPanels.forEach((panel) => { + if (panel.id !== undefined) { + panelHasExplicitRepeat.set(panel.id, !!panel.repeat); + panelIsLibraryPanel.set(panel.id, !!panel.libraryPanel); + if (panel.gridPos?.w !== undefined) { + panelOriginalWidth.set(panel.id, panel.gridPos.w); + } + } + }); + + // Helper to recursively process grid items + function processGridItems(items: GridLayoutItemKind[]): void { + if (!Array.isArray(items)) { + return; + } + + items.forEach((item) => { + if (item.spec?.element?.name) { + // Extract panel ID from element name (format: "panel-{id}") + const match = item.spec.element.name.match(/^panel-(\d+)$/); + if (match) { + const panelId = parseInt(match[1], 10); + const hasExplicitRepeat = panelHasExplicitRepeat.get(panelId); + const isLibraryPanel = panelIsLibraryPanel.get(panelId); + + // Only normalize library panels - check both input panel and element kind + const element = normalized.elements?.[item.spec.element.name]; + const isElementLibraryPanel = element?.kind === 'LibraryPanel'; + + // If this is a library panel item and repeat wasn't explicitly set on the instance, + // remove the repeat property (backend adds it from library panel definition) + // Also restore the original width when removing repeat properties + if ((isLibraryPanel || isElementLibraryPanel) && hasExplicitRepeat === false && item.spec.repeat) { + delete item.spec.repeat; + // Always restore the original width from the input panel + const originalWidth = panelOriginalWidth.get(panelId); + if (originalWidth !== undefined) { + item.spec.width = originalWidth; + } + } + } + } + }); + } + + // Process GridLayout items + if (normalized.layout?.kind === 'GridLayout' && normalized.layout.spec?.items) { + processGridItems(normalized.layout.spec.items); + } + + // Process RowsLayout items + if (normalized.layout?.kind === 'RowsLayout' && normalized.layout.spec?.rows) { + normalized.layout.spec.rows.forEach((row: RowsLayoutRowKind) => { + if (row.spec?.layout?.kind === 'GridLayout' && row.spec.layout.spec?.items) { + processGridItems(row.spec.layout.spec.items); + } + }); + } + + return normalized; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts index 9838f889a6d..9dbf8e36f0a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts @@ -1,6 +1,7 @@ import { readdirSync, readFileSync } from 'fs'; import path from 'path'; +import { normalizeBackendOutputForFrontendComparison } from './serialization-test-utils'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformSaveModelToScene } from './transformSaveModelToScene'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; @@ -229,8 +230,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { expect(frontendOutput).toBeDefined(); expect(backendOutputAfterLoadedByScene).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + // For migrated dashboards, panels are in the root level, not in spec.panels + const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; + const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( + backendOutputAfterLoadedByScene, + inputPanels + ); + // Compare only the spec structures - this is the core transformation - expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(frontendOutput); }); }); @@ -292,8 +302,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { expect(frontendOutput).toBeDefined(); expect(backendOutputAfterLoadedByScene).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + // For migrated dashboards, panels are in the root level, not in spec.panels + const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; + const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( + backendOutputAfterLoadedByScene, + inputPanels + ); + // Compare only the spec structures - this is the core transformation - expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(frontendOutput); }); }); }); diff --git a/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts b/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts index d818e4953a3..45a310403e4 100644 --- a/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts @@ -3,6 +3,7 @@ import path from 'path'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; +import { normalizeBackendOutputForFrontendComparison } from 'app/features/dashboard-scene/serialization/serialization-test-utils'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ensureV2Response } from './ResponseTransformers'; @@ -205,8 +206,13 @@ describe('Backend / Frontend result comparison', () => { expect(frontendOutput.spec).toBeDefined(); expect(backendOutput.spec).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + const inputPanels = jsonInput.spec?.panels || []; + const normalizedBackendSpec = normalizeBackendOutputForFrontendComparison(backendOutput.spec, inputPanels); + // Compare the spec structures - expect(backendOutput.spec).toEqual(frontendOutput.spec); + expect(normalizedBackendSpec).toEqual(frontendOutput.spec); // Verify the conversion doesn't throw errors and produces a valid structure expect(() => JSON.stringify(frontendOutput)).not.toThrow(); From b17ba6677e3cadb3a4d9f57f247209092b512600 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 21 Nov 2025 11:51:56 +0100 Subject: [PATCH 026/423] Logs: Feature flag logsInfiniteScrolling removed (#113585) * Remove feature flag definition * Remove feature flag from code * Update imports * LogsNavigationPages: remove deprecated component * Finally deprecate navigation pages * Update tests * Update translations * Remove unused code * More cleanup * Test cleanup * Remove deprecated props * More props removal * Update feature flags * Revert changes --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 5 - pkg/services/featuremgmt/registry.go | 8 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.json | 3 +- public/app/core/utils/shortLinks.test.ts | 17 -- public/app/core/utils/shortLinks.ts | 8 - public/app/features/explore/Explore.tsx | 1 - .../app/features/explore/Logs/Logs.test.tsx | 62 +---- public/app/features/explore/Logs/Logs.tsx | 86 ++----- .../features/explore/Logs/LogsContainer.tsx | 23 +- .../explore/Logs/LogsNavigation.test.tsx | 138 +---------- .../features/explore/Logs/LogsNavigation.tsx | 224 +----------------- .../explore/Logs/LogsNavigationPages.test.tsx | 54 ----- .../explore/Logs/LogsNavigationPages.tsx | 113 --------- .../logs/components/ControlledLogRows.tsx | 3 +- .../logs/components/InfiniteScroll.test.tsx | 9 - .../logs/components/InfiniteScroll.tsx | 4 +- .../components/panel/InfiniteScroll.test.tsx | 9 - .../logs/components/panel/InfiniteScroll.tsx | 4 +- public/app/features/logs/logsModel.test.ts | 17 +- public/app/features/logs/logsModel.ts | 4 +- public/app/plugins/panel/logs/LogsPanel.tsx | 2 +- public/locales/en-US/grafana.json | 6 +- 24 files changed, 47 insertions(+), 755 deletions(-) delete mode 100644 public/app/features/explore/Logs/LogsNavigationPages.test.tsx delete mode 100644 public/app/features/explore/Logs/LogsNavigationPages.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a4eca4f9ee7..ce5adca782c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -41,7 +41,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | | `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | -| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | | `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e5c59000c8d..5e9ad9ff8d4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -381,11 +381,6 @@ export interface FeatureToggles { */ timeComparison?: boolean; /** - * Enables infinite scrolling for the Logs panel in Explore and Dashboards - * @default true - */ - logsInfiniteScrolling?: boolean; - /** * Enables shared crosshair in table panel */ tableSharedCrosshair?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 28ff9e1809e..a557e32087b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -615,14 +615,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, - { - Name: "logsInfiniteScrolling", - Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards", - Stage: FeatureStageGeneralAvailability, - Expression: "true", - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - }, { Name: "tableSharedCrosshair", Description: "Enables shared crosshair in table panel", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4ee6e57a9ed..99753233108 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -85,7 +85,6 @@ panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true timeComparison,experimental,@grafana/dataviz-squad,false,false,true -logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bb0e03295a0..d60ad45f64e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2545,7 +2545,8 @@ "metadata": { "name": "logsInfiniteScrolling", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-09T10:54:03Z" + "creationTimestamp": "2023-11-09T10:54:03Z", + "deletionTimestamp": "2025-11-07T10:59:01Z" }, "spec": { "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 39a43af6629..f88f302b4db 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -164,7 +164,6 @@ describe('buildShortUrl', () => { describe('getLogsPermalinkRange', () => { let row: LogRowModel, rows: LogRowModel[]; beforeEach(() => { - config.featureToggles.logsInfiniteScrolling = true; row = createLogRow({ timeEpochMs: 1111112222222, }); @@ -175,22 +174,6 @@ describe('getLogsPermalinkRange', () => { row, ]; }); - afterAll(() => { - config.featureToggles.logsInfiniteScrolling = false; - }); - - it('returns the original range if infinite scrolling is not enabled', () => { - config.featureToggles.logsInfiniteScrolling = false; - const range = { - from: 1111111111111, - to: 1111112222222, - }; - const expectedRange = { - from: new Date(1111111111111).toISOString(), - to: new Date(1111112222222).toISOString(), - }; - expect(getLogsPermalinkRange(row, [row], range)).toEqual(expectedRange); - }); it('returns the range relative to the previous log line', () => { const range = { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 706857c122c..208f2ae0fcc 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -170,14 +170,6 @@ function getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel | } export function getLogsPermalinkRange(row: LogRowModel, rows: LogRowModel[], absoluteRange: AbsoluteTimeRange) { - const range = { - from: new Date(absoluteRange.from).toISOString(), - to: new Date(absoluteRange.to).toISOString(), - }; - if (!config.featureToggles.logsInfiniteScrolling) { - return range; - } - // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust // Look for the previous sibling log, and use its timestamp const allLogs = rows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index f8bd275930b..79678f4e15a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -475,7 +475,6 @@ export class Explore extends PureComponent { onStopScanning={this.onStopScanning} eventBus={this.logsEventBus} splitOpenFn={this.splitOpenFnLogs} - scrollElement={this.scrollElement} isFilterLabelActive={this.isFilterLabelActive} onClickFilterString={this.onClickFilterString} onClickFilterOutString={this.onClickFilterOutString} diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 958a8c589f8..93836280762 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -65,6 +65,8 @@ describe('Logs', () => { let originalHref = window.location.href; beforeEach(() => { + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + window.HTMLElement.prototype.scroll = jest.fn(); localStorage.clear(); jest.clearAllMocks(); }); @@ -128,9 +130,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -160,39 +160,6 @@ describe('Logs', () => { return { ...rendered, store: fakeStore }; }; - describe('scrolling behavior', () => { - let originalInnerHeight: number; - beforeEach(() => { - originalInnerHeight = window.innerHeight; - window.innerHeight = 1000; - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - window.HTMLElement.prototype.scroll = jest.fn(); - }); - afterEach(() => { - window.innerHeight = originalInnerHeight; - }); - - it('should call `scrollElement.scroll`', () => { - const logs = []; - for (let i = 0; i < 50; i++) { - logs.push(makeLog({ uid: `uid${i}`, rowId: `id${i}`, timeEpochMs: i })); - } - const scrollElementMock = { - scroll: jest.fn(), - scrollTop: 920, - }; - setup( - { scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState: { logs: { id: 'uid47' } } }, - undefined, - logs - ); - - // element.getBoundingClientRect().top will always be 0 for jsdom - // calc will be `scrollElement.scrollTop - window.innerHeight / 2` -> 920 - 500 = 420 - expect(scrollElementMock.scroll).toBeCalledWith({ behavior: 'smooth', top: 420 }); - }); - }); - it('should render logs', () => { setup(); const logsSection = screen.getByTestId('logRows'); @@ -246,9 +213,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -296,9 +261,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -349,9 +312,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -412,22 +373,6 @@ describe('Logs', () => { expect(fakeChangePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} }); }); - it('should scroll the scrollElement into view if rows contain id', () => { - const panelState = { logs: { id: '3' } }; - const scrollElementMock = { scroll: jest.fn() }; - setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); - - expect(scrollElementMock.scroll).toHaveBeenCalled(); - }); - - it('should not scroll the scrollElement into view if rows does not contain id', () => { - const panelState = { logs: { id: 'not-included' } }; - const scrollElementMock = { scroll: jest.fn() }; - setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); - - expect(scrollElementMock.scroll).not.toHaveBeenCalled(); - }); - it('should call reportInteraction on permalinkClick', async () => { const panelState = { logs: { id: 'not-included' } }; const rows = [ @@ -479,8 +424,6 @@ describe('Logs', () => { }); it('should call createAndCopyShortLink on permalinkClick - with infinite scrolling', async () => { - const featureToggleValue = config.featureToggles.logsInfiniteScrolling; - config.featureToggles.logsInfiniteScrolling = true; const rows = [ makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }), makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 1 }), @@ -503,7 +446,6 @@ describe('Logs', () => { ) ); expect(createAndCopyShortLink).toHaveBeenCalledWith(expect.stringMatching('visualisationType%22:%22logs')); - config.featureToggles.logsInfiniteScrolling = featureToggleValue; }); }); diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 7820e5eb9e9..055f4d454b7 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -119,11 +119,8 @@ interface Props extends Themeable2 { ) => Promise; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode; getFieldLinks: GetFieldLinksFn; - addResultsToCache: () => void; - clearCache: () => void; eventBus: EventBus; panelState?: ExplorePanelsState; - scrollElement?: HTMLDivElement; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; logsFrames?: DataFrame[]; range: TimeRange; @@ -183,8 +180,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { getFieldLinks, theme, logsQueries, - clearCache, - addResultsToCache, exploreId, getRowContext, getLogRowContextUi, @@ -193,7 +188,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { panelState, eventBus, onPinLineCallback, - scrollElement, } = props; const [showLabels, setShowLabels] = useState(store.getBool(SETTINGS_KEYS.showLabels, false)); const [showTime, setShowTime] = useState(store.getBool(SETTINGS_KEYS.showTime, true)); @@ -365,29 +359,17 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { [props.eventBus] ); - const scrollIntoView = useCallback( - (element: HTMLElement) => { - if (config.featureToggles.logsInfiniteScrolling) { - if (logsContainerRef.current) { - topLogsRef.current?.scrollIntoView(); - logsContainerRef.current.scroll({ - behavior: 'smooth', - top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } + const scrollIntoView = useCallback((element: HTMLElement) => { + if (logsContainerRef.current) { + topLogsRef.current?.scrollIntoView?.(); + logsContainerRef.current.scroll({ + behavior: 'smooth', + top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } - return; - } - - if (scrollElement) { - scrollElement.scroll({ - behavior: 'smooth', - top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } - }, - [scrollElement] - ); + return; + }, []); const sortOrderChanged = useCallback( (newSortOrder: LogsSortOrder) => { @@ -626,13 +608,11 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const scrollToTopLogs = useCallback(() => { - if (config.featureToggles.logsInfiniteScrolling) { - if (logsContainerRef.current) { - logsContainerRef.current.scroll({ - behavior: 'auto', - top: 0, - }); - } + if (logsContainerRef.current) { + logsContainerRef.current.scroll({ + behavior: 'auto', + top: 0, + }); } topLogsRef.current?.scrollIntoView(); }, []); @@ -684,7 +664,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]); - const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]); const infiniteScrollAvailable = useMemo( () => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan), [logsQueries] @@ -1060,11 +1039,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { visualisationType === 'logs' && hasData && ( <> -

+
= (props: Props) => { />
- + )} {config.featureToggles.newLogsPanel && visualisationType === 'logs' && ( @@ -1277,7 +1241,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n }), stickyNavigation: css({ overflow: 'visible', - ...(config.featureToggles.logsInfiniteScrolling && { marginBottom: '0px' }), + marginBottom: '0px', }), logsVolumePanel: css({ marginBottom: theme.spacing(1.5), @@ -1290,17 +1254,3 @@ const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); return { dedupedRows, dedupCount }; }; - -const createNavigationRange = (logRows: LogRowModel[]): { from: number; to: number } | undefined => { - if (!logRows || logRows.length === 0) { - return undefined; - } - const firstTimeStamp = logRows[0].timeEpochMs; - const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs; - - if (lastTimeStamp < firstTimeStamp) { - return { from: lastTimeStamp, to: firstTimeStamp }; - } - - return { from: firstTimeStamp, to: lastTimeStamp }; -}; diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 13bc78b5849..f93d66c6446 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -31,13 +31,7 @@ import { ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types/store'; import { getTimeZone } from '../../profile/state/selectors'; -import { - addResultsToCache, - clearCache, - loadSupplementaryQueryData, - selectIsWaitingForData, - setSupplementaryQueryEnabled, -} from '../state/query'; +import { loadSupplementaryQueryData, selectIsWaitingForData, setSupplementaryQueryEnabled } from '../state/query'; import { updateTimeRange, loadMoreLogs } from '../state/time'; import { LiveTailControls } from '../useLiveTailControls'; import { getFieldLinksForExplore } from '../utils/links'; @@ -58,7 +52,6 @@ interface LogsContainerProps extends PropsFromRedux { onStopScanning: () => void; eventBus: EventBus; splitOpenFn: SplitOpen; - scrollElement?: HTMLDivElement; isFilterLabelActive: (key: string, value: string, refId?: string) => Promise; onClickFilterString: (value: string, refId?: string) => void; onClickFilterOutString: (value: string, refId?: string) => void; @@ -260,14 +253,6 @@ class LogsContainer extends PureComponent { - this.props.addResultsToCache(this.props.exploreId); - }; - - clearCache = () => { - this.props.clearCache(this.props.exploreId); - }; - loadLogsVolumeData = () => { this.props.loadSupplementaryQueryData(this.props.exploreId, SupplementaryQueryType.LogsVolume); }; @@ -298,7 +283,6 @@ class LogsContainer extends PureComponent ({ type LogsNavigationProps = ComponentProps; const defaultProps: LogsNavigationProps = { - absoluteRange: { from: 1637319381811, to: 1637322981811 }, - timeZone: 'local', - queries: [], - loading: false, logsSortOrder: undefined, - visibleRange: { from: 1637322959000, to: 1637322981811 }, - onChangeTime: jest.fn(), scrollToTopLogs: jest.fn(), - addResultsToCache: jest.fn(), - clearCache: jest.fn(), }; const setup = (propOverrides?: Partial) => { @@ -37,132 +26,13 @@ const setup = (propOverrides?: Partial) => { }; describe('LogsNavigation', () => { - it('should always render 3 navigation buttons', () => { + it('should render scroll to top with default logs order', async () => { setup(); - expect(screen.getByTestId('newerLogsButton')).toBeInTheDocument(); - expect(screen.getByTestId('olderLogsButton')).toBeInTheDocument(); + expect(screen.getByTestId('scrollToTop')).toBeInTheDocument(); - }); - it('should render 3 navigation buttons in correct order when default logs order', () => { - const { container } = setup(); - const expectedOrder = ['newerLogsButton', 'olderLogsButton', 'scrollToTop']; - const elements = container.querySelectorAll( - '[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]' - ); - expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder); - }); + await userEvent.click(screen.getByTestId('scrollToTop')); - it('should render 3 navigation buttons in correct order when flipped logs order', () => { - const { container } = setup({ logsSortOrder: LogsSortOrder.Ascending }); - const expectedOrder = ['olderLogsButton', 'newerLogsButton', 'scrollToTop']; - const elements = container.querySelectorAll( - '[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]' - ); - expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder); - }); - - it('should disable fetch buttons when logs are loading', () => { - setup({ loading: true }); - const olderLogsButton = screen.getByTestId('olderLogsButton'); - const newerLogsButton = screen.getByTestId('newerLogsButton'); - expect(olderLogsButton).toBeDisabled(); - expect(newerLogsButton).toBeDisabled(); - }); - - it('should render logs navigation pages section', () => { - setup(); - expect(screen.getByTestId('logsNavigationPages')).toBeInTheDocument(); - }); - - it('should correctly request older logs when flipped order', async () => { - const onChangeTimeMock = jest.fn(); - const { rerender } = setup({ onChangeTime: onChangeTimeMock }); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319359000, to: 1637322959000 }); - - rerender( - - ); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319338000, to: 1637322938000 }); - }); - - it('should correctly display the active page', async () => { - const queries: DataQuery[] = []; - const { rerender } = setup({ - absoluteRange: { from: 1704737384139, to: 1704737684139 }, - visibleRange: { from: 1704737384207, to: 1704737683316 }, - queries, - logsSortOrder: LogsSortOrder.Descending, - }); - - expect(await screen.findByTestId('page1')).toBeInTheDocument(); - expect(screen.getByTestId('page1').firstChild).toHaveClass('selectedBg'); - - expect(screen.queryByTestId('page2')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByTestId('olderLogsButton')); - - rerender( - - ); - - expect(await screen.findByTestId('page1')).toBeInTheDocument(); - expect(screen.getByTestId('page1').firstChild).not.toHaveClass('selectedBg'); - - expect(await screen.findByTestId('page2')).toBeInTheDocument(); - expect(screen.getByTestId('page2').firstChild).toHaveClass('selectedBg'); - - expect(screen.queryByTestId('page3')).not.toBeInTheDocument(); - }); - - it('should reset the scroll when pagination is clicked', async () => { - const scrollToTopLogsMock = jest.fn(); - setup({ scrollToTopLogs: scrollToTopLogsMock }); - - expect(scrollToTopLogsMock).not.toHaveBeenCalled(); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(scrollToTopLogsMock).toHaveBeenCalled(); - }); - - it('should not trigger actions while loading', async () => { - const scrollToTopLogs = jest.fn(); - const changeTimeMock = jest.fn(); - setup({ scrollToTopLogs, onChangeTime: changeTimeMock, loading: true }); - - expect(scrollToTopLogs).not.toHaveBeenCalled(); - expect(changeTimeMock).not.toHaveBeenCalled(); - await userEvent.click(screen.getByTestId('olderLogsButton')); - await userEvent.click(screen.getByTestId('newerLogsButton')); - expect(scrollToTopLogs).not.toHaveBeenCalled(); - expect(changeTimeMock).not.toHaveBeenCalled(); - }); - - it('should not add results to cache unless pagination is used', async () => { - const addResultsToCache = jest.fn(); - setup({ addResultsToCache }); - - expect(addResultsToCache).not.toHaveBeenCalled(); - expect(screen.getByTestId('olderLogsButton')).not.toBeDisabled(); - expect(screen.getByTestId('newerLogsButton')).toBeDisabled(); - - await userEvent.click(screen.getByTestId('olderLogsButton')); - await userEvent.click(screen.getByTestId('newerLogsButton')); - - expect(addResultsToCache).toHaveBeenCalledTimes(1); + expect(defaultProps.scrollToTopLogs).toHaveBeenCalledTimes(1); }); }); diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx index 47a092b7138..b9b1c0c2c97 100644 --- a/public/app/features/explore/Logs/LogsNavigation.tsx +++ b/public/app/features/explore/Logs/LogsNavigation.tsx @@ -1,220 +1,30 @@ import { css } from '@emotion/css'; -import { isEqual } from 'lodash'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { memo, useCallback } from 'react'; -import { AbsoluteTimeRange, GrafanaTheme2, LogsSortOrder } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; +import { GrafanaTheme2, LogsSortOrder } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; -import { DataQuery, TimeZone } from '@grafana/schema'; -import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui'; +import { Button, Icon, useTheme2 } from '@grafana/ui'; import { getChromeHeaderLevelHeight } from 'app/core/components/AppChrome/TopBar/useChromeHeaderHeight'; -import { LogsNavigationPages } from './LogsNavigationPages'; - type Props = { - absoluteRange: AbsoluteTimeRange; - timeZone: TimeZone; - queries: DataQuery[]; - loading: boolean; - visibleRange: AbsoluteTimeRange; logsSortOrder?: LogsSortOrder | null; - onChangeTime: (range: AbsoluteTimeRange) => void; scrollToTopLogs: () => void; scrollToBottomLogs?: () => void; - addResultsToCache: () => void; - clearCache: () => void; }; -export type LogsPage = { - logsRange: AbsoluteTimeRange; - queryRange: AbsoluteTimeRange; -}; - -function LogsNavigation({ - absoluteRange, - logsSortOrder, - timeZone, - loading, - onChangeTime, - scrollToTopLogs, - scrollToBottomLogs, - visibleRange, - queries, - clearCache, - addResultsToCache, -}: Props) { - const [pages, setPages] = useState([]); - - // These refs are to determine, if we want to clear up logs navigation when totally new query is run - const expectedQueriesRef = useRef(); - const expectedRangeRef = useRef(); - // This ref is to store range span for future queres based on firstly selected time range - // e.g. if last 5 min selected, always run 5 min range - const rangeSpanRef = useRef(0); - - const currentPageIndex = useMemo( - () => - pages.findIndex((page) => { - return page.queryRange.to === absoluteRange.to; - }), - [absoluteRange.to, pages] - ); - +function LogsNavigation({ logsSortOrder, scrollToTopLogs }: Props) { const oldestLogsFirst = logsSortOrder === LogsSortOrder.Ascending; - const onFirstPage = oldestLogsFirst ? currentPageIndex === pages.length - 1 : currentPageIndex === 0; - const onLastPage = oldestLogsFirst ? currentPageIndex === 0 : currentPageIndex === pages.length - 1; const theme = useTheme2(); const styles = getStyles(theme, oldestLogsFirst); - // Main effect to set pages and index - useEffect(() => { - const newPage = { logsRange: visibleRange, queryRange: absoluteRange }; - let newPages: LogsPage[] = []; - // We want to start new pagination if queries change or if absolute range is different than expected - if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) { - clearCache(); - setPages([newPage]); - expectedQueriesRef.current = queries; - rangeSpanRef.current = absoluteRange.to - absoluteRange.from; - } else { - setPages((pages) => { - // Remove duplicates with new query - newPages = pages.filter((page) => !isEqual(newPage.queryRange, page.queryRange)); - // Sort pages based on logsOrder so they visually align with displayed logs - newPages = [...newPages, newPage].sort((a, b) => sortPages(a, b, logsSortOrder)); - return newPages; - }); - } - }, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]); - - const changeTime = useCallback( - ({ from, to }: AbsoluteTimeRange) => { - addResultsToCache(); - expectedRangeRef.current = { from, to }; - onChangeTime({ from, to }); - }, - [onChangeTime, addResultsToCache] - ); - - const sortPages = (a: LogsPage, b: LogsPage, logsSortOrder?: LogsSortOrder | null) => { - if (logsSortOrder === LogsSortOrder.Ascending) { - return a.queryRange.to > b.queryRange.to ? 1 : -1; - } - return a.queryRange.to > b.queryRange.to ? -1 : 1; - }; - - const olderLogsButton = ( - - ); - - const newerLogsButton = ( - - ); - - const onPageClick = useCallback( - (page: LogsPage, pageNumber: number) => { - reportInteraction('grafana_explore_logs_pagination_clicked', { - pageType: 'page', - pageNumber, - }); - changeTime({ from: page.queryRange.from, to: page.queryRange.to }); - scrollToTopLogs(); - }, - [changeTime, scrollToTopLogs] - ); - const onScrollToTopClick = useCallback(() => { reportInteraction('grafana_explore_logs_scroll_top_clicked'); scrollToTopLogs(); }, [scrollToTopLogs]); - const onScrollToBottomClick = useCallback(() => { - reportInteraction('grafana_explore_logs_scroll_bottom_clicked'); - scrollToBottomLogs?.(); - }, [scrollToBottomLogs]); - return (
- {!config.featureToggles.logsInfiniteScrolling && ( - <> - {oldestLogsFirst ? olderLogsButton : newerLogsButton} - - {oldestLogsFirst ? newerLogsButton : olderLogsButton} - - )} - {scrollToBottomLogs && ( - - )} - ))} -
-
- - ); -} - -const getStyles = (theme: GrafanaTheme2, loading: boolean) => { - return { - pagesWrapper: css({ - height: '100%', - paddingLeft: theme.spacing(0.5), - display: 'flex', - flexDirection: 'column', - '&::after': { - content: "''", - display: 'block', - background: `repeating-linear-gradient(135deg, ${theme.colors.background.primary}, ${theme.colors.background.primary} 5px, ${theme.colors.background.secondary} 5px, ${theme.colors.background.secondary} 15px)`, - width: '3px', - height: 'inherit', - marginBottom: theme.spacing(1), - }, - }), - pagesContainer: css({ - display: 'flex', - padding: 0, - flexDirection: 'column', - }), - page: css({ - display: 'flex', - margin: theme.spacing(2, 0), - cursor: loading ? 'auto' : 'pointer', - whiteSpace: 'normal', - '.selectedBg': { - background: theme.colors.primary.main, - }, - '.selectedText': { - color: theme.colors.primary.main, - }, - }), - line: css({ - width: '3px', - height: '100%', - alignItems: 'center', - background: theme.colors.text.secondary, - }), - time: css({ - width: '60px', - minHeight: '80px', - fontSize: theme.v1.typography.size.sm, - paddingLeft: theme.spacing(0.5), - display: 'flex', - alignItems: 'center', - }), - }; -}; diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index 4094d4f515c..874234837ac 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -13,7 +13,6 @@ import { SplitOpen, TimeRange, } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LogsVisualisationType } from '../../explore/Logs/Logs'; @@ -150,7 +149,7 @@ const LogRowsComponent = forwardRef { - config.featureToggles.logsInfiniteScrolling = true; -}); -afterAll(() => { - config.featureToggles.logsInfiniteScrolling = originalState; -}); - describe('InfiniteScroll', () => { test('Wraps components without adding DOM elements', async () => { const { container } = render( diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index e21ca4a4447..3c4ab5001b1 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ReactNode, MutableRefObject, useCallback, useEffect, useRef, useState } import { AbsoluteTimeRange, CoreApp, LogRowModel, TimeRange, rangeUtil } from '@grafana/data'; // import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/internal'; import { Trans } from '@grafana/i18n'; -import { config, reportInteraction } from '@grafana/runtime'; +import { reportInteraction } from '@grafana/runtime'; import { LogsSortOrder, TimeZone } from '@grafana/schema'; import { Button, Icon } from '@grafana/ui'; @@ -86,7 +86,7 @@ export const InfiniteScroll = ({ } function handleScroll(event: Event | WheelEvent) { - if (!scrollElement || !loadMoreLogs || !rows.length || loading || !config.featureToggles.logsInfiniteScrolling) { + if (!scrollElement || !loadMoreLogs || !rows.length || loading) { return; } const scrollDirection = shouldLoadMore(event, lastEvent.current, countRef, scrollElement, lastScroll.current); diff --git a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx index 631c2831e9c..2507a3b5176 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx @@ -2,7 +2,6 @@ import { act, render, screen } from '@testing-library/react'; import { VariableSizeList } from 'react-window'; import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LogsSortOrder } from '@grafana/schema'; import { ScrollDirection, SCROLLING_THRESHOLD } from '../InfiniteScroll'; @@ -110,14 +109,6 @@ function setup( return { element, events, scrollTo, wheel }; } -const originalState = config.featureToggles.logsInfiniteScrolling; -beforeAll(() => { - config.featureToggles.logsInfiniteScrolling = true; -}); -afterAll(() => { - config.featureToggles.logsInfiniteScrolling = originalState; -}); - describe('InfiniteScroll', () => { describe.each([LogsSortOrder.Descending, LogsSortOrder.Ascending])( 'When the sort order is descending', diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 45cc616ea61..3c7de64b6be 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config, reportInteraction } from '@grafana/runtime'; +import { reportInteraction } from '@grafana/runtime'; import { Spinner, useStyles2 } from '@grafana/ui'; import { canScrollBottom, canScrollTop, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; @@ -139,7 +139,7 @@ export const InfiniteScroll = ({ ); useEffect(() => { - if (!scrollElement || !loadMore || !config.featureToggles.logsInfiniteScrolling) { + if (!scrollElement || !loadMore) { return; } diff --git a/public/app/features/logs/logsModel.test.ts b/public/app/features/logs/logsModel.test.ts index f6221b6c6af..23d0badb4f5 100644 --- a/public/app/features/logs/logsModel.test.ts +++ b/public/app/features/logs/logsModel.test.ts @@ -21,7 +21,6 @@ import { sortDataFrame, toDataFrame, } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { getMockFrames } from 'app/plugins/datasource/loki/mocks/frames'; @@ -292,7 +291,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -374,7 +373,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -386,9 +385,7 @@ describe('dataFrameToLogsModel', () => { }); }); - it('with infinite scrolling enabled it should return expected logs model', () => { - config.featureToggles.logsInfiniteScrolling = true; - + it('it should return expected logs model', () => { const series: DataFrame[] = [ createDataFrame({ fields: [ @@ -421,8 +418,6 @@ describe('dataFrameToLogsModel', () => { value: `1 line displayed`, kind: LogsMetaKind.String, }); - - config.featureToggles.logsInfiniteScrolling = false; }); it('given one series with limit as custom meta property should return correct limit', () => { @@ -430,7 +425,7 @@ describe('dataFrameToLogsModel', () => { const logsModel = dataFrameToLogsModel(series, 1); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); }); @@ -639,7 +634,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -758,7 +753,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(3); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index 87cdc031ffc..e60887518fe 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -41,7 +41,6 @@ import { } from '@grafana/data'; import { SIPrefix } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema'; import { colors } from '@grafana/ui'; import { getThemeColor } from 'app/core/utils/colors'; @@ -576,8 +575,7 @@ function adjustMetaInfo(logsModel: LogsModel, visibleRangeMs?: number, requested metaLimitValue = `${limit} lines shown — ${coverage}% (${rangeUtil.msRangeToTimeString(visibleRangeMs)}) of ${rangeUtil.msRangeToTimeString(requestedRangeMs)}`; } } else { - const description = config.featureToggles.logsInfiniteScrolling ? 'displayed' : 'returned'; - metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} ${description}`; + metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} displayed`; } logsModelMeta[limitIndex] = { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index c2659859b3e..191e503fd90 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -464,7 +464,7 @@ export const LogsPanel = ({ const loadMoreLogs = useCallback( async (scrollRange: AbsoluteTimeRange) => { - if (!data.request || !config.featureToggles.logsInfiniteScrolling || loadingRef.current) { + if (!data.request || loadingRef.current) { return; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9539dbefc30..cc900022bb4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9973,11 +9973,7 @@ "wrap-lines": "Wrap lines" }, "logs-navigation": { - "newer-logs": "Newer logs", - "older-logs": "Older logs", - "scroll-bottom": "Scroll to bottom", - "scroll-top": "Scroll to top", - "start-of-range": "Start of range" + "scroll-top": "Scroll to top" }, "logs-panel": { "render-common-labels": { From af684248b5c2ccdc1adf1601ec84d004bc03303f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:21:19 +0100 Subject: [PATCH 027/423] feat: add reindex trigger as a migration validation (#114283) * feat: add search validation * chore: remove comment * fix: remove local test * fix: revert rebuild index portion * chore: remove unnecessary comments * refactor: validator * fix: revert setting --- pkg/server/wire_gen.go | 4 +- pkg/storage/unified/migrations/migrator.go | 1 - .../unified/migrations/resource_migration.go | 148 +++--------------- pkg/storage/unified/migrations/service.go | 23 ++- pkg/storage/unified/migrations/validator.go | 99 ++++++++++++ 5 files changed, 129 insertions(+), 146 deletions(-) create mode 100644 pkg/storage/unified/migrations/validator.go diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 0cb27791c19..716cd7a9c50 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -534,7 +534,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) - unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err @@ -1181,7 +1181,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) - unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err diff --git a/pkg/storage/unified/migrations/migrator.go b/pkg/storage/unified/migrations/migrator.go index bdbb69f98a1..d95aa59ace7 100644 --- a/pkg/storage/unified/migrations/migrator.go +++ b/pkg/storage/unified/migrations/migrator.go @@ -144,7 +144,6 @@ func newUnifiedMigrator( } } -// migrate function -- works for a single kind type migratorFunc = func(ctx context.Context, orgId int64, opts legacy.MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*legacy.BlobStoreInfo, error) func (m *unifiedMigration) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index 663bf030f67..cf67d220ec3 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -16,65 +16,34 @@ import ( ) // ValidationFunc is a function that validates migration results. -// It receives the database session, migration response, and logger for reporting. -// Return an error if validation fails, nil if validation passes or is skipped. -type ValidationFunc func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error + +type Validator interface { + Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error +} // ResourceMigration handles migration of specific resource types from legacy to unified storage. -// It implements migrator.CodeMigration and provides a generic, extensible way to migrate any -// resource type by: -// -// 1. Iterating through all organizations -// 2. For each org, delegating to LegacyMigrator to read from legacy and write to unified storage -// 3. Validating migration results using the provided validation function (if any) -// -// To add a new resource type migration, simply create a new ResourceMigration instance in -// service.go with the appropriate schema.GroupResource specifications and optional validation function. type ResourceMigration struct { migrator.MigrationBase - migrator UnifiedMigrator - resources []schema.GroupResource - migrationID string - validationFunc ValidationFunc // Optional: custom validation logic for this migration - log log.Logger + migrator UnifiedMigrator + resources []schema.GroupResource + migrationID string + validator Validator // Optional: custom validation logic for this migration + log log.Logger } // NewResourceMigration creates a new migration for the specified resources. -// This is the primary way to register new resource migrations. -// -// Parameters: -// - legacyMigrator: handles reading from legacy storage and writing to unified storage -// - resources: list of GroupResource to migrate -// - migrationID: unique identifier for this migration -// - validationFunc: optional validation function to verify migration results. -// If nil, no validation will be performed. -// -// Example with legacy table count validation: -// -// NewResourceMigration( -// migrator, -// []schema.GroupResource{{Group: "playlist.grafana.app", Resource: "playlists"}}, -// "playlists", -// NewLegacyTableCountValidator(map[string]LegacyTableInfo{ -// "playlist.grafana.app/playlists": {Table: "playlist", WhereClause: "org_id = ?"}, -// }), -// ) -// -// Example without validation: -// -// NewResourceMigration(migrator, resources, "new-resource", nil) func NewResourceMigration( migrator UnifiedMigrator, resources []schema.GroupResource, migrationID string, - validationFunc ValidationFunc, + validator Validator, ) *ResourceMigration { return &ResourceMigration{ - migrator: migrator, - resources: resources, - migrationID: migrationID, - validationFunc: validationFunc, - log: log.New("storage.unified.resource_migration." + migrationID), + migrator: migrator, + resources: resources, + migrationID: migrationID, + validator: validator, + log: log.New("storage.unified.resource_migration." + migrationID), } } @@ -139,7 +108,7 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, } // Validate the migration results - if err := m.validateMigration(sess, response); err != nil { + if err := m.validateMigration(migrationCtx, sess, response); err != nil { m.log.Error("Migration validation failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) return fmt.Errorf("migration validation failed for org %d (%s): %w", org.ID, org.Name, err) } @@ -155,13 +124,13 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, } // validateMigration calls the custom validation function if provided -func (m *ResourceMigration) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { - if m.validationFunc == nil { +func (m *ResourceMigration) validateMigration(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse) error { + if m.validator == nil { m.log.Debug("No validation function provided, skipping validation") return nil } - return m.validationFunc(sess, response, m.log) + return m.validator.Validate(ctx, sess, response, m.log) } // LegacyTableInfo defines how to map a unified storage resource to its legacy table @@ -170,85 +139,6 @@ type LegacyTableInfo struct { WhereClause string // WHERE clause template with org_id parameter (e.g., "org_id = ? and is_folder = false") } -// NewLegacyTableCountValidator creates a ValidationFunc that validates migration by comparing -// counts between legacy tables and unified storage. -// -// This is a helper for the common case of validating that all items from legacy tables -// were successfully migrated to unified storage. -// -// Parameters: -// - legacyTableMap: maps "group/resource" keys to LegacyTableInfo for validation. -// Only resources with mappings will be validated. -// -// Example: -// -// validator := NewLegacyTableCountValidator(map[string]LegacyTableInfo{ -// "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, -// "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, -// }) -func NewLegacyTableCountValidator(legacyTableMap map[string]LegacyTableInfo) ValidationFunc { - return func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { - // Check for rejected items - if len(response.Rejected) > 0 { - log.Warn("Migration had rejected items", "count", len(response.Rejected)) - for i, rejected := range response.Rejected { - if i < 10 { // Log first 10 rejected items - log.Warn("Rejected item", - "namespace", rejected.Key.Namespace, - "group", rejected.Key.Group, - "resource", rejected.Key.Resource, - "name", rejected.Key.Name, - "reason", rejected.Error) - } - } - // Rejections are not fatal - they may be expected for invalid data - } - - // Validate counts for each resource type - for _, summary := range response.Summary { - key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) - tableInfo, ok := legacyTableMap[key] - if !ok { - log.Debug("No legacy table mapping for resource, skipping count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace) - continue - } - - // Get legacy count - orgID, err := ParseOrgIDFromNamespace(summary.Namespace) - if err != nil { - return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) - } - - legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() - if err != nil { - return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) - } - - // Account for rejected items in validation - expectedCount := summary.Count + int64(len(response.Rejected)) - - log.Info("Count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace, - "legacy_count", legacyCount, - "unified_count", summary.Count, - "rejected", len(response.Rejected), - "history", summary.History) - - // Validate that we migrated all items (allowing for rejected items) - if legacyCount > expectedCount { - return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", - summary.Resource, summary.Group, summary.Namespace, - legacyCount, summary.Count, len(response.Rejected)) - } - } - - return nil - } -} - func ParseOrgIDFromNamespace(namespace string) (int64, error) { // Use authlib to properly parse all namespace formats including "default" for org 1 info, err := types.ParseNamespace(namespace) diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 5bcb2fdf8c2..699be937d72 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -11,6 +11,7 @@ import ( sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "k8s.io/apimachinery/pkg/runtime/schema" @@ -24,29 +25,28 @@ type UnifiedStorageMigrationServiceImpl struct { cfg *setting.Cfg sqlStore db.DB kv kvstore.KVStore + client resource.ResourceClient } var _ contract.UnifiedStorageMigrationService = (*UnifiedStorageMigrationServiceImpl)(nil) // ProvideUnifiedStorageMigrationService is a Wire provider that creates the migration service. -// The service implements registry.BackgroundService and runs migrations during server startup. func ProvideUnifiedStorageMigrationService( migrator UnifiedMigrator, cfg *setting.Cfg, sqlStore db.DB, kv kvstore.KVStore, + client resource.ResourceClient, ) contract.UnifiedStorageMigrationService { return &UnifiedStorageMigrationServiceImpl{ migrator: migrator, cfg: cfg, sqlStore: sqlStore, kv: kv, + client: client, } } -// Run executes unified storage migrations as a background service. -// This blocks until migrations complete. If migrations fail, an error is returned -// which will prevent Grafana from starting. func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { // TODO: temporary skip migrations in test environments to prevent integration test timeouts. if os.Getenv("GRAFANA_TEST_DB") != "" { @@ -61,18 +61,15 @@ func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { // TODO: Re-enable once migrations are ready // TODO: add guarantee that this only runs once - // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore) + // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore, p.client) return nil } -// RegisterMigrations initializes and registers all unified storage migrations. -// This function is the entry point for all data migrations from legacy storage -// to unified storage. It returns an error if migrations fail, preventing Grafana -// from starting with inconsistent data. func RegisterMigrations( migrator UnifiedMigrator, cfg *setting.Cfg, sqlStore db.DB, + client resource.ResourceClient, ) error { ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") defer span.End() @@ -85,7 +82,7 @@ func RegisterMigrations( // Register resource migrations // To add a new resource type, simply add another migration here with the appropriate resources - registerResourceMigrations(mg, migrator) + registerResourceMigrations(mg, migrator, client) // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") @@ -99,9 +96,7 @@ func RegisterMigrations( return nil } -// registerResourceMigrations registers all unified storage resource migrations. -// Add new resource types here by creating additional ResourceMigration instances. -func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator) { +func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { dashboardsAndFolders := NewResourceMigration( migrator, []schema.GroupResource{ @@ -109,7 +104,7 @@ func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedM {Group: "dashboard.grafana.app", Resource: "dashboards"}, }, "folders-dashboards", - NewLegacyTableCountValidator(map[string]LegacyTableInfo{ + NewCountValidator(client, map[string]LegacyTableInfo{ "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, }), diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go new file mode 100644 index 00000000000..6deef77bb6d --- /dev/null +++ b/pkg/storage/unified/migrations/validator.go @@ -0,0 +1,99 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/xorm" +) + +type CountValidator struct { + client resourcepb.ResourceIndexClient + legacyTableMap map[string]LegacyTableInfo +} + +func NewCountValidator(client resourcepb.ResourceIndexClient, legacyTableMap map[string]LegacyTableInfo) Validator { + return &CountValidator{client: client, legacyTableMap: legacyTableMap} +} + +func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + if len(response.Rejected) > 0 { + log.Warn("Migration had rejected items", "count", len(response.Rejected)) + for i, rejected := range response.Rejected { + if i < 10 { // Log first 10 rejected items + log.Warn("Rejected item", + "namespace", rejected.Key.Namespace, + "group", rejected.Key.Group, + "resource", rejected.Key.Resource, + "name", rejected.Key.Name, + "reason", rejected.Error) + } + } + // Rejections are not fatal - they may be expected for invalid data + } + + // Validate counts for each resource type + for _, summary := range response.Summary { + key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) + tableInfo, ok := v.legacyTableMap[key] + if !ok { + log.Debug("No legacy table mapping for resource, skipping count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace) + continue + } + + // Get legacy count from database + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() + if err != nil { + return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) + } + + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + + // Find the count for this specific resource type + var unifiedCount int64 + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break + } + } + + // Account for rejected items in validation + expectedCount := unifiedCount + int64(len(response.Rejected)) + + log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", unifiedCount, + "migration_summary_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) + + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, unifiedCount, len(response.Rejected)) + } + } + + return nil +} From b366217aa6604e04f9f155dc641a1c3b0265963a Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:40:44 +0000 Subject: [PATCH 028/423] SchemaV2ToScenes: Adjust ad hoc multi value check in v2 (#114291) adjust ad hoc multi value check in v2 --- .../serialization/transformSaveModelSchemaV2ToScene.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 4e9e8f7fc81..0534ad76e56 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -319,7 +319,9 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S defaultKeys: variable.spec.defaultKeys, useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, - supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators), + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings({ type: ds.type })?.meta.multiValueFilterOperators + ), }); } if (variable.kind === defaultCustomVariableKind().kind) { @@ -517,7 +519,7 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, supportsMultiValueOperators: Boolean( - getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators + getDataSourceSrv().getInstanceSettings({ type: ds.type })?.meta.multiValueFilterOperators ), }); } From 203549ce3caa9aab6a5f70103e9e4d78b0831e81 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 21 Nov 2025 12:53:15 +0100 Subject: [PATCH 029/423] Chore: Fix enterprise imports drift (#114288) * Chore: Fix enterprise imports drift * Fix indirect dependency --- go.mod | 2 +- pkg/extensions/enterprise_imports.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d8df0cd6bd7..eb9780e649f 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/crewjam/saml v0.4.14 // @grafana/identity-access-team github.com/dgraph-io/badger/v4 v4.7.0 // @grafana/grafana-search-and-storage github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group + github.com/docker/go-connections v0.6.0 // @grafana/grafana-app-platform-squad github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // @grafana/grafana-datasources-core-services github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 // @grafana/grafana-datasources-core-services github.com/dustin/go-humanize v1.0.1 // @grafana/observability-traces-and-profiling @@ -405,7 +406,6 @@ require ( github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.4.0+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect; @grafana/grafana-app-platform-squad github.com/docker/go-units v0.5.0 // indirect github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 113c2f8e4bb..472652cc103 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -15,6 +15,7 @@ import ( _ "github.com/blugelabs/bluge" _ "github.com/blugelabs/bluge_segment_api" _ "github.com/crewjam/saml" + _ "github.com/docker/go-connections/nat" _ "github.com/go-jose/go-jose/v4" _ "github.com/gobwas/glob" _ "github.com/googleapis/gax-go/v2" @@ -30,6 +31,7 @@ import ( _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/spyzhov/ajson" _ "github.com/stretchr/testify/require" + _ "github.com/testcontainers/testcontainers-go" _ "gocloud.dev/secrets/awskms" _ "gocloud.dev/secrets/azurekeyvault" _ "gocloud.dev/secrets/gcpkms" @@ -54,9 +56,7 @@ 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/testcontainers/testcontainers-go" + _ "github.com/grafana/tempo/pkg/traceql" ) From 669382c21269343e1b4690623f39d7bd413895d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 21 Nov 2025 14:18:32 +0100 Subject: [PATCH 030/423] datasources: ClearAuthHeadersMiddleware: refactor (#113707) * refactor: extract logic * directly use the setting.cfg in the middleware * more granular config handling, per section * fixed unit test * refactor code to avoid lint error --- pkg/services/contexthandler/contexthandler.go | 48 +- .../clear_auth_headers_middleware.go | 17 +- .../clear_auth_headers_middleware_test.go | 553 ++++++------------ .../pluginsintegration/pluginsintegration.go | 2 +- 4 files changed, 230 insertions(+), 390 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 52b1488e71e..8c48121f925 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -207,6 +207,33 @@ type AuthHTTPHeaderList struct { Items []string } +func GetAuthHTTPHeaders(jwtAuth *setting.AuthJWTSettings, authProxy *setting.AuthProxySettings) []string { + var items []string + + // used by basic auth, api keys and potentially jwt auth + items = append(items, "Authorization") + + // remove X-Grafana-Device-Id as it is only used for auth in authn clients. + items = append(items, "X-Grafana-Device-Id") + + // if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization + if jwtAuth.Enabled && jwtAuth.HeaderName != "" && jwtAuth.HeaderName != "Authorization" { + items = append(items, jwtAuth.HeaderName) + } + + // if auth proxy is enabled add the main proxy header and all configured headers + if authProxy.Enabled { + items = append(items, authProxy.HeaderName) + for _, header := range authProxy.Headers { + if header != "" { + items = append(items, header) + } + } + } + + return items +} + // WithAuthHTTPHeaders returns a new context in which all possible configured auth header will be included // and later retrievable by AuthHTTPHeaderListFromContext. func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context { @@ -217,26 +244,7 @@ func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context } } - // used by basic auth, api keys and potentially jwt auth - list.Items = append(list.Items, "Authorization") - - // remove X-Grafana-Device-Id as it is only used for auth in authn clients. - list.Items = append(list.Items, "X-Grafana-Device-Id") - - // if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization - if cfg.JWTAuth.Enabled && cfg.JWTAuth.HeaderName != "" && cfg.JWTAuth.HeaderName != "Authorization" { - list.Items = append(list.Items, cfg.JWTAuth.HeaderName) - } - - // if auth proxy is enabled add the main proxy header and all configured headers - if cfg.AuthProxy.Enabled { - list.Items = append(list.Items, cfg.AuthProxy.HeaderName) - for _, header := range cfg.AuthProxy.Headers { - if header != "" { - list.Items = append(list.Items, header) - } - } - } + list.Items = append(list.Items, GetAuthHTTPHeaders(&cfg.JWTAuth, &cfg.AuthProxy)...) return context.WithValue(ctx, authHTTPHeaderListKey, list) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go index 5c93e1da565..1321c1fc474 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go @@ -6,21 +6,26 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/setting" ) // NewClearAuthHeadersMiddleware creates a new backend.HandlerMiddleware // that will clear any outgoing HTTP headers that was part of the incoming // HTTP request and used when authenticating to Grafana. -func NewClearAuthHeadersMiddleware() backend.HandlerMiddleware { +func NewClearAuthHeadersMiddleware(cfgJWTAuth *setting.AuthJWTSettings, cfgAuthProxy *setting.AuthProxySettings) backend.HandlerMiddleware { return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ClearAuthHeadersMiddleware{ - BaseHandler: backend.NewBaseHandler(next), + BaseHandler: backend.NewBaseHandler(next), + cfgJWTAuth: cfgJWTAuth, + cfgAuthProxy: cfgAuthProxy, } }) } type ClearAuthHeadersMiddleware struct { backend.BaseHandler + cfgJWTAuth *setting.AuthJWTSettings + cfgAuthProxy *setting.AuthProxySettings } func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -30,11 +35,9 @@ func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend return } - list := contexthandler.AuthHTTPHeaderListFromContext(ctx) - if list != nil { - for _, k := range list.Items { - h.DeleteHTTPHeader(k) - } + items := contexthandler.GetAuthHTTPHeaders(m.cfgJWTAuth, m.cfgAuthProxy) + for _, k := range items { + h.DeleteHTTPHeader(k) } } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go index 5ca047881e3..aa9bc3aa469 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" - "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -16,397 +15,227 @@ import ( func TestClearAuthHeadersMiddleware(t *testing.T) { const otherHeader = "test" - t.Run("When no auth headers in reqContext", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) - require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) - req.Header.Set(otherHeader, "test") + t.Run("When requests are for a datasource", func(t *testing.T) { + cfg := setting.NewCfg() + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy)), + ) - t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } - pluginCtx := backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, - } - - t.Run("No auth headers to clear when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{otherHeader: {"test"}}, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, http.Header{http.CanonicalHeaderKey(otherHeader): {"test"}}, cdt.CallResourceReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) + require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) }) - t.Run("And requests are for an app", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{ + otherHeader: {"test"}, + "Authorization": {"secret"}, + "X-Grafana-Device-Id": {"secret"}, + }, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) + require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + }) - pluginCtx := backend.PluginContext{ - AppInstanceSettings: &backend.AppInstanceSettings{}, - } - - t.Run("No auth headers to clear when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) + require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{otherHeader: {"test"}}, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, http.Header{http.CanonicalHeaderKey(otherHeader): {"test"}}, cdt.CallResourceReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.SubscribeStreamReq) + require.Len(t, cdt.SubscribeStreamReq.Headers, 1) + require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.PublishStreamReq) + require.Len(t, cdt.PublishStreamReq.Headers, 1) + require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) - }) + t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { + err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, + }, &backend.StreamSender{}) + require.NoError(t, err) + require.NotNil(t, cdt.RunStreamReq) + require.Len(t, cdt.RunStreamReq.Headers, 1) + require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) }) }) - t.Run("When auth headers in reqContext", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) - require.NoError(t, err) + t.Run("When requests are for an app", func(t *testing.T) { + cfg := setting.NewCfg() + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy)), + ) - t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + req.Header.Set("Authorization", "val") - req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) + const otherHeader = "x-Other" + req.Header.Set(otherHeader, "test") - pluginCtx := backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, - } + pluginCtx := backend.PluginContext{ + AppInstanceSettings: &backend.AppInstanceSettings{}, + } - t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{ - otherHeader: {"test"}, - "Authorization": {"secret"}, - "X-Grafana-Device-Id": {"secret"}, - }, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) - }) - - t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) + require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) }) - t.Run("And requests are for an app", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{ + otherHeader: {"test"}, + "Authorization": {"secret"}, + "X-Grafana-Device-Id": {"secret"}, + }, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) + require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + }) - req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) - req.Header.Set("Authorization", "val") - - const otherHeader = "x-Other" - req.Header.Set(otherHeader, "test") - - pluginCtx := backend.PluginContext{ - AppInstanceSettings: &backend.AppInstanceSettings{}, - } - - t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) + require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{ - otherHeader: {"test"}, - "Authorization": {"secret"}, - "X-Grafana-Device-Id": {"secret"}, - }, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.SubscribeStreamReq) + require.Len(t, cdt.SubscribeStreamReq.Headers, 1) + require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.PublishStreamReq) + require.Len(t, cdt.PublishStreamReq.Headers, 1) + require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) - }) + t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { + err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, + }, &backend.StreamSender{}) + require.NoError(t, err) + require.NotNil(t, cdt.RunStreamReq) + require.Len(t, cdt.RunStreamReq.Headers, 1) + require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) }) }) } diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index a510ead1910..9c0cf4d10c6 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -200,7 +200,7 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken middlewares = append(middlewares, clientmiddleware.NewTracingHeaderMiddleware(), - clientmiddleware.NewClearAuthHeadersMiddleware(), + clientmiddleware.NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames), clientmiddleware.NewCachingMiddleware(cachingServiceClient), From c0f6d90971bb7fa4f2bc0c77a8339d199ca39306 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:19:55 -0600 Subject: [PATCH 031/423] Loki: Set Query limits context header (#114112) * feat(lokiQueryLimitsContext): send full query range & expr on initial split requests --- .../src/types/featureToggles.gen.ts | 6 +- .../dataquery/x/LokiDataQuery_types.gen.ts | 8 + pkg/services/featuremgmt/registry.go | 9 +- pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 1587 ++++------------- pkg/tsdb/loki/api.go | 11 + pkg/tsdb/loki/api_test.go | 52 + .../kinds/dataquery/types_dataquery_gen.go | 13 + pkg/tsdb/loki/parse_query.go | 15 + pkg/tsdb/loki/parse_query_test.go | 69 + pkg/tsdb/loki/types.go | 6 + public/app/features/explore/Logs/Logs.tsx | 1 + .../explore/Logs/LogsVolumePanelList.tsx | 38 +- .../Logs/utils/logsVolumeResponse.test.ts | 70 +- .../explore/Logs/utils/logsVolumeResponse.ts | 28 +- .../app/plugins/datasource/loki/dataquery.cue | 8 + .../plugins/datasource/loki/dataquery.gen.ts | 8 + .../plugins/datasource/loki/mergeResponses.ts | 7 + .../plugins/datasource/loki/mocks/frames.ts | 28 + .../datasource/loki/querySplitting.test.ts | 195 +- .../plugins/datasource/loki/querySplitting.ts | 51 +- .../app/plugins/datasource/loki/queryUtils.ts | 20 + .../plugins/datasource/loki/responseUtils.ts | 22 +- .../loki/shardQuerySplitting.test.ts | 134 +- .../datasource/loki/shardQuerySplitting.ts | 30 +- public/locales/en-US/grafana.json | 6 +- 26 files changed, 1088 insertions(+), 1335 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 5e9ad9ff8d4..09c123d527a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -596,7 +596,7 @@ export interface FeatureToggles { */ alertingPrometheusRulesPrimary?: boolean; /** - * Used in Logs Drilldown to split queries into multiple queries based on the number of shards + * Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards */ exploreLogsShardSplitting?: boolean; /** @@ -1178,6 +1178,10 @@ export interface FeatureToggles { */ ttlPluginInstanceManager?: boolean; /** + * Send X-Loki-Query-Limits-Context header to Loki on first split request + */ + lokiQueryLimitsContext?: boolean; + /** * Enables the new version of rudderstack * @default false */ diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index f8f056a3c8d..2e3c4991c65 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -50,6 +50,14 @@ export interface LokiDataQuery extends common.DataQuery { * Used to override the name of the series. */ legendFormat?: string; + /** + * The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + */ + limitsContext?: { + expr: string; + from: number; + to: number; + }; /** * Used to limit the number of log rows returned. */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a557e32087b..26690f92481 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -983,7 +983,7 @@ var ( }, { Name: "exploreLogsShardSplitting", - Description: "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", + Description: "Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, @@ -1938,6 +1938,13 @@ var ( FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "lokiQueryLimitsContext", + Description: "Send X-Loki-Query-Limits-Context header to Loki on first split request", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + }, { Name: "rudderstackUpgrade", Description: "Enables the new version of rudderstack", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 99753233108..d0a2674aa55 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -263,4 +263,5 @@ kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true +lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index d60ad45f64e..1f9315705ce 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3,46 +3,10 @@ "apiVersion": "featuretoggle.grafana.app/v0alpha1", "metadata": {}, "items": [ - { - "metadata": { - "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-03T14:39:58Z", - "deletionTimestamp": "2025-11-17T15:58:43Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Add cumulative and window functions to the add field from calculation transformation", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "adhocFiltersInTooltips", - "resourceVersion": "1756814786992", - "creationTimestamp": "2025-07-29T17:53:43Z", - "deletionTimestamp": "2025-11-12T10:05:30Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" - } - }, - "spec": { - "description": "Enable adhoc filter buttons in visualization tooltips", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-05T12:01:31Z" }, "spec": { @@ -55,7 +19,7 @@ { "metadata": { "name": "alertEnrichment", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-06T12:16:07Z" }, "spec": { @@ -69,7 +33,7 @@ { "metadata": { "name": "alertEnrichmentConditional", - "resourceVersion": "1757418974334", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -84,7 +48,7 @@ { "metadata": { "name": "alertEnrichmentMultiStep", - "resourceVersion": "1757418224384", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -99,7 +63,7 @@ { "metadata": { "name": "alertRuleRestore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-05T14:15:26Z" }, "spec": { @@ -112,7 +76,7 @@ { "metadata": { "name": "alertRuleUseFiredAtForStartsAt", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-22T11:16:38Z" }, "spec": { @@ -125,7 +89,7 @@ { "metadata": { "name": "alertingAIAnalyzeCentralStateHistory", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -139,7 +103,7 @@ { "metadata": { "name": "alertingAIFeedback", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T12:38:09Z" }, "spec": { @@ -153,7 +117,7 @@ { "metadata": { "name": "alertingAIGenAlertRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -167,7 +131,7 @@ { "metadata": { "name": "alertingAIGenTemplates", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -181,7 +145,7 @@ { "metadata": { "name": "alertingAIImproveAlertRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -195,7 +159,7 @@ { "metadata": { "name": "alertingBacktesting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-12-14T14:44:14Z" }, "spec": { @@ -207,7 +171,7 @@ { "metadata": { "name": "alertingBulkActionsInUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-24T14:49:59Z" }, "spec": { @@ -222,11 +186,8 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1763541314825", - "creationTimestamp": "2024-05-29T15:01:38Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-05-29T15:01:38Z" }, "spec": { "description": "Enables the new central alert history.", @@ -237,7 +198,7 @@ { "metadata": { "name": "alertingDisableSendAlertsExternal", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-23T12:29:19Z" }, "spec": { @@ -250,7 +211,7 @@ { "metadata": { "name": "alertingEnrichmentAssistantInvestigations", - "resourceVersion": "1757606567075", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -262,25 +223,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "alertingEnrichmentAssistantInvestigationsUI", - "resourceVersion": "1757541861834", - "creationTimestamp": "2025-09-10T22:04:21Z", - "deletionTimestamp": "2025-09-11T16:02:47Z" - }, - "spec": { - "description": "Enable Assistant Investigations enrichment type in the UI.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertingEnrichmentPerRule", - "resourceVersion": "1756206837948", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -295,7 +241,7 @@ { "metadata": { "name": "alertingFilterV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-11T11:29:26Z" }, "spec": { @@ -308,7 +254,7 @@ { "metadata": { "name": "alertingImportAlertmanagerAPI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-10T08:32:50Z" }, "spec": { @@ -322,7 +268,7 @@ { "metadata": { "name": "alertingImportAlertmanagerUI", - "resourceVersion": "1754585847887", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -337,7 +283,7 @@ { "metadata": { "name": "alertingImportYAMLUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-21T15:59:41Z" }, "spec": { @@ -351,7 +297,7 @@ { "metadata": { "name": "alertingJiraIntegration", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-14T12:22:04Z" }, "spec": { @@ -365,7 +311,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -378,7 +324,7 @@ { "metadata": { "name": "alertingListViewV2PreviewToggle", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-22T08:50:34Z" }, "spec": { @@ -391,7 +337,7 @@ { "metadata": { "name": "alertingMigrationUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-14T16:40:05Z" }, "spec": { @@ -405,7 +351,7 @@ { "metadata": { "name": "alertingNotificationHistory", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-17T13:26:26Z" }, "spec": { @@ -419,7 +365,7 @@ { "metadata": { "name": "alertingNotificationsStepMode", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-22T11:07:45Z" }, "spec": { @@ -433,7 +379,7 @@ { "metadata": { "name": "alertingPrometheusRulesPrimary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-27T12:27:16Z" }, "spec": { @@ -446,7 +392,7 @@ { "metadata": { "name": "alertingProvenanceLockWrites", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T18:16:06Z" }, "spec": { @@ -459,7 +405,7 @@ { "metadata": { "name": "alertingQueryAndExpressionsStepMode", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { @@ -473,7 +419,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -483,25 +429,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "alertingRuleNotificationMessageSectionExtension", - "resourceVersion": "1756193222535", - "creationTimestamp": "2025-08-26T07:27:02Z", - "deletionTimestamp": "2025-08-26T11:13:57Z" - }, - "spec": { - "description": "Enable rule notification message section extension.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertingRulePermanentlyDelete", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-03T11:18:25Z" }, "spec": { @@ -516,11 +447,8 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1763541314825", - "creationTimestamp": "2025-03-27T14:39:26Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-03-27T14:39:26Z" }, "spec": { "description": "Enables the UI functionality to recover and view deleted alert rules", @@ -533,7 +461,7 @@ { "metadata": { "name": "alertingRuleVersionHistoryRestore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-17T12:25:32Z" }, "spec": { @@ -548,11 +476,8 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1759485036332", - "creationTimestamp": "2025-01-27T17:47:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-03 09:50:36.332762 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-01-27T17:47:33Z" }, "spec": { "description": "Enables the compressed protobuf-based alert state storage. Default is enabled.", @@ -564,7 +489,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -576,12 +501,9 @@ { "metadata": { "name": "alertingTriage", - "resourceVersion": "1763541314825", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the alerting triage feature", @@ -594,7 +516,7 @@ { "metadata": { "name": "alertingUIOptimizeReducer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-18T10:59:00Z" }, "spec": { @@ -608,7 +530,7 @@ { "metadata": { "name": "alertingUIUseBackendFilters", - "resourceVersion": "1762966218072", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-13T14:52:14Z" }, "spec": { @@ -621,12 +543,9 @@ { "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", - "resourceVersion": "1759339813575", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-01 17:30:13.575464 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "", @@ -637,29 +556,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "alertingUseOldSimplifiedRoutingHashAlgorithm", - "resourceVersion": "1759339782639", - "creationTimestamp": "2025-10-01T17:29:29Z", - "deletionTimestamp": "2025-10-01T17:30:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-01 17:29:42.63941 +0000 UTC" - } - }, - "spec": { - "description": "", - "stage": "deprecated", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -671,7 +571,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -683,11 +583,8 @@ { "metadata": { "name": "alertmanagerRemoteSecondaryWithRemoteState", - "resourceVersion": "1753776005753", - "creationTimestamp": "2025-07-25T15:06:59Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-29 08:00:05.753498 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-25T15:06:59Z" }, "spec": { "description": "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", @@ -699,7 +596,7 @@ { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -712,7 +609,7 @@ { "metadata": { "name": "appPlatformGrpcClientAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { @@ -725,7 +622,7 @@ { "metadata": { "name": "assetSriChecks", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-04T10:56:35Z" }, "spec": { @@ -738,7 +635,7 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-13T09:41:35Z" }, "spec": { @@ -751,7 +648,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -764,11 +661,8 @@ { "metadata": { "name": "awsDatasourcesHttpProxy", - "resourceVersion": "1762964349996", - "creationTimestamp": "2025-11-12T18:51:23Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 16:19:09.996919 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-12T18:51:23Z" }, "spec": { "description": "Enables http proxy settings for aws datasources", @@ -780,7 +674,7 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { @@ -793,7 +687,7 @@ { "metadata": { "name": "azureMonitorDisableLogLimit", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-24T13:32:09Z" }, "spec": { @@ -806,7 +700,7 @@ { "metadata": { "name": "azureMonitorEnableUserAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-27T14:01:54Z" }, "spec": { @@ -819,7 +713,7 @@ { "metadata": { "name": "azureMonitorLogsBuilderEditor", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T14:15:25Z" }, "spec": { @@ -832,7 +726,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -845,7 +739,7 @@ { "metadata": { "name": "azureResourcePickerUpdates", - "resourceVersion": "1754910058337", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -860,7 +754,7 @@ { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -872,9 +766,8 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1753448760331", - "creationTimestamp": "2022-05-31T19:03:34Z", - "deletionTimestamp": "2025-11-06T14:43:27Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { "description": "Allow elements nesting", @@ -886,7 +779,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -899,7 +792,7 @@ { "metadata": { "name": "cdnPluginsLoadFirst", - "resourceVersion": "1758882341746", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -910,24 +803,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "cdnPluginsLoadedFirst", - "resourceVersion": "1758881920003", - "creationTimestamp": "2025-09-26T10:18:40Z", - "deletionTimestamp": "2025-09-26T10:25:41Z" - }, - "spec": { - "description": "Prioritize loading plugins from the CDN before other sources", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "false" - } - }, { "metadata": { "name": "cdnPluginsUrls", - "resourceVersion": "1759489886228", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -941,7 +820,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -955,7 +834,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -967,7 +846,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -980,7 +859,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -993,7 +872,7 @@ { "metadata": { "name": "cloudWatchRoundUpEndTime", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-27T15:10:28Z" }, "spec": { @@ -1006,7 +885,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -1017,27 +896,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "correlations", - "resourceVersion": "1762442825881", - "creationTimestamp": "2022-09-16T13:14:27Z", - "deletionTimestamp": "2025-11-13T09:21:46Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Correlations page", - "stage": "GA", - "codeowner": "@grafana/datapro", - "expression": "true" - } - }, { "metadata": { "name": "crashDetection", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { @@ -1050,7 +912,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV1", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1062,7 +924,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1071,28 +933,10 @@ "codeowner": "@grafana/grafana-app-platform-squad" } }, - { - "metadata": { - "name": "dashboardDsAdHocFiltering", - "resourceVersion": "1756814786992", - "creationTimestamp": "2025-07-23T08:12:25Z", - "deletionTimestamp": "2025-11-10T17:17:49Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" - } - }, - "spec": { - "description": "Enables adhoc filtering support for the dashboard datasource", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "dashboardLevelTimeMacros", - "resourceVersion": "1753435849295", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T09:49:07Z" }, "spec": { @@ -1105,12 +949,9 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1763643877862", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", @@ -1121,11 +962,8 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1763533458962", - "creationTimestamp": "2024-10-23T08:55:45Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-10-23T08:55:45Z" }, "spec": { "description": "Enables experimental new dashboard layouts", @@ -1136,7 +974,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -1150,7 +988,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -1164,7 +1002,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -1178,7 +1016,7 @@ { "metadata": { "name": "dashboardSchemaValidationLogging", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1190,11 +1028,8 @@ { "metadata": { "name": "dashboardTemplates", - "resourceVersion": "1763643877862", - "creationTimestamp": "2025-10-28T20:05:32Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-28T20:05:32Z" }, "spec": { "description": "Enables a flow to get started with a new dashboard from a template", @@ -1205,7 +1040,7 @@ { "metadata": { "name": "dashboardUndoRedo", - "resourceVersion": "1757940426210", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1219,7 +1054,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -1233,7 +1068,7 @@ { "metadata": { "name": "dataplaneAggregator", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { @@ -1243,25 +1078,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "dataplaneFrontendFallback", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-07T21:13:19Z", - "deletionTimestamp": "2025-11-06T17:39:31Z" - }, - "spec": { - "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "datasourceAPIServers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-19T08:28:27Z" }, "spec": { @@ -1274,7 +1094,7 @@ { "metadata": { "name": "datasourceConnectionsTab", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-21T17:39:48Z" }, "spec": { @@ -1287,7 +1107,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -1300,7 +1120,7 @@ { "metadata": { "name": "disableClassicHTTPHistogram", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-18T19:37:44Z" }, "spec": { @@ -1313,7 +1133,7 @@ { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -1326,7 +1146,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -1339,11 +1159,8 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1762552416963", - "creationTimestamp": "2023-04-12T16:24:34Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { "description": "Disables dataplane specific processing in server side expressions.", @@ -1351,50 +1168,11 @@ "codeowner": "@grafana/grafana-datasources-core-services" } }, - { - "metadata": { - "name": "dskitBackgroundServices", - "resourceVersion": "1757339637779", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-08 13:53:57.77994 +0000 UTC" - } - }, - "spec": { - "description": "Enables dskit background service wrapper", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, - { - "metadata": { - "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1762783224740", - "creationTimestamp": "2023-01-24T09:43:44Z", - "deletionTimestamp": "2025-11-12T14:47:44Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-10 14:00:24.740459 +0000 UTC" - } - }, - "spec": { - "description": "Enables drag and drop for CSV and Excel files", - "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true - } - }, { "metadata": { "name": "elasticsearchCrossClusterSearch", - "resourceVersion": "1757441088767", - "creationTimestamp": "2024-12-12T22:20:04Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-09 18:04:48.76781 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-12-12T22:20:04Z" }, "spec": { "description": "Enables cross cluster search in the Elasticsearch data source", @@ -1406,7 +1184,7 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-15T17:05:54Z" }, "spec": { @@ -1418,7 +1196,7 @@ { "metadata": { "name": "enableAppChromeExtensions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-30T04:32:08Z" }, "spec": { @@ -1433,7 +1211,7 @@ { "metadata": { "name": "enableDashboardEmptyExtensions", - "resourceVersion": "1759194774156", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-13T07:03:13Z" }, "spec": { @@ -1448,11 +1226,8 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1763539789149", - "creationTimestamp": "2023-04-24T14:46:31Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:09:49.149669 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { "description": "Enables the edit functionality in the datagrid panel", @@ -1463,7 +1238,7 @@ { "metadata": { "name": "enableExtensionsAdminPage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-05T15:55:10Z" }, "spec": { @@ -1476,7 +1251,7 @@ { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { @@ -1486,26 +1261,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "enablePluginImporter", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-16T04:42:28Z", - "deletionTimestamp": "2025-10-23T04:18:23Z" - }, - "spec": { - "description": "Set this to true to use the new PluginImporter functionality", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "enableSCIM", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { @@ -1517,7 +1276,7 @@ { "metadata": { "name": "enableScopesInMetricsExplore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { @@ -1530,7 +1289,7 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1543,7 +1302,7 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1556,11 +1315,11 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { - "description": "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", + "description": "Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -1569,7 +1328,7 @@ { "metadata": { "name": "exploreMetricsRelatedLogs", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { @@ -1579,38 +1338,10 @@ "frontend": true } }, - { - "metadata": { - "name": "expressionParser", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-17T00:59:11Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enable new expression parser", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true - } - }, - { - "metadata": { - "name": "extensionSidebar", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-04-03T10:16:35Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables the extension sidebar", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true - } - }, { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -1622,7 +1353,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-05-10T13:37:04Z", "deletionTimestamp": "2025-05-20T08:18:08Z" }, @@ -1633,27 +1364,10 @@ "frontend": true } }, - { - "metadata": { - "name": "extractFieldsNameDeduplication", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-02T15:47:42Z", - "deletionTimestamp": "2025-11-12T10:08:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Make sure extracted field names are unique in the dataframe", - "stage": "experimental", - "codeowner": "@grafana/datapro", - "frontend": true - } - }, { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -1666,7 +1380,7 @@ { "metadata": { "name": "favoriteDatasources", - "resourceVersion": "1754648387873", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1680,7 +1394,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -1690,42 +1404,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "featureToggleAdminPage", - "resourceVersion": "1758022099771", - "creationTimestamp": "2023-07-18T20:43:32Z", - "deletionTimestamp": "2025-08-29T14:46:39Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-16 11:28:19.771156 +0000 UTC" - } - }, - "spec": { - "description": "Enable admin page for managing feature toggles from the Grafana front-end. Grafana Cloud only.", - "stage": "experimental", - "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromDocs": true - } - }, - { - "metadata": { - "name": "feedbackButton", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-12-02T17:08:15Z", - "deletionTimestamp": "2025-11-20T09:53:52Z" - }, - "spec": { - "description": "Enables a button to send feedback from the Grafana UI", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromDocs": true - } - }, { "metadata": { "name": "fetchRulesUsingPost", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-29T12:17:44Z" }, "spec": { @@ -1735,25 +1417,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "filterOutBotsFromFrontendLogs", - "resourceVersion": "1758000919535", - "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z" - }, - "spec": { - "description": "Filter out bots from collecting data for Frontend Observability", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "expression": "false" - } - }, { "metadata": { "name": "foldersAppPlatformAPI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-03T14:15:23Z" }, "spec": { @@ -1765,28 +1432,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "formatString", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-10-13T18:17:12Z", - "deletionTimestamp": "2025-11-17T13:06:30Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enable format string transformer", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1800,7 +1449,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1814,7 +1463,7 @@ { "metadata": { "name": "grafanaAdvisor", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { @@ -1826,12 +1475,9 @@ { "metadata": { "name": "grafanaAssistantInProfilesDrilldown", - "resourceVersion": "1754572610001", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-08-07 13:16:50.001205 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables integration with Grafana Assistant in Profiles Drilldown", @@ -1841,24 +1487,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "grafanaAssistantInProfilesDrillfown", - "resourceVersion": "1754034112469", - "creationTimestamp": "2025-08-01T07:41:52Z", - "deletionTimestamp": "2025-08-01T07:43:17Z" - }, - "spec": { - "description": "Enables interation with Grafana Assitant in Profiles Drilldown", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true - } - }, { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-22T17:53:16Z", "deletionTimestamp": "2025-05-19T10:15:49Z" }, @@ -1869,26 +1501,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "grafanaPathfinder", - "resourceVersion": "1760434668782", - "creationTimestamp": "2025-10-14T10:40:40Z", - "deletionTimestamp": "2025-10-22T08:06:21Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-14 09:37:48.782577 +0000 UTC" - } - }, - "spec": { - "description": "Enables Pathfinder app", - "stage": "preview", - "codeowner": "@grafana/pathfinder" - } - }, { "metadata": { "name": "grafanaconThemes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-06T11:08:04Z" }, "spec": { @@ -1903,7 +1519,7 @@ { "metadata": { "name": "graphiteBackendMode", - "resourceVersion": "1755870507537", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1917,7 +1533,7 @@ { "metadata": { "name": "groupAttributeSync", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { @@ -1930,7 +1546,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1940,28 +1556,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "groupToNestedTableTransformation", - "resourceVersion": "1762442825881", - "creationTimestamp": "2024-02-07T14:28:26Z", - "deletionTimestamp": "2025-11-17T11:57:22Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enables the group to nested table transformation", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "grpcServer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1973,7 +1571,7 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { @@ -1986,7 +1584,7 @@ { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-09T17:02:49Z" }, "spec": { @@ -1999,7 +1597,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -2011,7 +1609,7 @@ { "metadata": { "name": "infinityRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-14T12:54:04Z" }, "spec": { @@ -2023,7 +1621,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -2038,7 +1636,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -2050,7 +1648,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -2062,7 +1660,7 @@ { "metadata": { "name": "interactiveLearning", - "resourceVersion": "1761063016739", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-22T08:06:21Z" }, "spec": { @@ -2074,7 +1672,7 @@ { "metadata": { "name": "investigationsBackend", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { @@ -2084,25 +1682,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "inviteUserExperimental", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-07T19:09:59Z", - "deletionTimestamp": "2025-11-14T15:33:26Z" - }, - "spec": { - "description": "Renders invite user button along the app", - "stage": "experimental", - "codeowner": "@grafana/sharing-squad", - "frontend": true, - "hideFromDocs": true - } - }, { "metadata": { "name": "jaegerEnableGrpcEndpoint", - "resourceVersion": "1760451551713", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-31T18:19:16Z" }, "spec": { @@ -2114,7 +1697,7 @@ { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -2128,7 +1711,7 @@ { "metadata": { "name": "k8SFolderCounts", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -2141,7 +1724,7 @@ { "metadata": { "name": "k8SFolderMove", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -2154,7 +1737,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -2167,7 +1750,7 @@ { "metadata": { "name": "kubernetesAggregatorCapTokenAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-15T18:14:23Z" }, "spec": { @@ -2180,7 +1763,7 @@ { "metadata": { "name": "kubernetesAlertingRules", - "resourceVersion": "1754340669702", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2194,7 +1777,7 @@ { "metadata": { "name": "kubernetesAnnotations", - "resourceVersion": "1761142826172", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-06T18:22:20Z" }, "spec": { @@ -2207,7 +1790,7 @@ { "metadata": { "name": "kubernetesAuthZHandlerRedirect", - "resourceVersion": "1758820248165", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2221,11 +1804,8 @@ { "metadata": { "name": "kubernetesAuthnMutation", - "resourceVersion": "1753454405614", - "creationTimestamp": "2025-07-25T15:05:32Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-25 14:40:05.614358 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-25T15:05:32Z" }, "spec": { "description": "Enables create, delete, and update mutations for resources owned by IAM identity", @@ -2237,7 +1817,7 @@ { "metadata": { "name": "kubernetesAuthzApis", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-18T07:43:01Z" }, "spec": { @@ -2247,24 +1827,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "kubernetesAuthzEndpoints", - "resourceVersion": "1758779666607", - "creationTimestamp": "2025-09-25T05:54:26Z", - "deletionTimestamp": "2025-09-25T17:10:48Z" - }, - "spec": { - "description": "Enables K8s AuthZ endpoints", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true - } - }, { "metadata": { "name": "kubernetesAuthzResourcePermissionApis", - "resourceVersion": "1754668670559", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2278,11 +1844,8 @@ { "metadata": { "name": "kubernetesAuthzZanzanaSync", - "resourceVersion": "1758887751768", - "creationTimestamp": "2025-10-13T19:37:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-13T19:37:13Z" }, "spec": { "description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations", @@ -2294,7 +1857,7 @@ { "metadata": { "name": "kubernetesCorrelations", - "resourceVersion": "1757513374180", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2308,11 +1871,8 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1763533458962", - "creationTimestamp": "2024-06-05T14:34:23Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { "description": "Use the kubernetes API in the frontend for dashboards", @@ -2324,7 +1884,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -2337,7 +1897,7 @@ { "metadata": { "name": "kubernetesLibraryPanels", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-25T22:21:56Z" }, "spec": { @@ -2350,11 +1910,8 @@ { "metadata": { "name": "kubernetesLogsDrilldown", - "resourceVersion": "1760632282014", - "creationTimestamp": "2025-10-16T21:31:42Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-16 16:31:22.014483 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-16T21:31:42Z" }, "spec": { "description": "Adds support for Kubernetes logs drilldown", @@ -2366,7 +1923,7 @@ { "metadata": { "name": "kubernetesQueryCaching", - "resourceVersion": "1760972620939", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-20T16:11:25Z" }, "spec": { @@ -2379,12 +1936,9 @@ { "metadata": { "name": "kubernetesShortURLs", - "resourceVersion": "1756914263808", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables k8s short url api and uses it under the hood when handling legacy /api", @@ -2396,7 +1950,7 @@ { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -2409,7 +1963,7 @@ { "metadata": { "name": "kubernetesStars", - "resourceVersion": "1759149842036", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2420,24 +1974,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "kubernetesZanzanaPopulate", - "resourceVersion": "1758879077577", - "creationTimestamp": "2025-09-26T09:31:17Z", - "deletionTimestamp": "2025-09-26T09:35:02Z" - }, - "spec": { - "description": "Populate Zanzana on AuthZ CRDs creation or update", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true - } - }, { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-31T13:59:07Z" }, "spec": { @@ -2446,23 +1986,10 @@ "codeowner": "@grafana/grafana-frontend-platform" } }, - { - "metadata": { - "name": "localizationForPlugins", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-31T04:38:38Z", - "deletionTimestamp": "2025-08-29T14:46:39Z" - }, - "spec": { - "description": "Enables localization for plugins", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" - } - }, { "metadata": { "name": "logQLScope", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { @@ -2476,7 +2003,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -2485,25 +2012,10 @@ "codeowner": "@grafana/grafana-backend-group" } }, - { - "metadata": { - "name": "logRowsPopoverMenu", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-16T09:48:10Z", - "deletionTimestamp": "2025-11-07T10:57:27Z" - }, - "spec": { - "description": "Enable filtering menu displayed when text of a log line is selected", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -2517,7 +2029,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -2530,7 +2042,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -2541,25 +2053,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "logsInfiniteScrolling", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-09T10:54:03Z", - "deletionTimestamp": "2025-11-07T10:59:01Z" - }, - "spec": { - "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "logsPanelControls", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-07T14:38:55Z" }, "spec": { @@ -2573,7 +2070,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -2585,7 +2082,7 @@ { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { @@ -2598,7 +2095,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -2607,10 +2104,23 @@ "codeowner": "@grafana/observability-logs" } }, + { + "metadata": { + "name": "lokiQueryLimitsContext", + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" + }, + "spec": { + "description": "Send X-Loki-Query-Limits-Context header to Loki on first split request", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -2624,7 +2134,7 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -2636,7 +2146,7 @@ { "metadata": { "name": "lokiShardSplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-23T11:21:03Z" }, "spec": { @@ -2649,7 +2159,7 @@ { "metadata": { "name": "managedDualWriter", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-19T14:50:39Z" }, "spec": { @@ -2662,7 +2172,7 @@ { "metadata": { "name": "metricsFromProfiles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-09T10:55:28Z" }, "spec": { @@ -2675,7 +2185,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -2684,23 +2194,10 @@ "codeowner": "@grafana/alerting-squad" } }, - { - "metadata": { - "name": "multiTenantFrontend", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-04-25T09:24:25Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Register MT frontend", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" - } - }, { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T20:25:50Z" }, "spec": { @@ -2713,7 +2210,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -2725,7 +2222,7 @@ { "metadata": { "name": "newClickhouseConfigPageDesign", - "resourceVersion": "1754075145003", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2736,25 +2233,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "newDashboardSharingComponent", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-03T15:02:18Z", - "deletionTimestamp": "2025-08-29T14:46:39Z" - }, - "spec": { - "description": "Enables the new sharing drawer design", - "stage": "GA", - "codeowner": "@grafana/sharing-squad", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -2767,7 +2249,7 @@ { "metadata": { "name": "newFiltersUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { @@ -2780,7 +2262,7 @@ { "metadata": { "name": "newGauge", - "resourceVersion": "1760700645318", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-20T16:33:19Z" }, "spec": { @@ -2794,7 +2276,7 @@ { "metadata": { "name": "newInfluxDSConfigPageDesign", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-25T16:39:54Z" }, "spec": { @@ -2807,7 +2289,7 @@ { "metadata": { "name": "newLogContext", - "resourceVersion": "1754044501326", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z" }, "spec": { @@ -2820,11 +2302,8 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1762166984808", - "creationTimestamp": "2025-02-04T17:40:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-03 10:49:44.808226 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-02-04T17:40:17Z" }, "spec": { "description": "Enables the new logs panel", @@ -2834,24 +2313,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "newPDFRendering", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-08T12:09:34Z", - "deletionTimestamp": "2025-11-14T14:39:41Z" - }, - "spec": { - "description": "New implementation for the dashboard-to-PDF rendering", - "stage": "GA", - "codeowner": "@grafana/grafana-operator-experience-squad", - "expression": "true" - } - }, { "metadata": { "name": "newPanelPadding", - "resourceVersion": "1760780310038", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-12T15:40:46Z" }, "spec": { @@ -2864,7 +2329,7 @@ { "metadata": { "name": "newShareReportDrawer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-17T19:05:46Z" }, "spec": { @@ -2877,8 +2342,8 @@ { "metadata": { "name": "newTimeRangeZoomShortcuts", - "resourceVersion": "1763646782694", - "creationTimestamp": "2025-11-20T13:53:02Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Enables new keyboard shortcuts for time range zoom operations", @@ -2890,7 +2355,7 @@ { "metadata": { "name": "newVizSuggestions", - "resourceVersion": "1762456851857", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-12T19:26:29Z" }, "spec": { @@ -2904,7 +2369,7 @@ { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -2917,7 +2382,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -2930,11 +2395,8 @@ { "metadata": { "name": "onlyStoreActionSets", - "resourceVersion": "1759844046154", - "creationTimestamp": "2025-10-20T15:02:56Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-07 13:34:06.15476 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-20T15:02:56Z" }, "spec": { "description": "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", @@ -2947,7 +2409,7 @@ { "metadata": { "name": "otelLogsFormatting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T15:42:14Z" }, "spec": { @@ -2960,7 +2422,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -2971,39 +2433,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "panelMonitoring", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-10-09T05:19:08Z", - "deletionTimestamp": "2025-11-07T19:04:42Z" - }, - "spec": { - "description": "Enables panel monitoring through logs and measurements", - "stage": "GA", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "panelPadding", - "resourceVersion": "1760779980125", - "creationTimestamp": "2025-10-18T09:33:00Z", - "deletionTimestamp": "2025-10-18T09:38:30Z" - }, - "spec": { - "description": "Increases panel padding globally", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "expression": "false" - } - }, { "metadata": { "name": "panelTimeSettings", - "resourceVersion": "1761555646368", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-29T08:06:23Z" }, "spec": { @@ -3015,7 +2448,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -3027,7 +2460,7 @@ { "metadata": { "name": "passwordlessMagicLinkAuthentication", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { @@ -3040,7 +2473,7 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { @@ -3052,7 +2485,7 @@ { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -3061,27 +2494,10 @@ "codeowner": "@grafana/search-and-storage" } }, - { - "metadata": { - "name": "pinNavItems", - "resourceVersion": "1762958248290", - "creationTimestamp": "2024-06-10T11:40:03Z", - "deletionTimestamp": "2025-11-17T12:12:47Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } - }, - "spec": { - "description": "Enables pinning of nav items", - "stage": "GA", - "codeowner": "@grafana/grafana-search-navigate-organise", - "expression": "true" - } - }, { "metadata": { "name": "playlistsReconciler", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-20T03:09:31Z" }, "spec": { @@ -3091,26 +2507,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "pluginAssetProvider", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-17T15:20:35Z", - "deletionTimestamp": "2025-10-10T09:35:22Z" - }, - "spec": { - "description": "Allows decoupled core plugins to load from the Grafana CDN", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "pluginContainers", - "resourceVersion": "1756911074581", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3125,7 +2525,7 @@ { "metadata": { "name": "pluginInstallAPISync", - "resourceVersion": "1760543624249", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-24T12:09:26Z" }, "spec": { @@ -3138,7 +2538,7 @@ { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -3151,11 +2551,8 @@ { "metadata": { "name": "pluginStoreServiceLoading", - "resourceVersion": "1761144346944", - "creationTimestamp": "2025-10-17T20:01:43Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-22 14:45:46.944669 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-17T20:01:43Z" }, "spec": { "description": "Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded", @@ -3167,7 +2564,7 @@ { "metadata": { "name": "pluginsAutoUpdate", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-16T11:44:39Z" }, "spec": { @@ -3176,36 +2573,10 @@ "codeowner": "@grafana/plugins-platform-backend" } }, - { - "metadata": { - "name": "pluginsFrontendSandbox", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-05T08:51:36Z", - "deletionTimestamp": "2025-11-06T10:06:53Z" - }, - "spec": { - "description": "Enables the plugins frontend sandbox", - "stage": "privatePreview", - "codeowner": "@grafana/plugins-platform-backend" - } - }, - { - "metadata": { - "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-15T17:09:14Z", - "deletionTimestamp": "2025-11-13T15:31:57Z" - }, - "spec": { - "description": "Disables passing host environment variable to plugin processes", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" - } - }, { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { @@ -3218,7 +2589,7 @@ { "metadata": { "name": "postgresDSUsePGX", - "resourceVersion": "1753174666056", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-26T06:54:18Z", "deletionTimestamp": "2025-06-03T12:45:07Z" }, @@ -3231,7 +2602,7 @@ { "metadata": { "name": "preferLibraryPanelTitle", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-17T11:21:21Z" }, "spec": { @@ -3241,24 +2612,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "preinstallAutoUpdate", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-11-07T12:14:25Z", - "deletionTimestamp": "2025-11-10T14:06:30Z" - }, - "spec": { - "description": "Enables automatic updates for pre-installed plugins", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "true" - } - }, { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -3271,7 +2628,7 @@ { "metadata": { "name": "preventPanelChromeOverflow", - "resourceVersion": "1760704390127", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-17T14:40:08Z" }, "spec": { @@ -3282,25 +2639,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "promQLScope", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-01-29T20:22:17Z", - "deletionTimestamp": "2025-10-10T14:53:18Z" - }, - "spec": { - "description": "In-development feature that will allow injection of labels into prometheus queries.", - "stage": "GA", - "codeowner": "@grafana/oss-big-tent", - "hideFromDocs": true, - "expression": "true" - } - }, { "metadata": { "name": "prometheusAzureOverrideAudience", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-05-30T15:43:32Z", "deletionTimestamp": "2023-07-16T21:30:14Z" }, @@ -3311,24 +2653,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "prometheusCodeModeMetricNamesSearch", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-04-04T20:38:23Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names", - "stage": "experimental", - "codeowner": "@grafana/oss-big-tent", - "frontend": true - } - }, { "metadata": { "name": "prometheusSpecialCharsInLabelValues", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T21:31:08Z" }, "spec": { @@ -3341,12 +2669,9 @@ { "metadata": { "name": "prometheusTypeMigration", - "resourceVersion": "1757089774247", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-05 16:29:34.247055837 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Checks for deprecated Prometheus authentication methods (SigV4 and Azure), installs the relevant data source, and migrates the Prometheus data sources", @@ -3359,7 +2684,7 @@ { "metadata": { "name": "provisioning", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { @@ -3372,7 +2697,7 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { @@ -3385,7 +2710,7 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { @@ -3399,7 +2724,7 @@ { "metadata": { "name": "queryCacheRequestDeduplication", - "resourceVersion": "1757521912495", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3413,12 +2738,9 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1758208636622", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-07T18:31:45Z", - "deletionTimestamp": "2023-03-20T16:00:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-18 15:17:16.622111 +0000 UTC" - } + "deletionTimestamp": "2023-03-20T16:00:14Z" }, "spec": { "description": "Enables Saved queries (query library) feature", @@ -3429,7 +2751,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3442,7 +2764,7 @@ { "metadata": { "name": "queryServiceFromExplore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T10:00:33Z" }, "spec": { @@ -3455,7 +2777,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3468,7 +2790,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3481,7 +2803,7 @@ { "metadata": { "name": "queryServiceWithConnections", - "resourceVersion": "1756367172351", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-28T19:28:26Z", "deletionTimestamp": "2025-08-29T12:49:57Z" }, @@ -3492,24 +2814,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "recordedQueriesMulti", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-14T12:34:22Z", - "deletionTimestamp": "2025-11-10T20:31:43Z" - }, - "spec": { - "description": "Enables writing multiple items from a single query within Recorded Queries", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "expression": "true" - } - }, { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -3521,8 +2829,8 @@ { "metadata": { "name": "refreshTokenRequired", - "resourceVersion": "1763561990273", - "creationTimestamp": "2025-11-19T14:19:50Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Require that refresh tokens are present in oauth tokens.", @@ -3531,27 +2839,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "regressionTransformation", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-24T14:49:16Z", - "deletionTimestamp": "2025-07-01T13:59:22Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enables regression analysis transformation", - "stage": "preview", - "codeowner": "@grafana/datapro", - "frontend": true - } - }, { "metadata": { "name": "reloadDashboardsOnParamsChange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { @@ -3564,7 +2855,7 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { @@ -3576,7 +2867,7 @@ { "metadata": { "name": "rendererDisableAppPluginsPreload", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-24T14:43:06Z" }, "spec": { @@ -3590,7 +2881,7 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { @@ -3603,11 +2894,8 @@ { "metadata": { "name": "restoreDashboards", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-05-23T14:35:54Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-05-23T14:35:54Z" }, "spec": { "description": "Enables restore deleted dashboards feature", @@ -3619,12 +2907,9 @@ { "metadata": { "name": "restrictedPluginApis", - "resourceVersion": "1753776783657", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-29 08:13:03.657209 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables sharing a list of APIs with a list of plugins", @@ -3638,7 +2923,7 @@ { "metadata": { "name": "rolePickerDrawer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { @@ -3650,8 +2935,8 @@ { "metadata": { "name": "rudderstackUpgrade", - "resourceVersion": "1763481217872", - "creationTimestamp": "2025-11-18T15:53:37Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Enables the new version of rudderstack", @@ -3661,26 +2946,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "savedQueries", - "resourceVersion": "1756920131554", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 17:22:11.554759 +0000 UTC" - } - }, - "spec": { - "description": "Enables Saved Queries feature", - "stage": "preview", - "codeowner": "@grafana/sharing-squad" - } - }, { "metadata": { "name": "scanRowInvalidDashboardParseFallbackEnabled", - "resourceVersion": "1753730899886", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-30T14:18:38Z" }, "spec": { @@ -3692,7 +2961,7 @@ { "metadata": { "name": "scopeApi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { @@ -3705,7 +2974,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -3718,11 +2987,8 @@ { "metadata": { "name": "scopeSearchAllLevels", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-04-14T07:42:16Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-04-14T07:42:16Z" }, "spec": { "description": "Enable scope search to include all levels of the scope node tree", @@ -3734,7 +3000,7 @@ { "metadata": { "name": "secretsManagementAppPlatform", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-19T09:25:14Z" }, "spec": { @@ -3746,7 +3012,7 @@ { "metadata": { "name": "secretsManagementAppPlatformUI", - "resourceVersion": "1756816818369", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3759,11 +3025,8 @@ { "metadata": { "name": "sharingDashboardImage", - "resourceVersion": "1761782859498", - "creationTimestamp": "2025-07-15T21:07:39Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-30 00:07:39.498343 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-15T21:07:39Z" }, "spec": { "description": "Enables image sharing functionality for dashboards", @@ -3776,7 +3039,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -3785,43 +3048,11 @@ "codeowner": "@grafana/dashboards-squad" } }, - { - "metadata": { - "name": "skipTokenRotationIfRecent", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-06-03T06:59:40Z", - "deletionTimestamp": "2025-10-23T08:02:41Z" - }, - "spec": { - "description": "Skip token rotation if it was already rotated less than 5 seconds ago", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-06T16:28:52Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables previous SQL data source dataset dropdown behavior", - "stage": "preview", - "codeowner": "@grafana/oss-big-tent", - "frontend": true - } - }, { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1756828051955", - "creationTimestamp": "2024-02-27T21:16:00Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 15:47:31.955288671 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { "description": "Enables SQL Expressions, which can execute SQL queries against data source results.", @@ -3832,7 +3063,7 @@ { "metadata": { "name": "sqlExpressionsColumnAutoComplete", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T21:49:58Z" }, "spec": { @@ -3845,11 +3076,8 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1762552416963", - "creationTimestamp": "2023-09-07T20:02:07Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", @@ -3860,7 +3088,7 @@ { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-18T11:31:27Z" }, "spec": { @@ -3874,12 +3102,9 @@ { "metadata": { "name": "starsFromAPIServer", - "resourceVersion": "1762958248290", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "populate star status from apiserver", @@ -3892,7 +3117,7 @@ { "metadata": { "name": "storage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -3904,11 +3129,8 @@ { "metadata": { "name": "suggestedDashboards", - "resourceVersion": "1763643877862", - "creationTimestamp": "2025-11-07T13:38:59Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-07T13:38:59Z" }, "spec": { "description": "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", @@ -3916,24 +3138,10 @@ "codeowner": "@grafana/sharing-squad" } }, - { - "metadata": { - "name": "tableNextGen", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-26T03:57:57Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Allows access to the new react-data-grid based table component.", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true - } - }, { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -3946,7 +3154,7 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-24T11:52:03Z" }, "spec": { @@ -3959,7 +3167,7 @@ { "metadata": { "name": "teamFolders", - "resourceVersion": "1755099058649", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3970,24 +3178,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "teamHttpHeadersMimir", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-01-13T10:42:47Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "expression": "true" - } - }, { "metadata": { "name": "teamHttpHeadersTempo", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-22T19:13:31Z" }, "spec": { @@ -3996,42 +3190,11 @@ "codeowner": "@grafana/identity-access-team" } }, - { - "metadata": { - "name": "templateDashboards", - "resourceVersion": "1758897202631", - "creationTimestamp": "2025-09-26T14:33:22Z", - "deletionTimestamp": "2025-09-26T16:02:12Z" - }, - "spec": { - "description": "Enables template dashboards suggestions when creating new dashboards", - "stage": "experimental", - "codeowner": "@grafana/sharing-squad", - "frontend": true - } - }, - { - "metadata": { - "name": "templateVariablesUsesCombobox", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-01-31T09:53:13Z", - "deletionTimestamp": "2025-11-13T03:31:18Z" - }, - "spec": { - "description": "Use new **Combobox** component for template variables", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true - } - }, { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1763532089512", - "creationTimestamp": "2025-07-15T13:36:36Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:01:29.512182 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-15T13:36:36Z" }, "spec": { "description": "Enables creating alerts from Tempo data source", @@ -4042,12 +3205,9 @@ { "metadata": { "name": "tempoSearchBackendMigration", - "resourceVersion": "1758029567165", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-16 13:32:47.165146 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Run search queries through the tempo backend", @@ -4060,7 +3220,7 @@ { "metadata": { "name": "timeComparison", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-24T20:07:28Z" }, "spec": { @@ -4073,12 +3233,8 @@ { "metadata": { "name": "timeRangePan", - "resourceVersion": "1762290731154", - "creationTimestamp": "2025-11-05T01:39:46Z", - "deletionTimestamp": "2025-11-06T17:39:31Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-05T01:39:46Z" }, "spec": { "description": "Enables time range panning functionality", @@ -4090,7 +3246,7 @@ { "metadata": { "name": "timeRangeProvider", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { @@ -4099,28 +3255,11 @@ "codeowner": "@grafana/grafana-frontend-platform" } }, - { - "metadata": { - "name": "tlsMemcached", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-09T19:12:08Z", - "deletionTimestamp": "2025-11-12T15:49:28Z" - }, - "spec": { - "description": "Use TLS-enabled memcached in the enterprise caching feature", - "stage": "GA", - "codeowner": "@grafana/grafana-operator-experience-squad", - "expression": "true" - } - }, { "metadata": { "name": "transformationsEmptyPlaceholder", - "resourceVersion": "1763373021129", - "creationTimestamp": "2025-11-17T13:57:05Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-17 09:50:21.129721 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-17T13:57:05Z" }, "spec": { "description": "Show transformation quick-start cards in empty transformations state", @@ -4129,25 +3268,10 @@ "frontend": true } }, - { - "metadata": { - "name": "transformationsRedesign", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-07-12T16:35:49Z", - "deletionTimestamp": "2025-11-06T20:37:15Z" - }, - "spec": { - "description": "Enables the transformations redesign", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "ttlPluginInstanceManager", - "resourceVersion": "1763462850634", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-18T11:17:23Z" }, "spec": { @@ -4160,11 +3284,8 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1762958248290", - "creationTimestamp": "2024-12-13T10:41:18Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { "description": "Displays the navigation history so the user can navigate back to previous pages", @@ -4176,7 +3297,7 @@ { "metadata": { "name": "unifiedNavbars", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-09T12:51:22Z" }, "spec": { @@ -4190,7 +3311,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -4203,7 +3324,7 @@ { "metadata": { "name": "unifiedStorageBigObjectsSupport", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { @@ -4215,7 +3336,7 @@ { "metadata": { "name": "unifiedStorageGrpcConnectionPool", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-21T13:24:54Z" }, "spec": { @@ -4225,25 +3346,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageHistoryPruner", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-17T10:36:38Z", - "deletionTimestamp": "2025-11-17T19:47:37Z" - }, - "spec": { - "description": "Enables the unified storage history pruner", - "stage": "GA", - "codeowner": "@grafana/search-and-storage", - "hideFromDocs": true, - "expression": "true" - } - }, { "metadata": { "name": "unifiedStorageSearch", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { @@ -4253,29 +3359,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageSearchAfterWriteExperimentalAPI", - "resourceVersion": "1755089543487", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-08-13 12:52:23.487521 +0000 UTC" - } - }, - "spec": { - "description": "Enable experimental search-after-write guarantees to unified-storage search endpoints", - "stage": "experimental", - "codeowner": "@grafana/search-and-storage", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "unifiedStorageSearchDualReaderEnabled", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-18T12:43:56Z" }, "spec": { @@ -4288,7 +3375,7 @@ { "metadata": { "name": "unifiedStorageSearchSprinkles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { @@ -4301,7 +3388,7 @@ { "metadata": { "name": "unifiedStorageSearchUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-19T18:21:48Z" }, "spec": { @@ -4311,24 +3398,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageUseFullNgram", - "resourceVersion": "1758820248165", - "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z" - }, - "spec": { - "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", - "stage": "experimental", - "codeowner": "@grafana/search-and-storage", - "hideFromDocs": true - } - }, { "metadata": { "name": "unlimitedLayoutsNesting", - "resourceVersion": "1760013838902", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-10T12:15:54Z" }, "spec": { @@ -4341,12 +3414,9 @@ { "metadata": { "name": "useKubernetesShortURLsAPI", - "resourceVersion": "1756914263808", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Routes short url requests from /api to the /apis endpoint in the frontend. Depends on kubernetesShortURLs", @@ -4358,7 +3428,7 @@ { "metadata": { "name": "useMultipleScopeNodesEndpoint", - "resourceVersion": "1759237515008", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -4374,7 +3444,7 @@ { "metadata": { "name": "useScopeSingleNodeEndpoint", - "resourceVersion": "1753960766702", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T14:32:41Z" }, "spec": { @@ -4389,11 +3459,8 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-03-31T15:20:00Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-03-31T15:20:00Z" }, "spec": { "description": "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", @@ -4406,7 +3473,7 @@ { "metadata": { "name": "useSessionStorageForRedirection", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { @@ -4419,12 +3486,9 @@ { "metadata": { "name": "vizActionsAuth", - "resourceVersion": "1756904995830", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 13:09:55.830412 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Allows authenticated API calls in actions", @@ -4437,7 +3501,7 @@ { "metadata": { "name": "zanzana", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-19T13:59:47Z" }, "spec": { @@ -4450,11 +3514,8 @@ { "metadata": { "name": "zanzanaNoLegacyClient", - "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-21T14:03:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-21 16:10:16.739546 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-21T14:03:17Z" }, "spec": { "description": "Use openFGA as main authorization engine and disable legacy RBAC clietn.", diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index eb7f72e6570..8d5d4c6282f 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -96,6 +96,8 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return nil, backend.DownstreamError(fmt.Errorf("failed to create request: %w", err)) } + addQueryLimitsHeader(query, req) + if query.SupportingQueryType != SupportingQueryNone { value := getSupportingQueryHeaderValue(query.SupportingQueryType) if value != "" { @@ -108,6 +110,15 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return req, nil } +func addQueryLimitsHeader(query lokiQuery, req *http.Request) { + if len(query.LimitsContext.Expr) > 0 { + queryLimitStr, err := json.Marshal(query.LimitsContext) + if err == nil { + req.Header.Set("X-Loki-Query-Limits-Context", string(queryLimitStr)) + } + } +} + type lokiResponseError struct { Message string `json:"message"` TraceID string `json:"traceID,omitempty"` diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 29b15e7fb18..415f807cf10 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -2,10 +2,12 @@ package loki import ( "context" + "encoding/json" "fmt" "net/http" "strings" "testing" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery" @@ -47,6 +49,56 @@ func TestApiLogVolume(t *testing.T) { require.True(t, called) }) + t.Run("X-Loki-Query-Limits-Context header should be set when LimitsContext is provided", func(t *testing.T) { + called := false + from := time.Now().Truncate(time.Millisecond).Add(-1 * time.Hour) + to := time.Now().Truncate(time.Millisecond) + limitsContext := LimitsContext{ + Expr: "{cluster=\"us-central1\"}", + From: from, + To: to, + } + + limitsContextJson, _ := json.Marshal(limitsContext) + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, string(limitsContextJson), req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange, LimitsContext: limitsContext}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("X-Loki-Query-Limits-Context header should not get set when LimitsContext is missing expr", func(t *testing.T) { + called := false + from := time.Now().Truncate(time.Millisecond).Add(-1 * time.Hour) + to := time.Now().Truncate(time.Millisecond) + limitsContext := LimitsContext{ + Expr: "", + From: from, + To: to, + } + + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "", req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange, LimitsContext: limitsContext}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("X-Loki-Query-Limits-Context header should not get set when LimitsContext is not provided", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "", req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + t.Run("data sample queries should set data sample http header", func(t *testing.T) { called := false api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { diff --git a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go index c9764b174fd..9826c494599 100644 --- a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go @@ -18,6 +18,17 @@ const ( QueryEditorModeBuilder QueryEditorMode = "builder" ) +type LimitsContext struct { + Expr string `json:"expr"` + From int64 `json:"from"` + To int64 `json:"to"` +} + +// NewLimitsContext creates a new LimitsContext object. +func NewLimitsContext() *LimitsContext { + return &LimitsContext{} +} + type LokiQueryType string const ( @@ -59,6 +70,8 @@ type LokiDataQuery struct { Instant *bool `json:"instant,omitempty"` // Used to set step value for range queries. Step *string `json:"step,omitempty"` + // The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + LimitsContext *LimitsContext `json:"limitsContext,omitempty"` // A unique identifier for the query within the list of targets. // In server side expressions, the refId is used as a variable name to identify results. // By default, the UI will assign A->Z; however setting meaningful names may be useful. diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 06cf90cb339..0bbf76c4f05 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -156,6 +156,8 @@ func parseQuery(queryContext *backend.QueryDataRequest, logqlScopesEnabled bool) expr := interpolateVariables(model.Expr, interval, timeRange, queryType, step) + limitsConfig := generateLimitsConfig(model, interval, timeRange, queryType, step) + direction, err := parseDirection(model.Direction) if err != nil { return nil, err @@ -192,8 +194,21 @@ func parseQuery(queryContext *backend.QueryDataRequest, logqlScopesEnabled bool) RefID: query.RefID, SupportingQueryType: supportingQueryType, Scopes: model.Scopes, + LimitsContext: limitsConfig, }) } return qs, nil } + +func generateLimitsConfig(model *QueryJSONModel, interval time.Duration, timeRange time.Duration, queryType QueryType, step time.Duration) LimitsContext { + var limitsConfig LimitsContext + // Only supply limits context config if we have expression, and from and to + if model.LimitsContext != nil && model.LimitsContext.Expr != "" && model.LimitsContext.From > 0 && model.LimitsContext.To > 0 { + // If a limits expression was provided, interpolate it and parse the time range + limitsConfig.Expr = interpolateVariables(model.LimitsContext.Expr, interval, timeRange, queryType, step) + limitsConfig.From = time.UnixMilli(model.LimitsContext.From) + limitsConfig.To = time.UnixMilli(model.LimitsContext.To) + } + return limitsConfig +} diff --git a/pkg/tsdb/loki/parse_query_test.go b/pkg/tsdb/loki/parse_query_test.go index 497f1c880c8..f8028e0dcfe 100644 --- a/pkg/tsdb/loki/parse_query_test.go +++ b/pkg/tsdb/loki/parse_query_test.go @@ -1,6 +1,7 @@ package loki import ( + "strconv" "testing" "time" @@ -145,6 +146,74 @@ func TestParseQuery(t *testing.T) { require.Equal(t, `{namespace="logish"} |= "problems"`, models[0].Expr) }) + t.Run("parsing query model with invalid query limits context expr", func(t *testing.T) { + from := time.Now().Add(-3000 * time.Second) + fullFrom := time.Now().Add(-1 * time.Hour) + to := time.Now() + + queryContext := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(` + { + "expr": "count_over_time({service_name=\"apache\", __stream_shard__=\"2\"}[$__auto])", + "format": "time_series", + "refId": "A", + "limitsContext": {"expr": "", "from": ` + strconv.FormatInt(fullFrom.UnixMilli(), 10) + `, "to": ` + strconv.FormatInt(to.UnixMilli(), 10) + `} + }`, + ), + TimeRange: backend.TimeRange{ + From: from, + To: to, + }, + Interval: time.Second * 15, + MaxDataPoints: 200, + }, + }, + } + models, err := parseQuery(queryContext, true) + require.NoError(t, err) + require.Equal(t, `count_over_time({service_name="apache", __stream_shard__="2"}[15s])`, models[0].Expr) + // If the limits context expression is missing, we don't set any limits context + require.Equal(t, ``, models[0].LimitsContext.Expr) + require.Equal(t, time.Time{}, models[0].LimitsContext.To) + require.Equal(t, time.Time{}, models[0].LimitsContext.From) + }) + + t.Run("parsing query model with query limits context", func(t *testing.T) { + from := time.Now().Add(-3000 * time.Second) + fullFrom := time.Now().Add(-1 * time.Hour) + to := time.Now() + + queryContext := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(` + { + "expr": "count_over_time({service_name=\"apache\", __stream_shard__=\"2\"}[$__auto])", + "format": "time_series", + "refId": "A", + "limitsContext": {"expr": "count_over_time({service_name=\"apache\"}[$__auto])", "from": ` + strconv.FormatInt(fullFrom.UnixMilli(), 10) + `, "to": ` + strconv.FormatInt(to.UnixMilli(), 10) + `} + }`, + ), + TimeRange: backend.TimeRange{ + From: from, + To: to, + }, + Interval: time.Second * 15, + MaxDataPoints: 200, + }, + }, + } + models, err := parseQuery(queryContext, true) + require.NoError(t, err) + require.Equal(t, time.Second*15, models[0].Step) + require.Equal(t, `count_over_time({service_name="apache", __stream_shard__="2"}[15s])`, models[0].Expr) + require.Equal(t, `count_over_time({service_name="apache"}[15s])`, models[0].LimitsContext.Expr) + require.Equal(t, to.Truncate(time.Millisecond), models[0].LimitsContext.To) + require.Equal(t, fullFrom.Truncate(time.Millisecond), models[0].LimitsContext.From) + }) + t.Run("interpolate variables, range between 1s and 0.5s", func(t *testing.T) { expr := "go_goroutines $__interval $__interval_ms $__range $__range_s $__range_ms" queryType := dataquery.LokiQueryTypeRange diff --git a/pkg/tsdb/loki/types.go b/pkg/tsdb/loki/types.go index 0d6156ed158..dd86595ada0 100644 --- a/pkg/tsdb/loki/types.go +++ b/pkg/tsdb/loki/types.go @@ -11,6 +11,11 @@ import ( type QueryType = dataquery.LokiQueryType type SupportingQueryType = dataquery.SupportingQueryType type Direction = dataquery.LokiQueryDirection +type LimitsContext struct { + Expr string + From time.Time + To time.Time +} const ( QueryTypeRange = dataquery.LokiQueryTypeRange @@ -42,4 +47,5 @@ type lokiQuery struct { RefID string SupportingQueryType SupportingQueryType Scopes []scope.ScopeFilter + LimitsContext LimitsContext } diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 055f4d454b7..d1e7bc6b461 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -774,6 +774,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { diff --git a/public/app/features/explore/Logs/LogsVolumePanelList.tsx b/public/app/features/explore/Logs/LogsVolumePanelList.tsx index 4d0556cfdb5..cfd35f7a73c 100644 --- a/public/app/features/explore/Logs/LogsVolumePanelList.tsx +++ b/public/app/features/explore/Logs/LogsVolumePanelList.tsx @@ -26,7 +26,7 @@ import { mergeLogsVolumeDataFrames, isLogsVolumeLimited, getLogsVolumeMaximumRan import { SupplementaryResultError } from '../SupplementaryResultError'; import { LogsVolumePanel } from './LogsVolumePanel'; -import { isTimeoutErrorResponse } from './utils/logsVolumeResponse'; +import { isMaxBytesErrorResponse, isTimeoutErrorResponse } from './utils/logsVolumeResponse'; type Props = { logsVolumeData: DataQueryResponse | undefined; @@ -93,7 +93,8 @@ export const LogsVolumePanelList = ({ const canShowPartialData = config.featureToggles.lokiShardSplitting && logsVolumeData && logsVolumeData.data.length > 0; const timeoutError = isTimeoutErrorResponse(logsVolumeData); - + const maxBytesError = isMaxBytesErrorResponse(logsVolumeData); + const queryTooLargeError = timeoutError || maxBytesError; const from = dateTime(Math.max(absoluteRange.from, allLogsVolumeMaximumRange.from)); const to = dateTime(Math.min(absoluteRange.to, allLogsVolumeMaximumRange.to)); const visibleRange: TimeRange = { from, to, raw: { from, to } }; @@ -123,7 +124,7 @@ export const LogsVolumePanelList = ({ Loading... ); - } else if (timeoutError && !canShowPartialData) { + } else if (queryTooLargeError && !canShowPartialData) { return (

- - The query is trying to access too much data. Try one or more of the following: - + {timeoutError && ( + + The query is trying to access too much data. Try one or more of the following: + + )} + {maxBytesError && ( + + The query would read too many bytes. Try one or more of the following: + + )}

)} @@ -197,7 +209,7 @@ function getStyles(theme: GrafanaTheme2) { color: 'inherit', lineHeight: 0, }), - nodeName: css({ + nodeButton: css({ boxShadow: 'none', border: 'none', background: 'transparent', @@ -215,11 +227,18 @@ function getStyles(theme: GrafanaTheme2) { textOverflow: 'ellipsis', }, }), + nodeName: css({ + display: 'flex', + gap: theme.spacing(0.5), + flexGrow: 1, + alignItems: 'center', + overflow: 'hidden', + }), hiddenIcon: css({ color: theme.colors.text.secondary, marginLeft: theme.spacing(1), }), - nodeNameClone: css({ + nodeButtonClone: css({ color: theme.colors.text.secondary, cursor: 'not-allowed', }), diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index da18ca17dd4..66b87804f76 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -272,7 +272,7 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), marginBottom: theme.spacing(1), float: 'right', - alignItems: 'center', + alignItems: 'flex-start', }), timeControls: css({ display: 'flex', From f6dfbe0e15066f463fd8df3d410cac9018308805 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:17:24 +0000 Subject: [PATCH 134/423] I18n: Download translations from Crowdin (#114520) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 84 +++++++++++++++++++++++------ public/locales/de-DE/grafana.json | 84 +++++++++++++++++++++++------ public/locales/es-ES/grafana.json | 84 +++++++++++++++++++++++------ public/locales/fr-FR/grafana.json | 84 +++++++++++++++++++++++------ public/locales/hu-HU/grafana.json | 84 +++++++++++++++++++++++------ public/locales/id-ID/grafana.json | 84 +++++++++++++++++++++++------ public/locales/it-IT/grafana.json | 84 +++++++++++++++++++++++------ public/locales/ja-JP/grafana.json | 84 +++++++++++++++++++++++------ public/locales/ko-KR/grafana.json | 84 +++++++++++++++++++++++------ public/locales/nl-NL/grafana.json | 84 +++++++++++++++++++++++------ public/locales/pl-PL/grafana.json | 84 +++++++++++++++++++++++------ public/locales/pt-BR/grafana.json | 84 +++++++++++++++++++++++------ public/locales/pt-PT/grafana.json | 84 +++++++++++++++++++++++------ public/locales/ru-RU/grafana.json | 84 +++++++++++++++++++++++------ public/locales/sv-SE/grafana.json | 84 +++++++++++++++++++++++------ public/locales/tr-TR/grafana.json | 84 +++++++++++++++++++++++------ public/locales/zh-Hans/grafana.json | 84 +++++++++++++++++++++++------ public/locales/zh-Hant/grafana.json | 84 +++++++++++++++++++++++------ 18 files changed, 1224 insertions(+), 288 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 0a61dfbdd25..5a27d2da0e2 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -483,6 +483,7 @@ "noOptionsMessage-no-datasources-found": "Nebyly nalezeny žádné zdroje dat" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Kopírovat odkaz", "duplicate": "Duplikovat", "export": "Exportovat", @@ -3500,6 +3501,14 @@ "label-negative-y": "Záporné Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Vyžadovat místo z pravé strany", "gaps-options": { @@ -3544,6 +3553,10 @@ }, "name-show-unfilled-area": "Zobrazit nevyplněnou oblast", "name-value-display": "Zobrazení hodnoty", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Skryté", "label-text-color": "Barva textu", @@ -4212,6 +4225,7 @@ "clear": "Vymazat", "collapse": "Sbalit", "disabled": "", + "discard": "", "edit": "Upravit", "help": "Nápověda", "loading": "Načítání…", @@ -4829,7 +4843,6 @@ "variable": "{{type}} proměnná", "variable-set": "Proměnné" }, - "open": "Otevřít podokno možností", "row": { "header": { "hide": "Skrýt", @@ -5182,6 +5195,7 @@ "title-matched_other": "Shoda možností: {{count}}/{{totalCount}}" }, "outline": { + "pane-header": "", "repeated-item": "Opakovat", "tree-item": { "empty": "(prázdné)", @@ -5393,6 +5407,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Načítání konfigurace" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Načítání a inicializace nástěnky", "title-not-found": "Panel s ID {{panelId}} nebyl nalezen" @@ -5496,11 +5536,8 @@ "tooltip": "Tato nástěnka byla označena jen pro čtení" }, "export": { - "arrow": "Exportovat", - "title": "Exportovat", "tooltip": { - "as-code": "Exportovat jako kód", - "json": "Exportovat jako JSON" + "as-code": "Exportovat jako kód" } }, "more-save-options": "Další možnosti uložení", @@ -5552,6 +5589,9 @@ "save-library-panel": "Uložit panel knihovny", "settings": "Nastavení nástěnky", "share-button": "Sdílet", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5883,9 +5923,6 @@ "name-values-separated-comma": "Hodnoty oddělené čárkou", "selection-options": "Možnosti výběru" }, - "dashboard-edit-pane-renderer": { - "outline": "Osnova" - }, "dashboard-link-form": { "back-to-list": "Zpět na seznam", "label-icon": "Ikona", @@ -7876,6 +7913,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8173,13 +8211,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Přejít zpět" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11179,6 +11210,10 @@ "pie-chart-type-options": { "label-donut": "Kobliha", "label-pie": "Koláč" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13145,6 +13180,12 @@ "label-same-as-value": "Stejné jako hodnota", "label-standard": "Standardní" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Střed" @@ -13484,7 +13525,8 @@ "forwards-time-aria-label": "Přesunout časový rozsah dopředu", "to": "do", "zoom-out-button": "Oddálit časový rozsah", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Použít časový rozsah", @@ -13586,6 +13628,16 @@ "label-threshold": "Prahová hodnota" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Přidat časové pásmo", "tooltip-remove-timezone": "Odebrat časové pásmo" diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 8da43ece8a3..983ccc5868b 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Keine Datenquellen gefunden" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Link kopieren", "duplicate": "Duplikat", "export": "Exportieren", @@ -3476,6 +3477,14 @@ "label-negative-y": "Negativ Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Leerzeichen von der rechten Seite erforderlich", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Ungefüllten Bereich anzeigen", "name-value-display": "Wertanzeige", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Ausgeblendet", "label-text-color": "Textfarbe", @@ -4172,6 +4185,7 @@ "clear": "Löschen", "collapse": "Einklappen", "disabled": "", + "discard": "", "edit": "Bearbeiten", "help": "Hilfe", "loading": "Wird geladen ...", @@ -4789,7 +4803,6 @@ "variable": "{{type}} Variable", "variable-set": "Variable" }, - "open": "Optionsfenster öffnen", "row": { "header": { "hide": "Ausblenden", @@ -5140,6 +5153,7 @@ "title-matched_other": "Übereinstimmend {{count}}/{{totalCount}} Optionen" }, "outline": { + "pane-header": "", "repeated-item": "Wiederholen", "tree-item": { "empty": "(leer)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Konfiguration wird geladen" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Laden und Initialisieren des Dashboards", "title-not-found": "Das Panel mit der ID {{panelId}} wurde nicht gefunden" @@ -5454,11 +5494,8 @@ "tooltip": "Dieses Dashboard wurde als schreibgeschützt markiert" }, "export": { - "arrow": "Exportieren", - "title": "Exportieren", "tooltip": { - "as-code": "Als Code exportieren", - "json": "Als JSON exportieren" + "as-code": "Als Code exportieren" } }, "more-save-options": "Weitere Speicheroptionen", @@ -5510,6 +5547,9 @@ "save-library-panel": "Bibliotheks-Panel speichern", "settings": "Dashboard-Einstellungen", "share-button": "Teilen", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Werte werden durch Komma getrennt", "selection-options": "Auswahloptionen" }, - "dashboard-edit-pane-renderer": { - "outline": "Rahmen" - }, "dashboard-link-form": { "back-to-list": "Zurück zur Liste", "label-icon": "Symbol", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Zurückgehen" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Donut", "label-pie": "Kreis" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Identisch mit Wert", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Mitte" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Zeitbereich nach vorne verschieben", "to": "bis", "zoom-out-button": "Zeitbereich verkleinern", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Zeitbereich anwenden", @@ -13472,6 +13514,16 @@ "label-threshold": "Schwellenwert" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Zeitzone hinzufügen", "tooltip-remove-timezone": "Zeitzone entfernen" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index f25a23bb253..bca3dd9a0df 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "No se han encontrado fuentes de datos" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Copiar enlace", "duplicate": "Duplicar", "export": "Exportar", @@ -3476,6 +3477,14 @@ "label-negative-y": "Y negativa" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Requerir espacio desde el lado derecho", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Mostrar área sin rellenar", "name-value-display": "Visualización del valor", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Oculto", "label-text-color": "Color de texto", @@ -4172,6 +4185,7 @@ "clear": "Borrar", "collapse": "Contraer", "disabled": "", + "discard": "", "edit": "Editar", "help": "Ayuda", "loading": "Cargando...", @@ -4789,7 +4803,6 @@ "variable": "{{type}} variable", "variable-set": "Variables" }, - "open": "Abrir panel de opciones", "row": { "header": { "hide": "Ocultar", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{count}} coincidencias/{{totalCount}} opciones" }, "outline": { + "pane-header": "", "repeated-item": "Repetir", "tree-item": { "empty": "(vacío)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Cargando configuración" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Cargando e iniciando el dashboard", "title-not-found": "Panel con ID {{panelId}} no encontrado" @@ -5454,11 +5494,8 @@ "tooltip": "Este dashboard se ha marcado como de solo lectura" }, "export": { - "arrow": "Exportar", - "title": "Exportar", "tooltip": { - "as-code": "Exportar como código", - "json": "Exportar como JSON" + "as-code": "Exportar como código" } }, "more-save-options": "Más opciones de guardado", @@ -5510,6 +5547,9 @@ "save-library-panel": "Guardar panel de biblioteca", "settings": "Ajustes del panel de control", "share-button": "Compartir", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Valores separados por comas", "selection-options": "Opciones de selección" }, - "dashboard-edit-pane-renderer": { - "outline": "Esquema" - }, "dashboard-link-form": { "back-to-list": "Regresar a la lista", "label-icon": "Icono", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Volver" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Anillo", "label-pie": "Círculo" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Igual que el valor", "label-standard": "Estándar" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Centro" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Adelantar el intervalo de tiempo", "to": "hasta", "zoom-out-button": "Reducir el intervalo de tiempo", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Aplicar intervalo de tiempo", @@ -13472,6 +13514,16 @@ "label-threshold": "Umbral" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Añadir zona horaria", "tooltip-remove-timezone": "Eliminar zona horaria" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 5d716092b57..c24f8a94696 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Aucune source de données trouvée" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Copier le lien", "duplicate": "Dupliquer", "export": "Exporter", @@ -3476,6 +3477,14 @@ "label-negative-y": "Axe Y négatif" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Exiger un espace du côté droit", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Afficher la zone non remplie", "name-value-display": "Affichage de la valeur", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Masqué", "label-text-color": "Couleur du texte", @@ -4172,6 +4185,7 @@ "clear": "Effacer", "collapse": "Réduire", "disabled": "", + "discard": "", "edit": "Modifier", "help": "Aide", "loading": "Chargement en cours...", @@ -4789,7 +4803,6 @@ "variable": "Variable {{type}}", "variable-set": "Variables" }, - "open": "Ouvrir le volet d'options", "row": { "header": { "hide": "Masquer", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{count}} option(s)/{{totalCount}} mise(s) en correspondance" }, "outline": { + "pane-header": "", "repeated-item": "Répéter", "tree-item": { "empty": "(vide)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Chargement de la configuration" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Chargement et initialisation du tableau de bord", "title-not-found": "Panneau avec l’ID {{panelId}} introuvable" @@ -5454,11 +5494,8 @@ "tooltip": "Ce tableau de bord a été marqué comme étant en lecture seule" }, "export": { - "arrow": "Exporter", - "title": "Exporter", "tooltip": { - "as-code": "Exporter en tant que code", - "json": "Exporter en tant que JSON" + "as-code": "Exporter en tant que code" } }, "more-save-options": "Plus d’options d’enregistrement", @@ -5510,6 +5547,9 @@ "save-library-panel": "Enregistrer le panneau de la bibliothèque", "settings": "Paramètres du tableau de bord", "share-button": "Partager", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Valeurs séparées par des virgules", "selection-options": "Options de sélection" }, - "dashboard-edit-pane-renderer": { - "outline": "Présentation" - }, "dashboard-link-form": { "back-to-list": "Retour à la liste", "label-icon": "Icône", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Retour" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Anneau", "label-pie": "Circulaire" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Identique à la valeur", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Centré" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Avancer la plage de temps", "to": "à", "zoom-out-button": "Dézoomer la plage de temps", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Appliquer la plage temporelle", @@ -13472,6 +13514,16 @@ "label-threshold": "Seuil" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Ajouter le fuseau horaire", "tooltip-remove-timezone": "Supprimer le fuseau horaire" diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index d0dbf31ba9e..287f64d1b3d 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Nem található adatforrás" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Hivatkozás másolása", "duplicate": "Duplikálás", "export": "Exportálás", @@ -3476,6 +3477,14 @@ "label-negative-y": "Negatív Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Hely szükséges a jobb oldalról", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Kitöltetlen terület megjelenítése", "name-value-display": "Érték megjelenítése", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Rejtett", "label-text-color": "Szöveg színe", @@ -4172,6 +4185,7 @@ "clear": "Törlés", "collapse": "Összecsukás", "disabled": "", + "discard": "", "edit": "Szerkesztés", "help": "Súgó", "loading": "Betöltés...", @@ -4789,7 +4803,6 @@ "variable": "{{type}} változó", "variable-set": "Változók" }, - "open": "Beállítások ablaktábla megnyitása", "row": { "header": { "hide": "Elrejtés", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{totalCount}}/{{count}} egyező lehetőség" }, "outline": { + "pane-header": "", "repeated-item": "Ismétlés", "tree-item": { "empty": "(üres)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Konfiguráció betöltése" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Irányítópult betöltése és inicializálása", "title-not-found": "A(z) {{panelId}} azonosítójú panel nem található" @@ -5454,11 +5494,8 @@ "tooltip": "Ez az irányítópult csak olvashatóként van megjelölve" }, "export": { - "arrow": "Exportálás", - "title": "Exportálás", "tooltip": { - "as-code": "Exportálás kódként", - "json": "Exportálás JSON-fájlként" + "as-code": "Exportálás kódként" } }, "more-save-options": "További mentési beállítások", @@ -5510,6 +5547,9 @@ "save-library-panel": "Könyvtárpanel mentése", "settings": "Irányítópult beállításai", "share-button": "Megosztás", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Értékek vesszővel elválasztva", "selection-options": "Kijelölés beállításai" }, - "dashboard-edit-pane-renderer": { - "outline": "Körvonal" - }, "dashboard-link-form": { "back-to-list": "Vissza a listához", "label-icon": "Ikon", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Visszalépés" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Fánkdiagram", "label-pie": "Kördiagram" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Értékkel megegyező", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automatikus", "label-center": "Középre" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Időtartomány mozgatása előre", "to": "vége", "zoom-out-button": "Időtartomány kicsinyítése", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Időtartomány alkalmazása", @@ -13472,6 +13514,16 @@ "label-threshold": "Küszöb" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Időzóna hozzáadása", "tooltip-remove-timezone": "Időzóna eltávolítása" diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 3880215b56c..cb248ff1494 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -474,6 +474,7 @@ "noOptionsMessage-no-datasources-found": "Tidak ada sumber data yang ditemukan" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Salin tautan", "duplicate": "Duplikasikan", "export": "Ekspor", @@ -3464,6 +3465,14 @@ "label-negative-y": "Y Negatif" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Membutuhkan ruang dari sisi kanan", "gaps-options": { @@ -3508,6 +3517,10 @@ }, "name-show-unfilled-area": "Tampilkan area yang tidak terisi", "name-value-display": "Tampilan nilai", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Tersembunyi", "label-text-color": "Warna teks", @@ -4152,6 +4165,7 @@ "clear": "Hapus", "collapse": "Ciutkan", "disabled": "", + "discard": "", "edit": "Edit", "help": "Bantuan", "loading": "Memuat...", @@ -4769,7 +4783,6 @@ "variable": "variabel {{type}}", "variable-set": "Variabel" }, - "open": "Buka panel opsi", "row": { "header": { "hide": "Sembunyikan", @@ -5119,6 +5132,7 @@ "title-matched_other": "Cocok {{count}}/{{totalCount}} opsi" }, "outline": { + "pane-header": "", "repeated-item": "Ulangi", "tree-item": { "empty": "(kosong)", @@ -5330,6 +5344,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Memuat konfigurasi" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Memuat & menginisialisasi dasbor", "title-not-found": "Panel dengan id {{panelId}} tidak ditemukan" @@ -5433,11 +5473,8 @@ "tooltip": "Dasbor ini ditandai sebagai hanya baca" }, "export": { - "arrow": "Ekspor", - "title": "Ekspor", "tooltip": { - "as-code": "Ekspor sebagai kode", - "json": "Ekspor sebagai JSON" + "as-code": "Ekspor sebagai kode" } }, "more-save-options": "Opsi simpan lainnya", @@ -5489,6 +5526,9 @@ "save-library-panel": "Simpan panel pustaka", "settings": "Pengaturan dasbor", "share-button": "Bagikan", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5817,9 +5857,6 @@ "name-values-separated-comma": "Nilai dipisahkan dengan koma", "selection-options": "Opsi pemilihan" }, - "dashboard-edit-pane-renderer": { - "outline": "Garis besar" - }, "dashboard-link-form": { "back-to-list": "Kembali ke daftar", "label-icon": "Ikon", @@ -7798,6 +7835,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8095,13 +8133,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Kembali" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11038,6 +11069,10 @@ "pie-chart-type-options": { "label-donut": "Donat", "label-pie": "Lingkaran" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -12977,6 +13012,12 @@ "label-same-as-value": "Sama dengan Nilai", "label-standard": "Standar" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Otomatis", "label-center": "Pusat" @@ -13313,7 +13354,8 @@ "forwards-time-aria-label": "Pindahkan rentang waktu ke depan", "to": "ke", "zoom-out-button": "Perkecil rentang waktu", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Gunakan rentang waktu", @@ -13415,6 +13457,16 @@ "label-threshold": "Ambang batas" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Tambahkan zona waktu", "tooltip-remove-timezone": "Hapus zona waktu" diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index a86adf9b336..731218c4f16 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Nessuna fonte dati trovata" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Copia link", "duplicate": "Duplica", "export": "Esporta", @@ -3476,6 +3477,14 @@ "label-negative-y": "Y negativo" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Richiedi spazio dal lato destro", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Mostra area non riempita", "name-value-display": "Visualizzazione del valore", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Nascosto", "label-text-color": "Colore del testo", @@ -4172,6 +4185,7 @@ "clear": "Cancella", "collapse": "Riduci", "disabled": "", + "discard": "", "edit": "Modifica", "help": "Guida", "loading": "Caricamento in corso...", @@ -4789,7 +4803,6 @@ "variable": "{{type}} variabile", "variable-set": "Variabili" }, - "open": "Apri il riquadro delle opzioni", "row": { "header": { "hide": "Nascondi", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{count}} corrispondente/{{totalCount}} opzioni" }, "outline": { + "pane-header": "", "repeated-item": "Ripeti", "tree-item": { "empty": "(vuoto)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Caricamento configurazione" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Caricamento e inizializzazione della dashboard", "title-not-found": "Pannello con ID {{panelId}} non trovato" @@ -5454,11 +5494,8 @@ "tooltip": "Questa dashboard è stata contrassegnata come di sola lettura" }, "export": { - "arrow": "Esporta", - "title": "Esporta", "tooltip": { - "as-code": "Esporta come codice", - "json": "Esporta in formato JSON" + "as-code": "Esporta come codice" } }, "more-save-options": "Altre opzioni di salvataggio", @@ -5510,6 +5547,9 @@ "save-library-panel": "Salva pannello della libreria", "settings": "Impostazioni dashboard", "share-button": "Condividi", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Valori separati da virgola", "selection-options": "Seleziona opzioni" }, - "dashboard-edit-pane-renderer": { - "outline": "Schema" - }, "dashboard-link-form": { "back-to-list": "Torna all'elenco", "label-icon": "Icona", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Torna indietro" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "A ciambella", "label-pie": "A torta" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Uguale al valore", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automatico", "label-center": "Centrale" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Usa un intervallo di tempo successivo", "to": "a", "zoom-out-button": "Riduci l'intervallo di tempo", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Applica intervallo di tempo", @@ -13472,6 +13514,16 @@ "label-threshold": "Soglia" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Aggiungi fuso orario", "tooltip-remove-timezone": "Rimuovi fuso orario" diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 42aa76d2642..5eb117faa50 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -474,6 +474,7 @@ "noOptionsMessage-no-datasources-found": "データソースが見つかりません" }, "alert-menu": { + "analyze-rule": "", "copy-link": "リンクをコピー", "duplicate": "複製", "export": "エクスポート", @@ -3464,6 +3465,14 @@ "label-negative-y": "Y軸を反転" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "右側にスペースが必要", "gaps-options": { @@ -3508,6 +3517,10 @@ }, "name-show-unfilled-area": "未入力の領域を表示", "name-value-display": "値を表示", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "非表示", "label-text-color": "文字色", @@ -4152,6 +4165,7 @@ "clear": "消去", "collapse": "折りたたみ表示", "disabled": "", + "discard": "", "edit": "編集", "help": "ヘルプ", "loading": "読み込み中...", @@ -4769,7 +4783,6 @@ "variable": "{{type}}変数", "variable-set": "変数" }, - "open": "オプションペインを開く", "row": { "header": { "hide": "非表示", @@ -5119,6 +5132,7 @@ "title-matched_other": "{{count}}/{{totalCount}}件のオプションが一致" }, "outline": { + "pane-header": "", "repeated-item": "繰り返し", "tree-item": { "empty": "(空白)", @@ -5330,6 +5344,32 @@ "share-public-dashboard-loader": { "loading-configuration": "設定を読み込み中" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "ダッシュボードの読み込みと初期化", "title-not-found": "ID{{panelId}}のパネルが見つかりません" @@ -5433,11 +5473,8 @@ "tooltip": "このダッシュボードは読み取り専用に設定されています" }, "export": { - "arrow": "エクスポート", - "title": "エクスポート", "tooltip": { - "as-code": "コードとしてエクスポート", - "json": "JSON形式でエクスポート" + "as-code": "コードとしてエクスポート" } }, "more-save-options": "その他の保存オプション", @@ -5489,6 +5526,9 @@ "save-library-panel": "ライブラリパネルを保存", "settings": "ダッシュボードの設定", "share-button": "共有", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5817,9 +5857,6 @@ "name-values-separated-comma": "カンマ区切りの値", "selection-options": "選択オプション" }, - "dashboard-edit-pane-renderer": { - "outline": "概要" - }, "dashboard-link-form": { "back-to-list": "一覧に戻る", "label-icon": "アイコン", @@ -7798,6 +7835,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8095,13 +8133,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "戻る" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11038,6 +11069,10 @@ "pie-chart-type-options": { "label-donut": "ドーナツ", "label-pie": "円" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -12977,6 +13012,12 @@ "label-same-as-value": "値と同じ", "label-standard": "標準" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "自動", "label-center": "中央" @@ -13313,7 +13354,8 @@ "forwards-time-aria-label": "時間範囲を前に移動", "to": "へ", "zoom-out-button": "時間範囲をズームアウト", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "時間範囲を適用", @@ -13415,6 +13457,16 @@ "label-threshold": "しきい値" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "タイムゾーンを追加", "tooltip-remove-timezone": "タイムゾーンを削除" diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 437982e410b..1cc6b2ab0a1 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -474,6 +474,7 @@ "noOptionsMessage-no-datasources-found": "데이터 소스를 찾을 수 없습니다" }, "alert-menu": { + "analyze-rule": "", "copy-link": "링크 복사", "duplicate": "복제", "export": "내보내기", @@ -3464,6 +3465,14 @@ "label-negative-y": "Y 음수 값" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "오른쪽에서 공간이 필요합니다.", "gaps-options": { @@ -3508,6 +3517,10 @@ }, "name-show-unfilled-area": "미채움 영역 표시", "name-value-display": "값 표시", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "숨김", "label-text-color": "텍스트 색상", @@ -4152,6 +4165,7 @@ "clear": "초기화", "collapse": "접기", "disabled": "", + "discard": "", "edit": "편집", "help": "도움말", "loading": "로딩 중...", @@ -4769,7 +4783,6 @@ "variable": "{{type}} 변수", "variable-set": "변수" }, - "open": "옵션 창 열기", "row": { "header": { "hide": "숨기기", @@ -5119,6 +5132,7 @@ "title-matched_other": "{{totalCount}}개 옵션 중 {{count}}개 일치함" }, "outline": { + "pane-header": "", "repeated-item": "반복", "tree-item": { "empty": "(비어 있음)", @@ -5330,6 +5344,32 @@ "share-public-dashboard-loader": { "loading-configuration": "구성 로딩 중" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "대시보드 로딩 및 초기화", "title-not-found": "ID가 {{panelId}}인 패널을 찾을 수 없음" @@ -5433,11 +5473,8 @@ "tooltip": "이 대시보드는 읽기 전용으로 표시되었습니다." }, "export": { - "arrow": "내보내기", - "title": "내보내기", "tooltip": { - "as-code": "코드로 내보내기", - "json": "JSON으로 내보내기" + "as-code": "코드로 내보내기" } }, "more-save-options": "저장 옵션 더 보기", @@ -5489,6 +5526,9 @@ "save-library-panel": "라이브러리 패널 저장", "settings": "대시보드 설정", "share-button": "공유", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5817,9 +5857,6 @@ "name-values-separated-comma": "쉼표로 구분된 값", "selection-options": "선택 옵션" }, - "dashboard-edit-pane-renderer": { - "outline": "개요" - }, "dashboard-link-form": { "back-to-list": "목록으로 돌아가기", "label-icon": "아이콘", @@ -7798,6 +7835,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8095,13 +8133,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "뒤로 가기" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11038,6 +11069,10 @@ "pie-chart-type-options": { "label-donut": "도넛", "label-pie": "원형" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -12977,6 +13012,12 @@ "label-same-as-value": "값과 동일", "label-standard": "표준" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "자동", "label-center": "중앙" @@ -13313,7 +13354,8 @@ "forwards-time-aria-label": "시간 범위를 이후로 변경", "to": "종료", "zoom-out-button": "시간 범위 확대", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "시간 범위 적용", @@ -13415,6 +13457,16 @@ "label-threshold": "임계값" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "시간대 추가", "tooltip-remove-timezone": "시간대 제거" diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 994f52eb9e4..b66136b8183 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Geen gegevensbronnen gevonden" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Link kopiëren", "duplicate": "Dupliceren", "export": "Exporteren", @@ -3476,6 +3477,14 @@ "label-negative-y": "Negatieve Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Ruimte van de rechterkant vereisen", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Niet-gevuld gebied weergeven", "name-value-display": "Weergave van waarden", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Verborgen", "label-text-color": "Tekstkleur", @@ -4172,6 +4185,7 @@ "clear": "Wissen", "collapse": "Samenvouwen", "disabled": "", + "discard": "", "edit": "Bewerken", "help": "Help", "loading": "Bezig met laden ...", @@ -4789,7 +4803,6 @@ "variable": "{{type}}variabele", "variable-set": "Variabelen" }, - "open": "Deelvenster Opties openen", "row": { "header": { "hide": "Verbergen", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{count}} overeenkomende/{{totalCount}} opties" }, "outline": { + "pane-header": "", "repeated-item": "Herhalen", "tree-item": { "empty": "(leeg)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Configuratie laden" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Dashboard laden en initialiseren", "title-not-found": "Paneel met id {{panelId}} niet gevonden" @@ -5454,11 +5494,8 @@ "tooltip": "Dit dashboard is gemarkeerd als alleen lezen" }, "export": { - "arrow": "Exporteren", - "title": "Exporteren", "tooltip": { - "as-code": "Exporteren als code", - "json": "Exporteren als JSON" + "as-code": "Exporteren als code" } }, "more-save-options": "Meer opties voor opslaan", @@ -5510,6 +5547,9 @@ "save-library-panel": "Bibliotheekpaneel", "settings": "Dashboardinstellingen", "share-button": "Delen", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Waarden gescheiden door komma", "selection-options": "Selectiemogelijkheden" }, - "dashboard-edit-pane-renderer": { - "outline": "Schetsen" - }, "dashboard-link-form": { "back-to-list": "Terug naar lijst", "label-icon": "Pictogram", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Ga terug" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Donut", "label-pie": "Taart" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Hetzelfde als waarde", "label-standard": "Standaard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automatisch", "label-center": "Midden" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Tijdsbereik vooruitzetten", "to": "tot", "zoom-out-button": "Tijdsbereik uitzoomen", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Tijdsbereik toepassen", @@ -13472,6 +13514,16 @@ "label-threshold": "Drempelwaarde" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Tijdzone toevoegen", "tooltip-remove-timezone": "Tijdzone verwijderen" diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 8cafca8a198..233b9ed0a50 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -483,6 +483,7 @@ "noOptionsMessage-no-datasources-found": "Nie znaleziono źródeł danych" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Kopiuj link", "duplicate": "Duplikuj", "export": "Eksport", @@ -3500,6 +3501,14 @@ "label-negative-y": "Ujemna oś Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Wymagaj spacji po prawej stronie", "gaps-options": { @@ -3544,6 +3553,10 @@ }, "name-show-unfilled-area": "Pokaż niewypełniony obszar", "name-value-display": "Wyświetlanie wartości", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Ukryte", "label-text-color": "Kolor tekstu", @@ -4212,6 +4225,7 @@ "clear": "Wyczyść", "collapse": "Zwiń", "disabled": "", + "discard": "", "edit": "Edytuj", "help": "Pomoc", "loading": "Ładowanie…", @@ -4829,7 +4843,6 @@ "variable": "Zmienna {{type}}", "variable-set": "Zmienne" }, - "open": "Otwórz okno opcji", "row": { "header": { "hide": "Ukryj", @@ -5182,6 +5195,7 @@ "title-matched_other": "Dopasowano {{count}}/{{totalCount}} opcji" }, "outline": { + "pane-header": "", "repeated-item": "Powtórz", "tree-item": { "empty": "(puste)", @@ -5393,6 +5407,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Wczytywanie konfiguracji" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Wczytywanie i inicjowanie pulpitu", "title-not-found": "Nie znaleziono panelu o identyfikatorze {{panelId}}" @@ -5496,11 +5536,8 @@ "tooltip": "Ten pulpit został oznaczony jako tylko do odczytu" }, "export": { - "arrow": "Eksportuj", - "title": "Eksportuj", "tooltip": { - "as-code": "Eksportuj jako kod", - "json": "Eksportowanie jako JSON" + "as-code": "Eksportuj jako kod" } }, "more-save-options": "Więcej opcji zapisywania", @@ -5552,6 +5589,9 @@ "save-library-panel": "Zapisz panel biblioteki", "settings": "Ustawienia panelu", "share-button": "Udostępnij", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5883,9 +5923,6 @@ "name-values-separated-comma": "Wartości rozdzielone przecinkami", "selection-options": "Opcje wyboru" }, - "dashboard-edit-pane-renderer": { - "outline": "Konspekt" - }, "dashboard-link-form": { "back-to-list": "Powrót do listy", "label-icon": "Ikona", @@ -7876,6 +7913,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8173,13 +8211,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Wróć" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11179,6 +11210,10 @@ "pie-chart-type-options": { "label-donut": "Pierścieniowy", "label-pie": "Kołowy" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13145,6 +13180,12 @@ "label-same-as-value": "Taki sam jak wartość", "label-standard": "Standardowy" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automatycznie", "label-center": "Pośrodku" @@ -13484,7 +13525,8 @@ "forwards-time-aria-label": "Przesuń zakres czasu do przodu", "to": "do", "zoom-out-button": "Oddal zakres czasu", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Zastosuj zakres czasu", @@ -13586,6 +13628,16 @@ "label-threshold": "Próg" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Dodaj strefę czasową", "tooltip-remove-timezone": "Usuń strefę czasową" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 1151ca293a1..411c26f63fb 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Nenhuma fonte de dados encontrada" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Copiar link", "duplicate": "Duplicar", "export": "Exportar", @@ -3476,6 +3477,14 @@ "label-negative-y": "Y negativo" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Exigir espaço do lado direito", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Mostrar área não preenchida", "name-value-display": "Exibição de valor", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Oculto", "label-text-color": "Cor do texto", @@ -4172,6 +4185,7 @@ "clear": "Limpar", "collapse": "Recolher", "disabled": "", + "discard": "", "edit": "Editar", "help": "Ajuda", "loading": "Carregando...", @@ -4789,7 +4803,6 @@ "variable": "Variável {{type}}", "variable-set": "Variáveis" }, - "open": "Abrir painel de opções", "row": { "header": { "hide": "Ocultar", @@ -5140,6 +5153,7 @@ "title-matched_other": "Opções correspondentes: {{count}}/{{totalCount}}" }, "outline": { + "pane-header": "", "repeated-item": "Repetido", "tree-item": { "empty": "(vazio)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Carregando configuração" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Carregando e inicializando o painel", "title-not-found": "Painel com ID {{panelId}} não encontrado" @@ -5454,11 +5494,8 @@ "tooltip": "Este painel foi marcado como somente leitura" }, "export": { - "arrow": "Exportar", - "title": "Exportar", "tooltip": { - "as-code": "Exportar como código", - "json": "Exportar como JSON" + "as-code": "Exportar como código" } }, "more-save-options": "Mais opções de salvamento", @@ -5510,6 +5547,9 @@ "save-library-panel": "Salvar painel da biblioteca", "settings": "Configurações do painel de controle", "share-button": "Compartilhar", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Valores separados por vírgula", "selection-options": "Opções de seleção" }, - "dashboard-edit-pane-renderer": { - "outline": "Contorno" - }, "dashboard-link-form": { "back-to-list": "Voltar para a lista", "label-icon": "Ícone", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Voltar" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Donut", "label-pie": "Pizza" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Com o mesmo valor", "label-standard": "Padrão" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automático", "label-center": "Centro" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Avançar intervalo de tempo", "to": "para", "zoom-out-button": "Diminuir o intervalo de tempo", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Aplicar intervalo de tempo", @@ -13472,6 +13514,16 @@ "label-threshold": "Limite" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Adicionar fuso horário", "tooltip-remove-timezone": "Remover fuso horário" diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 856c8b78a3e..a97f0c759f6 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Nenhuma fonte de dados encontrada" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Copiar link", "duplicate": "Duplicar", "export": "Exportar", @@ -3476,6 +3477,14 @@ "label-negative-y": "Y negativo" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Requerer espaço do lado direito", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Mostrar a área não preenchida", "name-value-display": "Apresentação dos valores", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Oculto", "label-text-color": "Cor do texto", @@ -4172,6 +4185,7 @@ "clear": "Limpar", "collapse": "Recolher", "disabled": "", + "discard": "", "edit": "Editar", "help": "Ajuda", "loading": "A carregar...", @@ -4789,7 +4803,6 @@ "variable": "Variável {{type}}", "variable-set": "Variáveis" }, - "open": "Abrir painel de opções", "row": { "header": { "hide": "Ocultar", @@ -5140,6 +5153,7 @@ "title-matched_other": "Correspondente a {{count}}/{{totalCount}} opções" }, "outline": { + "pane-header": "", "repeated-item": "Repetir", "tree-item": { "empty": "(vazio)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "A carregar a configuração" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "A carregar e inicializar o painel de controlo", "title-not-found": "Painel com ID {{panelId}} não encontrado" @@ -5454,11 +5494,8 @@ "tooltip": "Este painel de controlo foi marcado como apenas de leitura" }, "export": { - "arrow": "Exportar", - "title": "Exportar", "tooltip": { - "as-code": "Exportar como código", - "json": "Exportar como JSON" + "as-code": "Exportar como código" } }, "more-save-options": "Mais opções de guardar", @@ -5510,6 +5547,9 @@ "save-library-panel": "Guardar painel de biblioteca", "settings": "Definições do painel de controlo", "share-button": "Partilhar", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Valores separados por vírgulas", "selection-options": "Opções de seleção" }, - "dashboard-edit-pane-renderer": { - "outline": "Contorno" - }, "dashboard-link-form": { "back-to-list": "Voltar à lista", "label-icon": "Ícone", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Voltar" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Anel", "label-pie": "Circular" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Igual ao valor", "label-standard": "Predefinição" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Automático", "label-center": "Centro" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Mover o intervalo de tempo para a frente", "to": "para", "zoom-out-button": "Diminuir o zoom do intervalo de tempo", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Aplicar intervalo de tempo", @@ -13472,6 +13514,16 @@ "label-threshold": "Limite" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Adicionar fuso horário", "tooltip-remove-timezone": "Remover fuso horário" diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 50c67543a33..e916e740995 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -483,6 +483,7 @@ "noOptionsMessage-no-datasources-found": "Источники данных не найдены" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Копировать ссылку", "duplicate": "Дублировать", "export": "Экспорт", @@ -3500,6 +3501,14 @@ "label-negative-y": "Отрицательные для оси Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Требуется пространство с правой стороны", "gaps-options": { @@ -3544,6 +3553,10 @@ }, "name-show-unfilled-area": "Показывать незаполненную область", "name-value-display": "Отображение значений", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Скрыты", "label-text-color": "Цвет текста", @@ -4212,6 +4225,7 @@ "clear": "Очистить", "collapse": "Свернуть", "disabled": "", + "discard": "", "edit": "Редактировать", "help": "Справка", "loading": "Загрузка…", @@ -4829,7 +4843,6 @@ "variable": "Тип переменной: {{type}}", "variable-set": "Переменные" }, - "open": "Открыть область параметров", "row": { "header": { "hide": "Скрыть", @@ -5182,6 +5195,7 @@ "title-matched_other": "Совпадение: {{count}}/{{totalCount}} параметра" }, "outline": { + "pane-header": "", "repeated-item": "Повторить", "tree-item": { "empty": "(пусто)", @@ -5393,6 +5407,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Загрузка конфигурации" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Загрузка и инициализация дашборда", "title-not-found": "Панель с идентификатором {{panelId}} не найдена" @@ -5496,11 +5536,8 @@ "tooltip": "Дашборд был помечен как «только для чтения»" }, "export": { - "arrow": "Экспорт", - "title": "Экспорт", "tooltip": { - "as-code": "Экспортировать как код", - "json": "Экспорт в формате JSON" + "as-code": "Экспортировать как код" } }, "more-save-options": "Больше параметров сохранения", @@ -5552,6 +5589,9 @@ "save-library-panel": "Сохранить панель библиотеки", "settings": "Параметры дашборда", "share-button": "Общий доступ", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5883,9 +5923,6 @@ "name-values-separated-comma": "Значения, разделенные запятыми", "selection-options": "Параметры выбора" }, - "dashboard-edit-pane-renderer": { - "outline": "Структура" - }, "dashboard-link-form": { "back-to-list": "Назад к списку", "label-icon": "Значок", @@ -7876,6 +7913,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8173,13 +8211,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Назад" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11179,6 +11210,10 @@ "pie-chart-type-options": { "label-donut": "Кольцевая", "label-pie": "Круговая" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13145,6 +13180,12 @@ "label-same-as-value": "Аналогичный значению", "label-standard": "Стандартный" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Авто", "label-center": "По центру" @@ -13484,7 +13525,8 @@ "forwards-time-aria-label": "Переместить временной диапазон вперед", "to": "на", "zoom-out-button": "Уменьшение масштаба временного диапазона", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Применить временной диапазон", @@ -13586,6 +13628,16 @@ "label-threshold": "Пороговое значение" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Добавить часовой пояс", "tooltip-remove-timezone": "Удалить часовой пояс" diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index f88845db555..38758ccf40e 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Inga datakällor hittades" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Kopiera länk", "duplicate": "Dubblett", "export": "Exportera", @@ -3476,6 +3477,14 @@ "label-negative-y": "Negativt Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Kräv utrymme från höger", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Visa ofyllt område", "name-value-display": "Värdevisning", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Dolt", "label-text-color": "Textfärg", @@ -4172,6 +4185,7 @@ "clear": "Rensa", "collapse": "Minimera", "disabled": "", + "discard": "", "edit": "Redigera", "help": "Hjälp", "loading": "Laddar …", @@ -4789,7 +4803,6 @@ "variable": "{{type}}-variabel", "variable-set": "Variabler" }, - "open": "Öppna alternativfönstret", "row": { "header": { "hide": "Dölj", @@ -5140,6 +5153,7 @@ "title-matched_other": "Matchade {{count}}/{{totalCount}} alternativ" }, "outline": { + "pane-header": "", "repeated-item": "Upprepa", "tree-item": { "empty": "(tomt)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Läser in konfiguration" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Laddar och initierar instrumentpanel", "title-not-found": "Panel med ID {{panelId}} hittades inte" @@ -5454,11 +5494,8 @@ "tooltip": "Den här instrumentpanelen markerades som skrivskyddad" }, "export": { - "arrow": "Exportera", - "title": "Exportera", "tooltip": { - "as-code": "Exportera som kod", - "json": "Exportera som JSON" + "as-code": "Exportera som kod" } }, "more-save-options": "Fler sparalternativ", @@ -5510,6 +5547,9 @@ "save-library-panel": "Spara bibliotekspanel", "settings": "Instrumentpanelens inställningar", "share-button": "Dela", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Värden åtskilda med kommatecken", "selection-options": "Urvalsalternativ" }, - "dashboard-edit-pane-renderer": { - "outline": "Översikt" - }, "dashboard-link-form": { "back-to-list": "Tillbaka till listan", "label-icon": "Ikon", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Tillbaka" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Munk", "label-pie": "Paj" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Samma som värde", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Centrera" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Flytta tidsintervallet framåt", "to": "till", "zoom-out-button": "Zooma ut tidsintervall", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Tillämpa tidsintervall", @@ -13472,6 +13514,16 @@ "label-threshold": "Tröskel" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Lägg till tidszon", "tooltip-remove-timezone": "Ta bort tidszon" diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 574351940a4..c727847d792 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "Veri kaynağı bulunamadı" }, "alert-menu": { + "analyze-rule": "", "copy-link": "Bağlantıyı kopyala", "duplicate": "Çoğalt", "export": "Dışa aktar", @@ -3476,6 +3477,14 @@ "label-negative-y": "Negatif Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Sağ tarafta boşluk bırakılmasını zorunlu kıl", "gaps-options": { @@ -3520,6 +3529,10 @@ }, "name-show-unfilled-area": "Doldurulmamış alanı göster", "name-value-display": "Değer görüntüleme", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "Gizli", "label-text-color": "Metin rengi", @@ -4172,6 +4185,7 @@ "clear": "Temizle", "collapse": "Daralt", "disabled": "", + "discard": "", "edit": "Düzenle", "help": "Yardım", "loading": "Yükleniyor...", @@ -4789,7 +4803,6 @@ "variable": "{{type}} değişkeni", "variable-set": "Değişkenler" }, - "open": "Seçenekler bölmesini aç", "row": { "header": { "hide": "Gizle", @@ -5140,6 +5153,7 @@ "title-matched_other": "{{totalCount}} seçenekten {{count}} tanesi eşleşti" }, "outline": { + "pane-header": "", "repeated-item": "Tekrar et", "tree-item": { "empty": "(boş)", @@ -5351,6 +5365,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Yapılandırma yükleniyor" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "Pano yükleniyor ve başlatılıyor", "title-not-found": "{{panelId}} kimliğine sahip panel bulunamadı" @@ -5454,11 +5494,8 @@ "tooltip": "Bu pano salt okunur olarak işaretlendi" }, "export": { - "arrow": "Dışa aktar", - "title": "Dışa aktar", "tooltip": { - "as-code": "Kod olarak dışa aktar", - "json": "JSON olarak dışa aktar" + "as-code": "Kod olarak dışa aktar" } }, "more-save-options": "Diğer kaydetme seçenekleri", @@ -5510,6 +5547,9 @@ "save-library-panel": "Kütüphane panelini kaydet", "settings": "Pano ayarları", "share-button": "Paylaş", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5839,9 +5879,6 @@ "name-values-separated-comma": "Virgülle ayrılmış değerler", "selection-options": "Seçim ayarları" }, - "dashboard-edit-pane-renderer": { - "outline": "Ana hat" - }, "dashboard-link-form": { "back-to-list": "Listeye geri dön", "label-icon": "Simge", @@ -7824,6 +7861,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8121,13 +8159,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Geri dön" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11085,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Halka", "label-pie": "Pasta" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -13033,6 +13068,12 @@ "label-same-as-value": "Değerle aynı", "label-standard": "Standart" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "Otomatik", "label-center": "Merkez" @@ -13370,7 +13411,8 @@ "forwards-time-aria-label": "Zaman aralığını ileri al", "to": "bitiş", "zoom-out-button": "Zaman aralığını büyüt", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "Zaman aralığını uygula", @@ -13472,6 +13514,16 @@ "label-threshold": "Eşik" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "Zaman dilimi ekle", "tooltip-remove-timezone": "Zaman dilimini kaldır" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 67b617cc3b8..9e5952f913f 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -474,6 +474,7 @@ "noOptionsMessage-no-datasources-found": "未找到数据源" }, "alert-menu": { + "analyze-rule": "", "copy-link": "复制链接", "duplicate": "复制", "export": "导出", @@ -3464,6 +3465,14 @@ "label-negative-y": "负 Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "需要右侧的空间", "gaps-options": { @@ -3508,6 +3517,10 @@ }, "name-show-unfilled-area": "显示未填充区域", "name-value-display": "值显示", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "隐藏", "label-text-color": "文字颜色", @@ -4152,6 +4165,7 @@ "clear": "清除", "collapse": "收起", "disabled": "", + "discard": "", "edit": "编辑", "help": "帮助", "loading": "加载中...", @@ -4769,7 +4783,6 @@ "variable": "{{type}} 变量", "variable-set": "变量" }, - "open": "打开选项窗格", "row": { "header": { "hide": "隐藏", @@ -5119,6 +5132,7 @@ "title-matched_other": "匹配了 {{count}}/{{totalCount}} 个选项" }, "outline": { + "pane-header": "", "repeated-item": "重复", "tree-item": { "empty": "(空缺)", @@ -5330,6 +5344,32 @@ "share-public-dashboard-loader": { "loading-configuration": "载入配置" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "正在加载和初始化数据面板", "title-not-found": "未找到 ID 为 {{panelId}} 的面板" @@ -5433,11 +5473,8 @@ "tooltip": "此数据面板已被标记为只读" }, "export": { - "arrow": "导出", - "title": "导出", "tooltip": { - "as-code": "导出为代码", - "json": "导出为 JSON" + "as-code": "导出为代码" } }, "more-save-options": "更多保存选项", @@ -5489,6 +5526,9 @@ "save-library-panel": "保存库面板", "settings": "仪表板设置", "share-button": "分享", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5817,9 +5857,6 @@ "name-values-separated-comma": "以逗号分隔的值", "selection-options": "选择内容选项" }, - "dashboard-edit-pane-renderer": { - "outline": "轮廓" - }, "dashboard-link-form": { "back-to-list": "回到列表", "label-icon": "图标", @@ -7798,6 +7835,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8095,13 +8133,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "返回" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11038,6 +11069,10 @@ "pie-chart-type-options": { "label-donut": "圆环图", "label-pie": "饼图" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -12977,6 +13012,12 @@ "label-same-as-value": "与值相同", "label-standard": "标准" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "自动", "label-center": "中心" @@ -13313,7 +13354,8 @@ "forwards-time-aria-label": "向前移动时间范围", "to": "至", "zoom-out-button": "缩放时间范围", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "应用时间范围", @@ -13415,6 +13457,16 @@ "label-threshold": "阈值" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "添加时区", "tooltip-remove-timezone": "移除时区" diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index a387547aab8..1d2f8294d87 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -474,6 +474,7 @@ "noOptionsMessage-no-datasources-found": "未找到資料來源" }, "alert-menu": { + "analyze-rule": "", "copy-link": "複製網址", "duplicate": "重複", "export": "匯出", @@ -3464,6 +3465,14 @@ "label-negative-y": "負 Y" } }, + "suggestions": { + "horizontal": "", + "hz-stacked": "", + "hz-stacked-percent": "", + "vert-stacked": "", + "vert-stacked-percent": "", + "vertical": "" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "需要在右側保留空間", "gaps-options": { @@ -3508,6 +3517,10 @@ }, "name-show-unfilled-area": "顯示未填充區域", "name-value-display": "數值顯示", + "suggestions": { + "basic": "", + "lcd": "" + }, "value-display-options": { "label-hidden": "隱藏", "label-text-color": "文字色彩", @@ -4152,6 +4165,7 @@ "clear": "清除", "collapse": "收闔", "disabled": "", + "discard": "", "edit": "編輯", "help": "說明", "loading": "正在載入…", @@ -4769,7 +4783,6 @@ "variable": "{{type}}變數", "variable-set": "變量" }, - "open": "開啟選項窗格", "row": { "header": { "hide": "隱藏", @@ -5119,6 +5132,7 @@ "title-matched_other": "匹配的 {{count}}/{{totalCount}} 個選項" }, "outline": { + "pane-header": "", "repeated-item": "重複", "tree-item": { "empty": "(空)", @@ -5330,6 +5344,32 @@ "share-public-dashboard-loader": { "loading-configuration": "正在匯入設定" }, + "sidebar": { + "dashboard-options": { + "title": "", + "tooltip": "" + }, + "edit-schema": { + "title": "", + "tooltip": "" + }, + "export": { + "title": "", + "unsaved-modal": { + "text": "", + "title": "" + } + }, + "outline": { + "title": "", + "tooltip": "" + }, + "redo": "", + "snapshot": { + "tooltip": "" + }, + "undo": "" + }, "solo-panel": { "loading-initializing-dashboard": "正在載入並初始化儀表板", "title-not-found": "未找到 ID 為 {{panelId}} 的面板" @@ -5433,11 +5473,8 @@ "tooltip": "此儀表板已標記為唯讀" }, "export": { - "arrow": "匯出", - "title": "匯出", "tooltip": { - "as-code": "匯出為程式碼", - "json": "匯出為 JSON" + "as-code": "匯出為程式碼" } }, "more-save-options": "更多儲存選項", @@ -5489,6 +5526,9 @@ "save-library-panel": "儲存資料庫面板", "settings": "儀表板設定", "share-button": "分享", + "snapshot": { + "title": "" + }, "star-add-error": "", "star-added": "", "star-remove-error": "", @@ -5817,9 +5857,6 @@ "name-values-separated-comma": "以逗號分隔的值", "selection-options": "選擇選項" }, - "dashboard-edit-pane-renderer": { - "outline": "外框" - }, "dashboard-link-form": { "back-to-list": "返回清單", "label-icon": "圖示", @@ -7798,6 +7835,7 @@ "suggestions": { "arc": "", "circular": "", + "no-thresholds": "", "style": { "circular": "", "simple": "" @@ -8095,13 +8133,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "返回" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { @@ -11038,6 +11069,10 @@ "pie-chart-type-options": { "label-donut": "甜甜圈圖", "label-pie": "圓餅圖" + }, + "suggestions": { + "donut": "", + "pie": "" } }, "playlist": { @@ -12977,6 +13012,12 @@ "label-same-as-value": "與值相同", "label-standard": "標準" }, + "suggestions": { + "stat-color-background": "", + "stat-discrete-values": "", + "stat-discrete-values-color-background": "", + "stat-single-string": "" + }, "text-alignment-options": { "label-auto": "自動", "label-center": "置中" @@ -13313,7 +13354,8 @@ "forwards-time-aria-label": "將時間範圍向前移動", "to": "至", "zoom-out-button": "縮小時間範圍", - "zoom-out-tooltip": "" + "zoom-out-tooltip": "", + "zoom-out-tooltip-new": "" }, "time-range": { "apply": "套用時間範圍", @@ -13415,6 +13457,16 @@ "label-threshold": "閾值" } }, + "suggestions": { + "area": "", + "area-stacked": "", + "area-stacked-percentage": "", + "bar": "", + "bar-stacked": "", + "bar-stacked-percent": "", + "line": "", + "line-smooth": "" + }, "timezones-editor": { "tooltip-add-timezone": "新增時區", "tooltip-remove-timezone": "移除時區" From fe3e2bf9cbc6eb624b372436f30553b3a9fea795 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 27 Nov 2025 12:45:25 +0200 Subject: [PATCH 135/423] Provisioning: Unify resource and file list pages (#114508) * Provisioning: Unify resources and files view * Use interactive table * Add tests * Show status * Omit root * Fix status * Fix link * Tab spacing * Cleanup * Move funciton outside * Add source link * Hide source link for unsynced files * Show folders sync status * refactor * Fix sync folder logic * refactor * fix unsynced files type * Show external source link * tweaks * SHow pending for unsynced files --- .../provisioning/File/FilesView.test.tsx | 166 ---- .../features/provisioning/File/FilesView.tsx | 99 --- .../Repository/RepositoryResources.tsx | 146 ---- .../Repository/RepositoryStatusPage.tsx | 20 +- .../Repository/ResourceTreeView.tsx | 213 ++++++ public/app/features/provisioning/types.ts | 23 +- public/app/features/provisioning/utils/git.ts | 129 +++- .../provisioning/utils/treeUtils.test.ts | 715 ++++++++++++++++++ .../features/provisioning/utils/treeUtils.ts | 229 ++++++ public/locales/en-US/grafana.json | 30 +- 10 files changed, 1290 insertions(+), 480 deletions(-) delete mode 100644 public/app/features/provisioning/File/FilesView.test.tsx delete mode 100644 public/app/features/provisioning/File/FilesView.tsx delete mode 100644 public/app/features/provisioning/Repository/RepositoryResources.tsx create mode 100644 public/app/features/provisioning/Repository/ResourceTreeView.tsx create mode 100644 public/app/features/provisioning/utils/treeUtils.test.ts create mode 100644 public/app/features/provisioning/utils/treeUtils.ts diff --git a/public/app/features/provisioning/File/FilesView.test.tsx b/public/app/features/provisioning/File/FilesView.test.tsx deleted file mode 100644 index ed29db649ae..00000000000 --- a/public/app/features/provisioning/File/FilesView.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { render, screen, waitFor } from 'test/test-utils'; - -import { Repository, useGetRepositoryFilesQuery } from 'app/api/clients/provisioning/v0alpha1'; - -import { FilesView } from './FilesView'; - -jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ - useGetRepositoryFilesQuery: jest.fn(), -})); - -const mockUseGetRepositoryFilesQuery = jest.mocked(useGetRepositoryFilesQuery); -type RepositoryFilesQueryResult = ReturnType; - -const baseQueryResult = (): RepositoryFilesQueryResult => - ({ - currentData: undefined, - data: { items: [] }, - endpointName: 'getRepositoryFiles', - error: undefined, - fulfilledTimeStamp: undefined, - isError: false, - isFetching: false, - isLoading: false, - isSuccess: false, - originalArgs: { name: '' }, - refetch: jest.fn(), - requestId: 'test-request', - startedTimeStamp: 0, - status: 'uninitialized', - subscriptionOptions: undefined, - unsubscribe: jest.fn(), - }) satisfies RepositoryFilesQueryResult; - -const mockRepositoryFilesQuery = (overrides: Partial = {}) => { - mockUseGetRepositoryFilesQuery.mockReturnValue({ - ...baseQueryResult(), - ...overrides, - }); -}; - -const defaultRepository: Repository = { - metadata: { name: 'test-repo' }, - spec: { - title: 'Test repository', - type: 'github', - workflows: ['write'], - sync: { enabled: true, target: 'folder' }, - github: { branch: 'main' }, - }, -}; - -const localRepository: Repository = { - metadata: { name: 'local-repo' }, - spec: { - title: 'Local repository', - type: 'local', - workflows: [], - sync: { enabled: true, target: 'folder' }, - local: {}, - }, -}; - -const renderComponent = (repo: Repository = defaultRepository) => { - return render(); -}; - -describe('FilesView', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders spinner while loading', () => { - mockRepositoryFilesQuery({ isLoading: true, status: 'pending', data: undefined }); - - renderComponent(); - - expect(screen.getByTestId('Spinner')).toBeInTheDocument(); - }); - - it('renders file rows with view and history links when data is available', () => { - mockRepositoryFilesQuery({ - isSuccess: true, - status: 'fulfilled', - data: { - items: [{ path: 'dashboards/example.json', hash: 'abc', size: '10' }], - }, - }); - - renderComponent(); - - const viewLink = screen.getByRole('link', { name: 'View' }); - expect(viewLink).toHaveAttribute('href', '/admin/provisioning/test-repo/file/dashboards/example.json'); - - const historyLink = screen.getByRole('link', { name: 'History' }); - expect(historyLink).toHaveAttribute( - 'href', - '/admin/provisioning/test-repo/history/dashboards/example.json?repo_type=github' - ); - }); - - it('filters files using search input', async () => { - const mockItems = [ - { path: 'dashboards/example.json', hash: 'abc', size: '10' }, - { path: 'dashboards/other.yaml', hash: 'def', size: '20' }, - ]; - - mockRepositoryFilesQuery({ - isSuccess: true, - status: 'fulfilled', - data: { - items: mockItems, - }, - }); - - const { user } = renderComponent(); - - expect(screen.getAllByRole('row')).toHaveLength( - // +1 for the header row - mockItems.length + 1 - ); - - const input = screen.getByPlaceholderText('Search'); - await user.clear(input); - await user.type(input, 'other'); - - await waitFor(() => - expect(screen.getAllByRole('row')).toHaveLength( - // +1 for the header row - 2 - ) - ); - expect(screen.getByText('dashboards/other.yaml')).toBeInTheDocument(); - }); - - it('hides history link when repository type is not supported', () => { - mockRepositoryFilesQuery({ - isSuccess: true, - status: 'fulfilled', - data: { - items: [{ path: 'dashboards/example.json', hash: 'abc', size: '10' }], - }, - }); - - renderComponent(localRepository); - - expect(screen.getByRole('link', { name: 'View' })).toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'History' })).not.toBeInTheDocument(); - }); - - it('renders plain text and hides actions for .keep files', () => { - mockRepositoryFilesQuery({ - isSuccess: true, - status: 'fulfilled', - data: { - items: [{ path: 'dashboards/.keep', hash: 'abc', size: '0' }], - }, - }); - - renderComponent(); - - expect(screen.getByText('dashboards/.keep')).toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'dashboards/.keep' })).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'View' })).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'History' })).not.toBeInTheDocument(); - }); -}); diff --git a/public/app/features/provisioning/File/FilesView.tsx b/public/app/features/provisioning/File/FilesView.tsx deleted file mode 100644 index 97ac77b2fc4..00000000000 --- a/public/app/features/provisioning/File/FilesView.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from 'react'; - -import { Trans, t } from '@grafana/i18n'; -import { CellProps, Column, FilterInput, InteractiveTable, LinkButton, Spinner, Stack } from '@grafana/ui'; -import { Repository, useGetRepositoryFilesQuery } from 'app/api/clients/provisioning/v0alpha1'; - -import { PROVISIONING_URL } from '../constants'; -import { FileDetails } from '../types'; - -import { isFileHistorySupported } from './utils'; - -interface FilesViewProps { - repo: Repository; -} - -type FileCell = CellProps; - -export function FilesView({ repo }: FilesViewProps) { - const name = repo.metadata?.name ?? ''; - const query = useGetRepositoryFilesQuery({ name }); - const [searchQuery, setSearchQuery] = useState(''); - const data = [...(query.data?.items ?? [])].filter((file) => - file.path.toLowerCase().includes(searchQuery.toLowerCase()) - ); - const showHistoryBtn = isFileHistorySupported(repo.spec?.type); - - const columns: Array> = [ - { - id: 'path', - header: 'Path', - sortType: 'string', - cell: ({ row: { original } }: FileCell<'path'>) => { - const { path } = original; - const isDotKeepFile = getIsDotKeepFile(path); - if (isDotKeepFile) { - return path; - } - return {path}; - }, - }, - { - id: 'hash', - header: 'Hash', - sortType: 'string', - }, - { - id: 'actions', - header: '', - cell: ({ row: { original } }: FileCell<'path'>) => { - const { path } = original; - const isDotKeepFile = getIsDotKeepFile(path); - if (isDotKeepFile) { - return null; - } - return ( - - {(path.endsWith('.json') || path.endsWith('.yaml') || path.endsWith('.yml')) && ( - - View - - )} - {showHistoryBtn && ( - - History - - )} - - ); - }, - }, - ]; - - if (query.isLoading) { - return ( - - - - ); - } - - return ( - - - - - String(f.path)} /> - - ); -} - -function getIsDotKeepFile(path: string): boolean { - // e.g. 'dashboards/.keep' → true, 'dashboards/example.keep.json' → false - return path.split('/').pop() === '.keep'; -} diff --git a/public/app/features/provisioning/Repository/RepositoryResources.tsx b/public/app/features/provisioning/Repository/RepositoryResources.tsx deleted file mode 100644 index fdffe1cea74..00000000000 --- a/public/app/features/provisioning/Repository/RepositoryResources.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useMemo, useState } from 'react'; - -import { Trans, t } from '@grafana/i18n'; -import { CellProps, Column, FilterInput, InteractiveTable, Link, LinkButton, Spinner, Stack } from '@grafana/ui'; -import { Repository, ResourceListItem, useGetRepositoryResourcesQuery } from 'app/api/clients/provisioning/v0alpha1'; - -import { isFileHistorySupported } from '../File/utils'; -import { PROVISIONING_URL } from '../constants'; - -interface RepoProps { - repo: Repository; -} - -type ResourceCell = CellProps< - ResourceListItem, - ResourceListItem[T] ->; - -export function RepositoryResources({ repo }: RepoProps) { - const name = repo.metadata?.name ?? ''; - const query = useGetRepositoryResourcesQuery({ name }); - const [searchQuery, setSearchQuery] = useState(''); - const data = (query.data?.items ?? []).filter((Resource) => - Resource.path.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - // hide history button when repo type is pure git as it won't be implemented. - const historySupported = isFileHistorySupported(repo.spec?.type); - - const columns: Array> = useMemo( - () => [ - { - id: 'title', - header: 'Title', - sortType: 'string', - cell: ({ row: { original } }: ResourceCell<'title'>) => { - const { resource, name, title } = original; - if (resource === 'dashboards') { - return {title}; - } - if (resource === 'folders') { - return {title}; - } - return {title}; - }, - }, - { - id: 'resource', - header: 'Type', - sortType: 'string', - cell: ({ row: { original } }: ResourceCell<'resource'>) => { - return {original.resource}; - }, - }, - { - id: 'path', - header: 'Path', - sortType: 'string', - cell: ({ row: { original } }: ResourceCell<'path'>) => { - const { resource, name, path } = original; - if (resource === 'dashboards') { - return {path}; - } - return {path}; - }, - }, - { - id: 'hash', - header: 'Hash', - sortType: 'string', - cell: ({ row: { original } }: ResourceCell<'hash'>) => { - const { hash } = original; - return {hash.substring(0, 7)}; - }, - }, - { - id: 'folder', - header: 'Folder', - sortType: 'string', - cell: ({ row: { original } }: ResourceCell<'title'>) => { - const { folder } = original; - if (folder?.length) { - return {folder}; - } - return ; - }, - }, - { - id: 'actions', - header: '', - cell: ({ row: { original } }: ResourceCell) => { - const { resource, name, path } = original; - return ( - - {resource === 'dashboards' && ( - - View - - )} - {resource === 'folders' && ( - - View - - )} - {historySupported && ( - - History - - )} - - ); - }, - }, - ], - [repo.metadata?.name, historySupported, repo.spec?.type] - ); - - if (query.isLoading) { - return ( - - - - ); - } - - return ( - - - - - String(r.path)} - /> - - ); -} diff --git a/public/app/features/provisioning/Repository/RepositoryStatusPage.tsx b/public/app/features/provisioning/Repository/RepositoryStatusPage.tsx index 07eefb59ca9..b41bcc3260f 100644 --- a/public/app/features/provisioning/Repository/RepositoryStatusPage.tsx +++ b/public/app/features/provisioning/Repository/RepositoryStatusPage.tsx @@ -4,24 +4,22 @@ import { useParams } from 'react-router-dom-v5-compat'; import { SelectableValue, urlUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Alert, EmptyState, Spinner, Tab, TabContent, TabsBar, Text, TextLink } from '@grafana/ui'; +import { Alert, EmptyState, Spinner, Stack, Tab, TabContent, TabsBar, Text, TextLink } from '@grafana/ui'; import { useGetFrontendSettingsQuery, useListRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1'; import { Page } from 'app/core/components/Page/Page'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { isNotFoundError } from 'app/features/alerting/unified/api/util'; -import { FilesView } from '../File/FilesView'; import { InlineSecureValueWarning } from '../components/InlineSecureValueWarning'; import { PROVISIONING_URL } from '../constants'; import { RepositoryActions } from './RepositoryActions'; import { RepositoryOverview } from './RepositoryOverview'; -import { RepositoryResources } from './RepositoryResources'; +import { ResourceTreeView } from './ResourceTreeView'; enum TabSelection { Overview = 'overview', Resources = 'resources', - Files = 'files', } export default function RepositoryStatusPage() { @@ -50,12 +48,7 @@ export default function RepositoryStatusPage() { { value: TabSelection.Resources, label: t('provisioning.repository-status-page.tab-resources', 'Resources'), - title: t('provisioning.repository-status-page.tab-resources-title', 'Resources saved in grafana database'), - }, - { - value: TabSelection.Files, - label: t('provisioning.repository-status-page.tab-files', 'Files'), - title: t('provisioning.repository-status-page.tab-files-title', 'The raw file list from the repository'), + title: t('provisioning.repository-status-page.tab-resources-title', 'Repository files and resources'), }, ], [] @@ -99,7 +92,7 @@ export default function RepositoryStatusPage() { ) : ( <> {data ? ( - <> + {tabInfo.map((t: SelectableValue) => ( )} {tab === TabSelection.Overview && } - {tab === TabSelection.Resources && } - {tab === TabSelection.Files && } + {tab === TabSelection.Resources && } - + ) : (
not found diff --git a/public/app/features/provisioning/Repository/ResourceTreeView.tsx b/public/app/features/provisioning/Repository/ResourceTreeView.tsx new file mode 100644 index 00000000000..58282803841 --- /dev/null +++ b/public/app/features/provisioning/Repository/ResourceTreeView.tsx @@ -0,0 +1,213 @@ +import { css } from '@emotion/css'; +import { useMemo, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { + CellProps, + Column, + FilterInput, + Icon, + InteractiveTable, + Link, + LinkButton, + Spinner, + Stack, + useStyles2, +} from '@grafana/ui'; +import { + Repository, + useGetRepositoryFilesQuery, + useGetRepositoryResourcesQuery, +} from 'app/api/clients/provisioning/v0alpha1'; + +import { FlatTreeItem, TreeItem } from '../types'; +import { getRepoFileUrl } from '../utils/git'; +import { buildTree, filterTree, flattenTree, getIconName, mergeFilesAndResources } from '../utils/treeUtils'; + +interface ResourceTreeViewProps { + repo: Repository; +} + +type TreeCell = CellProps; + +function getGrafanaLink(item: TreeItem) { + if (item.resourceName) { + if (item.type === 'Dashboard') { + return `/d/${item.resourceName}`; + } + if (item.type === 'Folder') { + return `/dashboards/f/${item.resourceName}`; + } + } + return undefined; +} + +export function ResourceTreeView({ repo }: ResourceTreeViewProps) { + const styles = useStyles2(getStyles); + const name = repo.metadata?.name ?? ''; + + const filesQuery = useGetRepositoryFilesQuery({ name }); + const resourcesQuery = useGetRepositoryResourcesQuery({ name }); + + const [searchQuery, setSearchQuery] = useState(''); + + const isLoading = filesQuery.isLoading || resourcesQuery.isLoading; + + const flatItems = useMemo(() => { + const files = filesQuery.data?.items ?? []; + const resources = resourcesQuery.data?.items ?? []; + + const merged = mergeFilesAndResources(files, resources); + let tree = buildTree(merged); + + if (searchQuery) { + tree = filterTree(tree, searchQuery); + } + + return flattenTree(tree); + }, [filesQuery.data?.items, resourcesQuery.data?.items, searchQuery]); + + const columns: Array> = useMemo( + () => [ + { + id: 'title', + header: t('provisioning.resource-tree.header-title', 'Title'), + cell: ({ row: { original } }: TreeCell) => { + const { item, level } = original; + const iconName = getIconName(item.type); + const link = getGrafanaLink(item); + + return ( +
+ + {link ? {item.title} : {item.title}} +
+ ); + }, + }, + { + id: 'type', + header: t('provisioning.resource-tree.header-type', 'Type'), + cell: ({ row: { original } }: TreeCell) => { + return {original.item.type}; + }, + }, + { + id: 'status', + header: t('provisioning.resource-tree.header-status', 'Status'), + cell: ({ row: { original } }: TreeCell) => { + const { status } = original.item; + if (!status) { + return null; + } + return ( + + ); + }, + }, + { + id: 'hash', + header: t('provisioning.resource-tree.header-hash', 'Hash'), + cell: ({ row: { original } }: TreeCell) => { + const { hash } = original.item; + if (!hash) { + return null; + } + return ( + + {hash.substring(0, 7)} + + ); + }, + }, + { + id: 'actions', + header: '', + cell: ({ row: { original } }: TreeCell) => { + const { item } = original; + const isDotKeepFile = item.path.endsWith('.keep') || item.path.endsWith('.gitkeep'); + if (isDotKeepFile) { + return null; + } + + const viewLink = getGrafanaLink(item); + const sourceLink = item.hasFile ? getRepoFileUrl(repo.spec, item.path) : undefined; + + if (!viewLink && !sourceLink) { + return null; + } + + return ( + + {viewLink && ( + + View + + )} + {sourceLink && ( + + Source + + )} + + ); + }, + }, + ], + [repo.spec, styles] + ); + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + item.item.path} + /> + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + titleCell: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + icon: css({ + color: theme.colors.text.secondary, + flexShrink: 0, + }), + hash: css({ + fontFamily: theme.typography.fontFamilyMonospace, + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + }), + syncedIcon: css({ + color: theme.colors.success.text, + }), +}); diff --git a/public/app/features/provisioning/types.ts b/public/app/features/provisioning/types.ts index 5603df43c04..0c8dfec5dbd 100644 --- a/public/app/features/provisioning/types.ts +++ b/public/app/features/provisioning/types.ts @@ -83,7 +83,7 @@ export type AuthorInfo = { export type FileDetails = { path: string; - size: string; + size?: string; hash: string; }; @@ -98,3 +98,24 @@ export interface StatusInfo { title?: string; message?: string | string[]; } + +// Tree view types for combined Resources/Files view +export type ItemType = 'Folder' | 'File' | 'Dashboard'; +export type SyncStatus = 'synced' | 'pending'; + +export interface TreeItem { + title: string; + type: ItemType; + path: string; + level: number; + children: TreeItem[]; + resourceName?: string; + hash?: string; + status?: SyncStatus; + hasFile?: boolean; +} + +export interface FlatTreeItem { + item: TreeItem; + level: number; +} diff --git a/public/app/features/provisioning/utils/git.ts b/public/app/features/provisioning/utils/git.ts index 558f471744f..4fec609c388 100644 --- a/public/app/features/provisioning/utils/git.ts +++ b/public/app/features/provisioning/utils/git.ts @@ -17,16 +17,6 @@ export function validateBranchName(branchName?: string) { return branchName && branchNameRegex.test(branchName!); } -export const getRepoHref = (github?: RepositorySpec['github']) => { - if (!github?.url) { - return undefined; - } - if (!github.branch) { - return github.url; - } - return `${github.url}/tree/${github.branch}`; -}; - // Remove leading and trailing slashes from a string. const stripSlashes = (s: string) => s.replace(/^\/+|\/+$/g, ''); @@ -111,39 +101,106 @@ export function getHasTokenInstructions(type: RepoType): type is InstructionAvai return type === 'github' || type === 'gitlab' || type === 'bitbucket'; } -export function getRepoCommitUrl(spec?: RepositorySpec, commit?: string) { - let url: string | undefined = undefined; - let hasUrl = false; +export function getRepoFileUrl(spec?: RepositorySpec, filePath?: string) { + if (!spec || !spec.type || !filePath) { + return undefined; + } + switch (spec.type) { + case 'github': { + const { url, branch, path } = spec.github ?? {}; + if (!url) { + return undefined; + } + const fullPath = path ? `${path}${filePath}` : filePath; + return buildRepoUrl({ + baseUrl: url, + branch: branch || 'main', + providerSegments: ['blob'], + path: fullPath, + }); + } + case 'gitlab': { + const { url, branch, path } = spec.gitlab ?? {}; + if (!url) { + return undefined; + } + const fullPath = path ? `${path}${filePath}` : filePath; + return buildRepoUrl({ + baseUrl: url, + branch: branch || 'main', + providerSegments: ['-', 'blob'], + path: fullPath, + }); + } + case 'bitbucket': { + const { url, branch, path } = spec.bitbucket ?? {}; + if (!url) { + return undefined; + } + const fullPath = path ? `${path}${filePath}` : filePath; + return buildRepoUrl({ + baseUrl: url, + branch: branch || 'main', + providerSegments: ['src'], + path: fullPath, + }); + } + default: + return undefined; + } +} + +export function getRepoCommitUrl(spec?: RepositorySpec, commit?: string) { if (!spec || !spec.type || !commit) { - return { hasUrl, url }; + return { hasUrl: false, url: undefined }; } const gitType = spec.type; // local repositories don't have a URL - if (gitType !== 'local' && commit) { - switch (gitType) { - case 'github': - if (spec.github?.url) { - url = `${spec.github.url}/commit/${commit}`; - hasUrl = true; - } - break; - case 'gitlab': - if (spec.gitlab?.url) { - url = `${spec.gitlab.url}/-/commit/${commit}`; - hasUrl = true; - } - break; - case 'bitbucket': - if (spec.bitbucket?.url) { - url = `${spec.bitbucket.url}/commits/${commit}`; - hasUrl = true; - } - break; - } + if (gitType === 'local') { + return { hasUrl: false, url: undefined }; } - return { hasUrl, url }; + let url: string | undefined = undefined; + let providerSegments: string[] = []; + + switch (gitType) { + case 'github': + if (spec.github?.url) { + providerSegments = ['commit']; + url = buildRepoUrl({ + baseUrl: spec.github.url, + branch: undefined, + providerSegments, + path: commit, + }); + } + break; + case 'gitlab': + if (spec.gitlab?.url) { + providerSegments = ['-', 'commit']; + url = buildRepoUrl({ + baseUrl: spec.gitlab.url, + branch: undefined, + providerSegments, + path: commit, + }); + } + break; + case 'bitbucket': + if (spec.bitbucket?.url) { + providerSegments = ['commits']; + url = buildRepoUrl({ + baseUrl: spec.bitbucket.url, + branch: undefined, + providerSegments, + path: commit, + }); + } + break; + } + + return { hasUrl: !!url, url }; } diff --git a/public/app/features/provisioning/utils/treeUtils.test.ts b/public/app/features/provisioning/utils/treeUtils.test.ts new file mode 100644 index 00000000000..bce7c1cafb5 --- /dev/null +++ b/public/app/features/provisioning/utils/treeUtils.test.ts @@ -0,0 +1,715 @@ +import { ResourceListItem } from 'app/api/clients/provisioning/v0alpha1'; + +import { TreeItem } from '../types'; + +import { buildTree, filterTree, flattenTree, getItemType, getStatus, mergeFilesAndResources } from './treeUtils'; + +// Mock data +const mockFileDetails = { + path: 'dashboards/my-dashboard.json', + size: '1234', + hash: 'abc123def456', +}; + +const mockResource: ResourceListItem = { + path: 'dashboards/my-dashboard.json', + name: 'dashboard-uid', + title: 'My Dashboard', + resource: 'dashboards', + hash: 'abc123def456', + folder: '', + group: 'dashboard.grafana.app', +}; + +const mockFolderResource: ResourceListItem = { + path: 'dashboards', + name: 'folder-uid', + title: 'Dashboards Folder', + resource: 'folders', + hash: 'xyz789', + folder: '', + group: 'folder.grafana.app', +}; + +describe('mergeFilesAndResources', () => { + it('should merge files and resources by path', () => { + const files = [mockFileDetails]; + const resources = [mockResource]; + + const result = mergeFilesAndResources(files, resources); + + // 2 items: the file + inferred folder 'dashboards' + expect(result).toHaveLength(2); + const file = result.find((r) => r.path === 'dashboards/my-dashboard.json'); + expect(file?.file).toEqual(mockFileDetails); + expect(file?.resource).toEqual(mockResource); + + const folder = result.find((r) => r.path === 'dashboards'); + expect(folder?.file).toEqual({ path: 'dashboards', hash: '' }); + expect(folder?.resource).toBeUndefined(); + }); + + it('should handle files without matching resources', () => { + const files = [{ path: 'orphan-file.json', size: '100', hash: 'hash1' }]; + const resources: ResourceListItem[] = []; + + const result = mergeFilesAndResources(files, resources); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('orphan-file.json'); + expect(result[0].file).toBeDefined(); + expect(result[0].resource).toBeUndefined(); + }); + + it('should handle resources without matching files', () => { + const files: unknown[] = []; + const resources = [mockResource]; + + const result = mergeFilesAndResources(files, resources); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('dashboards/my-dashboard.json'); + expect(result[0].file).toBeUndefined(); + expect(result[0].resource).toEqual(mockResource); + }); + + it('should handle empty arrays', () => { + const result = mergeFilesAndResources([], []); + + expect(result).toHaveLength(0); + }); + + it('should filter out invalid file objects', () => { + const files = [ + mockFileDetails, + { invalid: 'object' }, // Missing path and hash + null, + undefined, + 'string', + ]; + const resources: ResourceListItem[] = []; + + const result = mergeFilesAndResources(files, resources); + + // 2 items: the file + inferred folder 'dashboards' + expect(result).toHaveLength(2); + expect(result.find((r) => r.path === 'dashboards/my-dashboard.json')).toBeDefined(); + expect(result.find((r) => r.path === 'dashboards')).toBeDefined(); + }); + + it('should skip resources with empty path (root)', () => { + const files: unknown[] = []; + const rootResource = { ...mockResource, path: '' }; + const resources = [rootResource, mockResource]; + + const result = mergeFilesAndResources(files, resources); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('dashboards/my-dashboard.json'); + }); + + it('should handle folder in resources but not in files', () => { + const files = [ + { + path: 'new-dashboard-2025-10-24-NKAPX.json', + hash: '78383507641a9fe0c6dc715bf81989c2732e84df', + }, + ]; + const resources: ResourceListItem[] = [ + { + path: 'new-dashboard-2025-10-24-NKAPX.json', + group: 'dashboard.grafana.app', + resource: 'dashboards', + name: 'dcf20b2odenyf4d', + hash: '78383507641a9fe0c6dc715bf81989c2732e84df', + title: 'v2 dashboard', + folder: 'repository-89cac64', + }, + { + path: 'unsynced-folder', + group: 'folder.grafana.app', + resource: 'folders', + name: 'unsynced-folder-pyqothnbi8kcxjvo7tnujum7', + hash: '', + title: 'unsynced-folder', + folder: 'repository-89cac64', + }, + ]; + + const result = mergeFilesAndResources(files, resources); + + expect(result).toHaveLength(2); + + const dashboard = result.find((r) => r.path === 'new-dashboard-2025-10-24-NKAPX.json'); + expect(dashboard?.file).toBeDefined(); + expect(dashboard?.resource).toBeDefined(); + + const folder = result.find((r) => r.path === 'unsynced-folder'); + expect(folder?.file).toBeUndefined(); + expect(folder?.resource).toBeDefined(); + expect(folder?.resource?.resource).toBe('folders'); + }); +}); + +describe('getItemType', () => { + it('should return Dashboard for dashboard resources', () => { + const result = getItemType('dashboards/test.json', mockResource); + + expect(result).toBe('Dashboard'); + }); + + it('should return Folder for folder resources', () => { + const result = getItemType('dashboards', mockFolderResource); + + expect(result).toBe('Folder'); + }); + + it('should return File for unsynced files regardless of extension', () => { + const result = getItemType('some/path/file.json', undefined); + + expect(result).toBe('File'); + }); + + it('should return File for non-JSON paths without resource', () => { + const result = getItemType('some/path/file.txt', undefined); + + expect(result).toBe('File'); + }); + + it('should return File when resource type is unknown', () => { + const unknownResource = { + ...mockResource, + resource: 'unknown-type', + }; + + const result = getItemType('some/path', unknownResource); + + expect(result).toBe('File'); + }); +}); + +describe('getStatus', () => { + it('should return synced when both hashes exist and match', () => { + expect(getStatus('abc123', 'abc123')).toBe('synced'); + }); + + it('should return pending when both hashes exist but differ', () => { + expect(getStatus('abc123', 'xyz789')).toBe('pending'); + }); + + it('should return pending when only file hash exists', () => { + expect(getStatus('abc123', undefined)).toBe('pending'); + }); + + it('should return pending when only resource hash exists', () => { + expect(getStatus(undefined, 'abc123')).toBe('pending'); + }); + + it('should return pending when neither hash exists', () => { + expect(getStatus(undefined, undefined)).toBe('pending'); + }); + + it('should return synced for inferred folder (empty file hash) with resource', () => { + // Empty hash means folder was inferred from file paths + expect(getStatus('', 'abc123')).toBe('synced'); + }); + + it('should return pending for inferred folder (empty file hash) without resource', () => { + expect(getStatus('', undefined)).toBe('pending'); + }); +}); + +describe('buildTree', () => { + it('should build tree with folder hierarchy', () => { + const mergedItems = [ + { path: 'folder', file: { path: 'folder', hash: '' } }, + { path: 'folder/subfolder', file: { path: 'folder/subfolder', hash: '' } }, + { path: 'folder/subfolder/file.json', file: { path: 'folder/subfolder/file.json', size: '100', hash: 'h1' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe('Folder'); + expect(result[0].path).toBe('folder'); + expect(result[0].children).toHaveLength(1); + expect(result[0].children[0].type).toBe('Folder'); + expect(result[0].children[0].path).toBe('folder/subfolder'); + }); + + it('should place files under correct parent folders', () => { + const mergedItems = [ + { path: 'folder', file: { path: 'folder', hash: '' } }, + { path: 'folder/file.txt', file: { path: 'folder/file.txt', size: '100', hash: 'h1' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('folder'); + expect(result[0].children).toHaveLength(1); + expect(result[0].children[0].path).toBe('folder/file.txt'); + expect(result[0].children[0].type).toBe('File'); + }); + + it('should sort folders before files', () => { + const mergedItems = [ + { path: 'file.txt', file: { path: 'file.txt', size: '100', hash: 'h1' } }, + { path: 'folder', file: { path: 'folder', hash: '' } }, + { path: 'folder/nested.txt', file: { path: 'folder/nested.txt', size: '100', hash: 'h2' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(2); + expect(result[0].type).toBe('Folder'); + expect(result[0].title).toBe('folder'); + expect(result[1].type).toBe('File'); + expect(result[1].title).toBe('file.txt'); + }); + + it('should sort alphabetically within same type', () => { + const mergedItems = [ + { path: 'zebra.json', file: { path: 'zebra.json', size: '100', hash: 'h1' } }, + { path: 'apple.json', file: { path: 'apple.json', size: '100', hash: 'h2' } }, + { path: 'mango.json', file: { path: 'mango.json', size: '100', hash: 'h3' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(3); + expect(result[0].title).toBe('apple.json'); + expect(result[1].title).toBe('mango.json'); + expect(result[2].title).toBe('zebra.json'); + }); + + it('should handle root-level items', () => { + const mergedItems = [{ path: 'root-file.txt', file: { path: 'root-file.txt', size: '100', hash: 'h1' } }]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('root-file.txt'); + expect(result[0].type).toBe('File'); + }); + + it('should handle deeply nested paths', () => { + const mergedItems = [ + { path: 'a', file: { path: 'a', hash: '' } }, + { path: 'a/b', file: { path: 'a/b', hash: '' } }, + { path: 'a/b/c', file: { path: 'a/b/c', hash: '' } }, + { path: 'a/b/c/d', file: { path: 'a/b/c/d', hash: '' } }, + { path: 'a/b/c/d/e', file: { path: 'a/b/c/d/e', hash: '' } }, + { path: 'a/b/c/d/e/file.txt', file: { path: 'a/b/c/d/e/file.txt', size: '100', hash: 'h1' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('a'); + + // Traverse to the deepest file + let current = result[0]; + const expectedPaths = ['a', 'a/b', 'a/b/c', 'a/b/c/d', 'a/b/c/d/e']; + for (let i = 0; i < expectedPaths.length; i++) { + expect(current.path).toBe(expectedPaths[i]); + expect(current.type).toBe('Folder'); + if (i < expectedPaths.length - 1) { + current = current.children[0]; + } + } + + // Check the file is in the last folder + const lastFolder = current; + expect(lastFolder.children).toHaveLength(1); + expect(lastFolder.children[0].path).toBe('a/b/c/d/e/file.txt'); + expect(lastFolder.children[0].type).toBe('File'); + }); + + it('should handle empty input', () => { + const result = buildTree([]); + + expect(result).toHaveLength(0); + }); + + it('should use resource info for folder nodes when available', () => { + const mergedItems = [ + { path: 'dashboards', resource: mockFolderResource }, + { path: 'dashboards/test.json', resource: mockResource }, + ]; + + const result = buildTree(mergedItems); + + expect(result).toHaveLength(1); + expect(result[0].title).toBe('Dashboards Folder'); + expect(result[0].resourceName).toBe('folder-uid'); + }); + + it('should set synced status when file and resource hashes match', () => { + const mergedItems = [ + { + path: 'dashboard.json', + file: { path: 'dashboard.json', size: '100', hash: 'abc123def456' }, + resource: mockResource, // mockResource has hash: 'abc123def456' + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].status).toBe('synced'); + }); + + it('should set pending status when file and resource hashes differ', () => { + const mergedItems = [ + { + path: 'dashboard.json', + file: { path: 'dashboard.json', size: '100', hash: 'different-hash' }, + resource: mockResource, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].status).toBe('pending'); + }); + + it('should not set status for non-JSON files', () => { + const mergedItems = [{ path: 'file.txt', file: { path: 'file.txt', size: '100', hash: 'h1' } }]; + + const result = buildTree(mergedItems); + + expect(result[0].status).toBeUndefined(); + }); + + it('should show unsynced JSON files as File type with pending status', () => { + const mergedItems = [{ path: 'dashboard.json', file: { path: 'dashboard.json', size: '100', hash: 'h1' } }]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('File'); + expect(result[0].status).toBe('pending'); + }); + + it('should set pending status when only resource exists', () => { + const mergedItems = [{ path: 'dashboard.json', resource: mockResource }]; + + const result = buildTree(mergedItems); + + expect(result[0].status).toBe('pending'); + }); + + it('should set folder status to synced when all children are synced', () => { + const syncedResource = { ...mockResource, hash: 'matching-hash' }; + const mergedItems = [ + { path: 'folder', file: { path: 'folder', hash: '' }, resource: mockFolderResource }, + { + path: 'folder/dashboard1.json', + file: { path: 'folder/dashboard1.json', size: '100', hash: 'matching-hash' }, + resource: syncedResource, + }, + { + path: 'folder/dashboard2.json', + file: { path: 'folder/dashboard2.json', size: '100', hash: 'matching-hash' }, + resource: { ...syncedResource, name: 'other-uid' }, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('Folder'); + expect(result[0].resourceName).toBe('folder-uid'); + expect(result[0].status).toBe('synced'); + }); + + it('should set folder status to pending when any child is pending', () => { + const syncedResource = { ...mockResource, hash: 'matching-hash' }; + const mergedItems = [ + { path: 'folder', resource: mockFolderResource }, + { + path: 'folder/dashboard1.json', + file: { path: 'folder/dashboard1.json', size: '100', hash: 'matching-hash' }, + resource: syncedResource, + }, + { + path: 'folder/dashboard2.json', + file: { path: 'folder/dashboard2.json', size: '100', hash: 'different-hash' }, + resource: syncedResource, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('Folder'); + expect(result[0].resourceName).toBe('folder-uid'); + expect(result[0].status).toBe('pending'); + }); + + it('should propagate pending status from nested folders', () => { + const syncedResource = { ...mockResource, hash: 'matching-hash' }; + const mergedItems = [ + { path: 'parent', file: { path: 'parent', hash: '' } }, + { path: 'parent/child', file: { path: 'parent/child', hash: '' } }, + { + path: 'parent/child/dashboard.json', + file: { path: 'parent/child/dashboard.json', size: '100', hash: 'different-hash' }, + resource: syncedResource, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].path).toBe('parent'); + expect(result[0].status).toBe('pending'); + expect(result[0].children[0].path).toBe('parent/child'); + expect(result[0].children[0].status).toBe('pending'); + }); + + it('should set pending status for unsynced folders with no dashboard children', () => { + const mergedItems = [ + { + path: 'unsynced-folder', + resource: { + path: 'unsynced-folder', + group: 'folder.grafana.app', + resource: 'folders', + name: 'unsynced-folder-pyqothnbi8kcxjvo7tnujum7', + hash: '', + title: 'unsynced-folder', + folder: 'repository-89cac64', + }, + }, + { + path: 'new-dashboard-2025-10-24-NKAPX.json', + file: { + path: 'new-dashboard-2025-10-24-NKAPX.json', + hash: '78383507641a9fe0c6dc715bf81989c2732e84df', + }, + resource: { + path: 'new-dashboard-2025-10-24-NKAPX.json', + group: 'dashboard.grafana.app', + resource: 'dashboards', + name: 'dcf20b2odenyf4d', + hash: '78383507641a9fe0c6dc715bf81989c2732e84df', + title: 'v2 dashboard', + folder: 'repository-89cac64', + }, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('Folder'); + expect(result[0].status).toBe('pending'); + }); + + it('should set pending status for folder in resources but not in files', () => { + // Folder only exists in resources (e.g., deleted from repo but not synced yet) + const mergedItems = [ + { path: 'folder', resource: mockFolderResource }, + { path: 'folder/file.txt', file: { path: 'folder/file.txt', size: '100', hash: 'h1' } }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('Folder'); + expect(result[0].resourceName).toBe('folder-uid'); + expect(result[0].status).toBe('pending'); + }); + + it('should set synced status for folder inferred from files with matching resource', () => { + // Folder inferred from file paths AND exists in resources → synced + const syncedResource = { ...mockResource, hash: 'matching-hash' }; + const mergedItems = [ + { path: 'folder', file: { path: 'folder', hash: '' }, resource: mockFolderResource }, + { + path: 'folder/dashboard.json', + file: { path: 'folder/dashboard.json', size: '100', hash: 'matching-hash' }, + resource: syncedResource, + }, + ]; + + const result = buildTree(mergedItems); + + expect(result[0].type).toBe('Folder'); + expect(result[0].resourceName).toBe('folder-uid'); + expect(result[0].status).toBe('synced'); + }); +}); + +describe('flattenTree', () => { + it('should flatten nested tree structure', () => { + const tree: TreeItem[] = [ + { + path: 'folder', + title: 'Folder', + type: 'Folder', + level: 0, + children: [ + { + path: 'folder/file.json', + title: 'file.json', + type: 'File', + level: 0, + children: [], + }, + ], + }, + ]; + + const result = flattenTree(tree); + + expect(result).toHaveLength(2); + expect(result[0].item.path).toBe('folder'); + expect(result[1].item.path).toBe('folder/file.json'); + }); + + it('should set correct level for each item', () => { + const tree: TreeItem[] = [ + { + path: 'folder', + title: 'Folder', + type: 'Folder', + level: 0, + children: [ + { + path: 'folder/subfolder', + title: 'Subfolder', + type: 'Folder', + level: 0, + children: [ + { + path: 'folder/subfolder/file.json', + title: 'file.json', + type: 'File', + level: 0, + children: [], + }, + ], + }, + ], + }, + ]; + + const result = flattenTree(tree); + + expect(result).toHaveLength(3); + expect(result[0].level).toBe(0); + expect(result[1].level).toBe(1); + expect(result[2].level).toBe(2); + }); + + it('should include all children', () => { + const tree: TreeItem[] = [ + { + path: 'folder', + title: 'Folder', + type: 'Folder', + level: 0, + children: [ + { path: 'folder/a.json', title: 'a.json', type: 'File', level: 0, children: [] }, + { path: 'folder/b.json', title: 'b.json', type: 'File', level: 0, children: [] }, + { path: 'folder/c.json', title: 'c.json', type: 'File', level: 0, children: [] }, + ], + }, + ]; + + const result = flattenTree(tree); + + expect(result).toHaveLength(4); + }); + + it('should handle empty tree', () => { + const result = flattenTree([]); + + expect(result).toHaveLength(0); + }); +}); + +describe('filterTree', () => { + const sampleTree: TreeItem[] = [ + { + path: 'dashboards', + title: 'Dashboards', + type: 'Folder', + level: 0, + children: [ + { + path: 'dashboards/monitoring.json', + title: 'System Monitoring', + type: 'Dashboard', + level: 0, + children: [], + }, + { + path: 'dashboards/sales.json', + title: 'Sales Report', + type: 'Dashboard', + level: 0, + children: [], + }, + ], + }, + { + path: 'config.json', + title: 'config.json', + type: 'File', + level: 0, + children: [], + }, + ]; + + it('should return all items when query is empty', () => { + const result = filterTree(sampleTree, ''); + + expect(result).toEqual(sampleTree); + }); + + it('should filter by path (case-insensitive)', () => { + const result = filterTree(sampleTree, 'MONITORING'); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('dashboards'); + expect(result[0].children).toHaveLength(1); + expect(result[0].children[0].path).toBe('dashboards/monitoring.json'); + }); + + it('should filter by title (case-insensitive)', () => { + const result = filterTree(sampleTree, 'sales report'); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('dashboards'); + expect(result[0].children).toHaveLength(1); + expect(result[0].children[0].title).toBe('Sales Report'); + }); + + it('should include parent folders when child matches', () => { + const result = filterTree(sampleTree, 'monitoring'); + + expect(result).toHaveLength(1); + expect(result[0].type).toBe('Folder'); + expect(result[0].path).toBe('dashboards'); + expect(result[0].children).toHaveLength(1); + }); + + it('should return empty array when nothing matches', () => { + const result = filterTree(sampleTree, 'nonexistent'); + + expect(result).toHaveLength(0); + }); + + it('should match folder itself if query matches folder name', () => { + const result = filterTree(sampleTree, 'dashboards'); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('dashboards'); + // When folder matches, all children are included + expect(result[0].children).toHaveLength(2); + }); + + it('should match root level items', () => { + const result = filterTree(sampleTree, 'config'); + + expect(result).toHaveLength(1); + expect(result[0].path).toBe('config.json'); + }); +}); diff --git a/public/app/features/provisioning/utils/treeUtils.ts b/public/app/features/provisioning/utils/treeUtils.ts new file mode 100644 index 00000000000..9cf56580a98 --- /dev/null +++ b/public/app/features/provisioning/utils/treeUtils.ts @@ -0,0 +1,229 @@ +import { IconName } from '@grafana/ui'; +import { ResourceListItem } from 'app/api/clients/provisioning/v0alpha1'; + +import { FileDetails, FlatTreeItem, ItemType, SyncStatus, TreeItem } from '../types'; + +const collator = new Intl.Collator(); + +interface MergedItem { + path: string; + file?: FileDetails; + resource?: ResourceListItem; +} + +function isFileDetails(obj: unknown): obj is FileDetails { + return typeof obj === 'object' && obj !== null && 'path' in obj && 'hash' in obj; +} + +export function mergeFilesAndResources(files: unknown[], resources: ResourceListItem[]): MergedItem[] { + const merged = new Map(); + const inferredFolders = new Set(); + + for (const file of files) { + if (isFileDetails(file)) { + merged.set(file.path, { path: file.path, file }); + + // Infer parent folders from file path + const parts = file.path.split('/'); + for (let i = 1; i < parts.length; i++) { + inferredFolders.add(parts.slice(0, i).join('/')); + } + } + } + + // Add inferred folders that don't already exist + for (const folderPath of inferredFolders) { + if (!merged.has(folderPath)) { + merged.set(folderPath, { path: folderPath, file: { path: folderPath, hash: '' } }); + } + } + + // Merge resources + for (const resource of resources) { + if (!resource.path) { + continue; + } + const existing = merged.get(resource.path); + if (existing) { + existing.resource = resource; + } else { + merged.set(resource.path, { path: resource.path, resource }); + } + } + + return Array.from(merged.values()); +} + +export function getItemType(path: string, resource?: ResourceListItem): ItemType { + if (resource?.resource === 'dashboards') { + return 'Dashboard'; + } + if (resource?.resource === 'folders') { + return 'Folder'; + } + // Inferred folder (no extension means it's a folder from file paths) + if (!resource && !path.includes('.')) { + return 'Folder'; + } + // Unsynced files are "File" - don't infer Dashboard from .json + return 'File'; +} + +export function getDisplayTitle(path: string, resource?: ResourceListItem): string { + if (resource?.title) { + return resource.title; + } + return path.split('/').pop() ?? path; +} + +export function getIconName(type: ItemType): IconName { + switch (type) { + case 'Folder': + return 'folder'; + case 'Dashboard': + return 'apps'; + case 'File': + default: + return 'file-alt'; + } +} + +export function getStatus(fileHash?: string, resourceHash?: string): SyncStatus { + if (fileHash !== undefined && resourceHash !== undefined) { + // Empty file hash means inferred folder (synced if resource exists) + return fileHash === '' || fileHash === resourceHash ? 'synced' : 'pending'; + } + return 'pending'; +} + +function calculateFolderStatus(node: TreeItem): SyncStatus | undefined { + if (node.type !== 'Folder') { + return node.status; + } + + // If any child is pending, folder is pending + for (const child of node.children) { + const childStatus = child.type === 'Folder' ? calculateFolderStatus(child) : child.status; + if (childStatus === 'pending') { + return 'pending'; + } + } + + return node.status; +} + +export function buildTree(mergedItems: MergedItem[]): TreeItem[] { + const nodeMap = new Map(); + const roots: TreeItem[] = []; + + // Create all nodes (files, dashboards, folders) + for (const item of mergedItems) { + const type = getItemType(item.path, item.resource); + const showStatus = type === 'Dashboard' || type === 'Folder' || item.path.endsWith('.json'); + + nodeMap.set(item.path, { + path: item.path, + title: getDisplayTitle(item.path, item.resource), + type, + level: 0, + children: [], + resourceName: item.resource?.name, + hash: item.file?.hash ?? item.resource?.hash, + status: showStatus ? getStatus(item.file?.hash, item.resource?.hash) : undefined, + hasFile: !!item.file, + }); + } + + // Build parent-child relationships + for (const [path, node] of nodeMap) { + const lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex === -1) { + roots.push(node); + } else { + const parentPath = path.substring(0, lastSlashIndex); + const parent = nodeMap.get(parentPath); + if (parent) { + parent.children.push(node); + } else { + roots.push(node); + } + } + } + + // Sort: folders first, then alphabetically, recursively + const sortNodes = (nodes: TreeItem[]) => { + nodes.sort((a, b) => { + if (a.type === 'Folder' && b.type !== 'Folder') { + return -1; + } + if (a.type !== 'Folder' && b.type === 'Folder') { + return 1; + } + return collator.compare(a.title, b.title); + }); + for (const node of nodes) { + sortNodes(node.children); + } + }; + + sortNodes(roots); + + // Update folder statuses recursively (folders inherit pending from children) + const updateFolderStatus = (nodes: TreeItem[]) => { + for (const node of nodes) { + if (node.type === 'Folder') { + updateFolderStatus(node.children); + node.status = calculateFolderStatus(node); + } + } + }; + + updateFolderStatus(roots); + return roots; +} + +export function flattenTree(items: TreeItem[], level = 0): FlatTreeItem[] { + const result: FlatTreeItem[] = []; + + for (const item of items) { + result.push({ + item: { ...item, level }, + level, + }); + + if (item.children.length > 0) { + result.push(...flattenTree(item.children, level + 1)); + } + } + + return result; +} + +/** + * Filter tree by search query (searches path and title). + * Returns filtered tree including ancestor folders for matching items. + */ +export function filterTree(items: TreeItem[], searchQuery: string): TreeItem[] { + if (!searchQuery) { + return items; + } + + const lowerQuery = searchQuery.toLowerCase(); + + const filterNode = (node: TreeItem): TreeItem | null => { + const matches = node.path.toLowerCase().includes(lowerQuery) || node.title.toLowerCase().includes(lowerQuery); + + if (matches) { + return node; + } + + if (node.type === 'Folder' && node.children.length > 0) { + const filteredChildren = node.children.map(filterNode).filter((n): n is TreeItem => n !== null); + return filteredChildren.length > 0 ? { ...node, children: filteredChildren } : null; + } + + return null; + }; + + return items.map(filterNode).filter((n): n is TreeItem => n !== null); +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5a55ab0843f..a8ed1bdf15c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Saving", "title-error-loading-file": "Error loading file" }, - "files-view": { - "columns": { - "history": "History", - "view": "View" - }, - "placeholder-search": "Search" - }, "finish-step": { "description-enable-previews": "Adds an image preview of dashboard changes in pull requests. Images of your Grafana dashboards will be shared in your Git repository and visible to anyone with repository access.", "description-generate-dashboard-previews": "Create preview links for pull requests", @@ -11956,14 +11949,6 @@ "webhook-last-event": "Last Event:", "webhook-url": "View Webhook" }, - "repository-resources": { - "columns": { - "history": "History", - "view-dashboard": "View", - "view-folder": "View" - }, - "placeholder-search": "Search" - }, "repository-status-page": { "back-to-repositories": "Back to repositories", "cleaning-up-resources": "Cleaning up repository resources", @@ -11971,12 +11956,10 @@ "not-found": "not found", "not-found-message": "Repository not found", "repository-config-exists-configuration": "Make sure the repository config exists in the configuration file.", - "tab-files": "Files", - "tab-files-title": "The raw file list from the repository", "tab-overview": "Overview", "tab-overview-title": "Repository overview", "tab-resources": "Resources", - "tab-resources-title": "Resources saved in grafana database", + "tab-resources-title": "Repository files and resources", "title": "Repository Status", "title-legacy-storage": "Legacy Storage", "title-queued-for-deletion": "Queued for deletion" @@ -11999,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Connect to any Git repository" }, + "resource-tree": { + "header-hash": "Hash", + "header-status": "Status", + "header-title": "Title", + "header-type": "Type", + "search-placeholder": "Search by path or title", + "source": "Source", + "status-pending": "Pending", + "status-synced": "Synced", + "view": "View" + }, "resource-view": { "base": "Base", "dashboard-preview": "Dashboard Preview", From 1c8f4a745f5b31ebd5399d16a0b2d501f731f6e2 Mon Sep 17 00:00:00 2001 From: "renovate-sh-app[bot]" <219655108+renovate-sh-app[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:47:52 +0100 Subject: [PATCH 136/423] chore(deps): update dependency node-forge to v1.3.2 [security] (#114522) --- yarn.lock | 113 +++++++----------------------------------------------- 1 file changed, 13 insertions(+), 100 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2022568eb8b..91be1a1621f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4251,7 +4251,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/type@npm:^3.0.10": +"@inquirer/type@npm:^3.0.10, @inquirer/type@npm:^3.0.9": version: 3.0.10 resolution: "@inquirer/type@npm:3.0.10" peerDependencies: @@ -4263,18 +4263,6 @@ __metadata: languageName: node linkType: hard -"@inquirer/type@npm:^3.0.9": - version: 3.0.9 - resolution: "@inquirer/type@npm:3.0.9" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10/960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 - languageName: node - linkType: hard - "@internationalized/date@npm:^3.10.0": version: 3.10.0 resolution: "@internationalized/date@npm:3.10.0" @@ -5038,14 +5026,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -8146,17 +8127,7 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.2.12": - version: 1.2.12 - resolution: "@storybook/icons@npm:1.2.12" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/5df56f0856764ed7e4bb24ef7a08a8a9c93f8eedcb16dac062f1dfd3bd1fe6cb4a0aa5a0794083d95e31c04960d126a4d2028cfb4c53681bf05513bb38eae9d2 - languageName: node - linkType: hard - -"@storybook/icons@npm:^1.6.0": +"@storybook/icons@npm:^1.2.12, @storybook/icons@npm:^1.6.0": version: 1.6.0 resolution: "@storybook/icons@npm:1.6.0" peerDependencies: @@ -9281,7 +9252,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.15.2, @swc/core@npm:^1.13.5": +"@swc/core@npm:1.15.2": version: 1.15.2 resolution: "@swc/core@npm:1.15.2" dependencies: @@ -9327,7 +9298,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": +"@swc/core@npm:^1.10.8, @swc/core@npm:^1.13.5, @swc/core@npm:^1.5.22": version: 1.15.3 resolution: "@swc/core@npm:1.15.3" dependencies: @@ -13540,20 +13511,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1": - version: 5.2.0 - resolution: "chai@npm:5.2.0" - dependencies: - assertion-error: "npm:^2.0.1" - check-error: "npm:^2.1.1" - deep-eql: "npm:^5.0.1" - loupe: "npm:^3.1.0" - pathval: "npm:^2.0.0" - checksum: 10/2ce03671c159c6a567bf1912756daabdbb7c075f3c0078f1b59d61da8d276936367ee696dfe093b49e1479d9ba93a6074c8e55d49791dddd8061728cdcad249e - languageName: node - linkType: hard - -"chai@npm:^5.2.0": +"chai@npm:^5.1.1, chai@npm:^5.2.0": version: 5.3.3 resolution: "chai@npm:5.3.3" dependencies: @@ -18451,18 +18409,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": - version: 11.3.0 - resolution: "fs-extra@npm:11.3.0" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^6.0.1" - universalify: "npm:^2.0.0" - checksum: 10/c9fe7b23dded1efe7bbae528d685c3206477e20cc60e9aaceb3f024f9b9ff2ee1f62413c161cb88546cc564009ab516dec99e9781ba782d869bb37e4fe04a97f - languageName: node - linkType: hard - -"fs-extra@npm:^11.3.2": +"fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0, fs-extra@npm:^11.3.2": version: 11.3.2 resolution: "fs-extra@npm:11.3.2" dependencies: @@ -23543,14 +23490,7 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": - version: 3.1.3 - resolution: "loupe@npm:3.1.3" - checksum: 10/9e98c34daf0eba48ccc603595e51f2ae002110982d84879cf78c51de2c632f0c571dfe82ce4210af60c32203d06b443465c269bda925076fe6d9b612cc65c321 - languageName: node - linkType: hard - -"loupe@npm:^3.1.4": +"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2, loupe@npm:^3.1.4": version: 3.2.1 resolution: "loupe@npm:3.2.1" checksum: 10/a4d78ec758aaa04e0e35d5cd1c15e970beb9cdbfd3d0f34f98b9bcda489f896a7190b3b6cc40b7a6dcb8e97e82e96eafaae10096aaa469804acdba6f7c2bde5f @@ -23645,7 +23585,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.17": +"magic-string@npm:^0.30.17, magic-string@npm:^0.30.3, magic-string@npm:^0.30.5": version: 0.30.21 resolution: "magic-string@npm:0.30.21" dependencies: @@ -23654,15 +23594,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.3, magic-string@npm:^0.30.5": - version: 0.30.17 - resolution: "magic-string@npm:0.30.17" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.0" - checksum: 10/2f71af2b0afd78c2e9012a29b066d2c8ba45a9cd0c8070f7fd72de982fb1c403b4e3afdb1dae00691d56885ede66b772ef6bedf765e02e3a7066208fe2fec4aa - languageName: node - linkType: hard - "mailparser@npm:^3.5.0": version: 3.7.1 resolution: "mailparser@npm:3.7.1" @@ -24889,9 +24820,9 @@ __metadata: linkType: hard "node-forge@npm:^1.3.1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d + version: 1.3.2 + resolution: "node-forge@npm:1.3.2" + checksum: 10/dcc54aaffe0cf52367214a20c0032aa9b209d9095dd14526504f1972d1900a07e96046b3684cb0c8d0cc3d48744dd18e02b7b447ab28fac615ffb850beeabf18 languageName: node linkType: hard @@ -28322,25 +28253,7 @@ __metadata: languageName: node linkType: hard -"react-docgen@npm:^7.0.0": - version: 7.0.3 - resolution: "react-docgen@npm:7.0.3" - dependencies: - "@babel/core": "npm:^7.18.9" - "@babel/traverse": "npm:^7.18.9" - "@babel/types": "npm:^7.18.9" - "@types/babel__core": "npm:^7.18.0" - "@types/babel__traverse": "npm:^7.18.0" - "@types/doctrine": "npm:^0.0.9" - "@types/resolve": "npm:^1.20.2" - doctrine: "npm:^3.0.0" - resolve: "npm:^1.22.1" - strip-indent: "npm:^4.0.0" - checksum: 10/53eaed76cceb55606584c6ab603f04ec78c066cfb9ed983e1f7b388a75bfb8c2fc9c6b7ab299bac311b3daeca95adb8076b58ca96b41907b33c518299268831f - languageName: node - linkType: hard - -"react-docgen@npm:^7.1.1": +"react-docgen@npm:^7.0.0, react-docgen@npm:^7.1.1": version: 7.1.1 resolution: "react-docgen@npm:7.1.1" dependencies: From 95174454e3873a53efc94e012784d905d6dccd84 Mon Sep 17 00:00:00 2001 From: "Marc M." <146180665+grafakus@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:05:15 +0100 Subject: [PATCH 137/423] ConditionalRendering: Fix for repeated items (#114160) --- ...-conditional-rendering-load-change.spec.ts | 52 +++- e2e-playwright/dashboard-new-layouts/utils.ts | 14 +- .../DashboardWithAllConditionalRendering.json | 271 ++++++++++++++++++ .../conditions/ConditionalRenderingData.tsx | 19 +- .../ConditionalRenderingTimeRangeSize.tsx | 4 + .../ConditionalRenderingVariable.tsx | 36 ++- .../conditional-rendering/conditions/utils.ts | 7 + .../group/ConditionalRenderingGroup.tsx | 23 +- .../hooks/useIsConditionallyHidden.tsx | 14 +- .../conditional-rendering/object.ts | 4 +- .../scene/layout-auto-grid/AutoGridItem.tsx | 17 +- .../layout-auto-grid/AutoGridItemRenderer.tsx | 39 +-- .../scene/layout-rows/RowItemRenderer.tsx | 5 +- .../scene/layout-rows/RowItemRepeater.tsx | 3 + .../scene/layout-rows/RowsLayoutManager.tsx | 2 + .../scene/layout-tabs/TabItemRenderer.tsx | 6 +- .../scene/layout-tabs/TabItemRepeater.tsx | 3 + .../scene/layout-tabs/TabsLayoutManager.tsx | 2 + 18 files changed, 468 insertions(+), 53 deletions(-) diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-conditional-rendering-load-change.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-conditional-rendering-load-change.spec.ts index 56c46ad8d68..a6f52738ff8 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-conditional-rendering-load-change.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-conditional-rendering-load-change.spec.ts @@ -4,6 +4,8 @@ import { test, expect, E2ESelectorGroups, DashboardPage, DashboardPageArgs } fro import testDashboard from '../dashboards/DashboardWithAllConditionalRendering.json'; +import { checkRepeatedPanelTitles } from './utils'; + test.use({ featureToggles: { kubernetesDashboards: true, @@ -93,7 +95,7 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d test.afterAll(async ({ request }) => { if (uid) { - await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/${uid}`); + await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/stacks-12345/dashboards/${uid}`); } }); @@ -407,4 +409,52 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d await expect(getTabShowNotMatches(dashboardPage, selectors)).toBeVisible(); await expect(getTabHideNotMatches(dashboardPage, selectors)).not.toBeVisible(); }); + + test.describe('Variable repeat', () => { + const repeatOptions = ['a', 'b', 'c']; + + async function failTestDataRequestForOption(page: Page, option: string) { + await page.route(/\/api\/ds\/query\?.*\bds_type=grafana-testdata-datasource/, async (route) => { + const rawPostData = route.request().postData(); + if (!rawPostData) { + return; + } + + // the first panel query has a label set to the current variable value + if (JSON.parse(rawPostData).queries[0].labels === `key=${option}`) { + await route.fulfill({ status: 500, body: '{}' }); + } else { + await route.continue(); + } + }); + } + + test('Hide when equals, hide when no data', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await loadDashboard(page, gotoDashboardPage); + + await getTab(dashboardPage, selectors, 'repeated items').click(); + + const optionForHiddenPanels = repeatOptions[0]; + + await failTestDataRequestForOption(page, optionForHiddenPanels); + + await checkRepeatedPanelTitles( + dashboardPage, + selectors, + 'Hide panel - ', + [ + `custom variable equals ${optionForHiddenPanels} (current = ${optionForHiddenPanels})`, + `no data (current = ${optionForHiddenPanels})`, + ], + true + ); + + const optionsForVisiblePanels = repeatOptions.slice(1); + + await checkRepeatedPanelTitles(dashboardPage, selectors, 'Hide panel - ', [ + ...optionsForVisiblePanels.map((o) => `custom variable equals ${optionForHiddenPanels} (current = ${o})`), + ...optionsForVisiblePanels.map((o) => `no data (current = ${o})`), + ]); + }); + }); }); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index c1e00e1a67b..89f4de0eec3 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -97,12 +97,18 @@ export async function checkRepeatedPanelTitles( dashboardPage: DashboardPage, selectors: E2ESelectorGroups, title: string, - options: Array + options: Array, + expectHidden = false ) { for (const option of options) { - await expect( - dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${title}${option}`)) - ).toBeVisible(); + const titleLocator = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title(`${title}${option}`) + ); + if (expectHidden) { + await expect(titleLocator).toBeHidden(); + } else { + await expect(titleLocator).toBeVisible(); + } } } diff --git a/e2e-playwright/dashboards/DashboardWithAllConditionalRendering.json b/e2e-playwright/dashboards/DashboardWithAllConditionalRendering.json index f0d81daa233..522a5d670ac 100644 --- a/e2e-playwright/dashboards/DashboardWithAllConditionalRendering.json +++ b/e2e-playwright/dashboards/DashboardWithAllConditionalRendering.json @@ -3308,6 +3308,170 @@ } } }, + "panel-37": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "group": "", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 37, + "links": [], + "title": "Hide panel - custom variable equals a (current = ${myCustomVariable})", + "vizConfig": { + "group": "text", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "", + "mode": "markdown" + } + }, + "version": "12.2.0-pre" + } + } + }, + "panel-38": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": { + "labels": "key=$myCustomVariable", + "scenarioId": "random_walk", + "seriesCount": 1 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 38, + "links": [], + "title": "Hide panel - no data (current = ${myCustomVariable})", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.2.0-pre" + } + } + }, "panel-4": { "kind": "Panel", "spec": { @@ -5091,6 +5255,80 @@ }, "title": "Tab - hide - time range <7d" } + }, + { + "kind": "TabsLayoutTab", + "spec": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "operator": "equals", + "value": "a", + "variable": "myCustomVariable" + } + } + ], + "visibility": "hide" + } + }, + "element": { + "kind": "ElementReference", + "name": "panel-37" + }, + "repeat": { + "mode": "variable", + "value": "myCustomVariable" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingData", + "spec": { + "value": false + } + } + ], + "visibility": "hide" + } + }, + "element": { + "kind": "ElementReference", + "name": "panel-38" + }, + "repeat": { + "mode": "variable", + "value": "myCustomVariable" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "Tab - repeated items" + } } ] } @@ -5122,6 +5360,39 @@ "query": "", "skipUrlSync": false } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": false, + "current": { + "text": "All", + "value": "$__all" + }, + "hide": "dontHide", + "includeAll": true, + "multi": false, + "name": "myCustomVariable", + "options": [ + { + "selected": false, + "text": "a", + "value": "a" + }, + { + "selected": false, + "text": "b", + "value": "b" + }, + { + "selected": false, + "text": "c", + "value": "c" + } + ], + "query": "a, b, c", + "skipUrlSync": false + } } ] }, diff --git a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingData.tsx b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingData.tsx index bc1078b7f1b..a7941569a0a 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingData.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingData.tsx @@ -68,22 +68,29 @@ export class ConditionalRenderingData extends SceneObjectBase; } diff --git a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingTimeRangeSize.tsx b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingTimeRangeSize.tsx index a717b68ea79..d22ad02b8f9 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingTimeRangeSize.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingTimeRangeSize.tsx @@ -80,6 +80,10 @@ export class ConditionalRenderingTimeRangeSize extends SceneObjectBase; } diff --git a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingVariable.tsx b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingVariable.tsx index 8f82b1f459f..827f7d9ca39 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingVariable.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingVariable.tsx @@ -20,7 +20,7 @@ import { getLowerTranslatedObjectType } from '../object'; import { ConditionalRenderingConditionWrapper } from './ConditionalRenderingConditionWrapper'; import { ConditionalRenderingConditionsSerializerRegistryItem } from './serializers'; -import { checkGroup, getObjectType } from './utils'; +import { checkGroup, getObject, getObjectType } from './utils'; type VariableConditionValueOperator = '=' | '!=' | '=~' | '!~'; @@ -40,14 +40,6 @@ export class ConditionalRenderingVariable extends SceneObjectBase { - if (v.state.name === this.state.variable) { - this._check(); - } - }, - }); - public constructor(state: ConditionalRenderingVariableState) { super(state); @@ -55,6 +47,20 @@ export class ConditionalRenderingVariable extends SceneObjectBase { + if (v.state.name === this.state.variable) { + this._check(); + } + }, + }); + this.forEachChild((child) => { if (!child.isActive) { this._subs.add(child.activate()); @@ -78,7 +84,13 @@ export class ConditionalRenderingVariable extends SceneObjectBase; } diff --git a/public/app/features/dashboard-scene/conditional-rendering/conditions/utils.ts b/public/app/features/dashboard-scene/conditional-rendering/conditions/utils.ts index af8a4110a11..6ce89e17610 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/conditions/utils.ts +++ b/public/app/features/dashboard-scene/conditional-rendering/conditions/utils.ts @@ -14,6 +14,13 @@ export function getGroup(condition: ConditionalRenderingConditions): Conditional } export function getObject(condition: ConditionalRenderingConditions): SceneObject | undefined { + const group = getGroup(condition); + const groupTarget = group.getTarget(); + + if (groupTarget) { + return groupTarget; + } + return getGroup(condition).parent; } diff --git a/public/app/features/dashboard-scene/conditional-rendering/group/ConditionalRenderingGroup.tsx b/public/app/features/dashboard-scene/conditional-rendering/group/ConditionalRenderingGroup.tsx index 5f0b3319e25..8343bd1914c 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/group/ConditionalRenderingGroup.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/group/ConditionalRenderingGroup.tsx @@ -2,7 +2,14 @@ import { lowerCase } from 'lodash'; import { useMemo } from 'react'; import { t } from '@grafana/i18n'; -import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { + SceneComponentProps, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneObjectRef, + SceneObjectState, +} from '@grafana/scenes'; import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Stack } from '@grafana/ui'; @@ -33,6 +40,7 @@ export class ConditionalRenderingGroup extends SceneObjectBase; public constructor(state: ConditionalRenderingGroupState) { super(state); @@ -52,6 +60,19 @@ export class ConditionalRenderingGroup extends SceneObjectBase condition.forceCheck()); + } + public check() { // Filter out undefined results // Because we negate the result if shouldShow is false, we can use `condition.state.result ?? true` directly below diff --git a/public/app/features/dashboard-scene/conditional-rendering/hooks/useIsConditionallyHidden.tsx b/public/app/features/dashboard-scene/conditional-rendering/hooks/useIsConditionallyHidden.tsx index 2292b63500a..b8b77fd44d6 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/hooks/useIsConditionallyHidden.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/hooks/useIsConditionallyHidden.tsx @@ -1,12 +1,13 @@ import { ReactNode } from 'react'; -import { SceneObject, useSceneObjectState } from '@grafana/scenes'; +import { useSceneObjectState } from '@grafana/scenes'; import { ConditionalRenderingGroup } from '../group/ConditionalRenderingGroup'; import { ConditionalRenderingOverlay } from './ConditionalRenderingOverlay'; let placeholderConditionalRendering: ConditionalRenderingGroup | undefined; + function getPlaceholderConditionalRendering(): ConditionalRenderingGroup { if (!placeholderConditionalRendering) { placeholderConditionalRendering = ConditionalRenderingGroup.createEmpty(); @@ -14,13 +15,10 @@ function getPlaceholderConditionalRendering(): ConditionalRenderingGroup { return placeholderConditionalRendering; } -export function useIsConditionallyHidden(scene: SceneObject): [boolean, string | undefined, ReactNode | null, boolean] { - const conditionalRenderingToRender = - 'conditionalRendering' in scene.state && scene.state.conditionalRendering instanceof ConditionalRenderingGroup - ? scene.state.conditionalRendering - : getPlaceholderConditionalRendering(); - - const { result, renderHidden } = useSceneObjectState(conditionalRenderingToRender, { +export function useIsConditionallyHidden( + conditionalRendering: ConditionalRenderingGroup = getPlaceholderConditionalRendering() +): [boolean, string | undefined, ReactNode | null, boolean] { + const { result, renderHidden } = useSceneObjectState(conditionalRendering, { shouldActivateOrKeepAlive: true, }); diff --git a/public/app/features/dashboard-scene/conditional-rendering/object.ts b/public/app/features/dashboard-scene/conditional-rendering/object.ts index 67fca6942be..f29ad53c231 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/object.ts +++ b/public/app/features/dashboard-scene/conditional-rendering/object.ts @@ -1,7 +1,7 @@ import { capitalize, lowerCase } from 'lodash'; import { t } from '@grafana/i18n'; -import { SceneObject } from '@grafana/scenes'; +import { SceneObject, VizPanel } from '@grafana/scenes'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; import { RowItem } from '../scene/layout-rows/RowItem'; @@ -50,7 +50,7 @@ export function getLowerTranslatedObjectType(type: ObjectsWithConditionalRenderi export function extractObjectType(object: SceneObject | undefined): ObjectsWithConditionalRendering { if (!object) { return 'element'; - } else if (object instanceof AutoGridItem) { + } else if (object instanceof AutoGridItem || object instanceof VizPanel) { return 'panel'; } else if (object instanceof RowItem) { return 'row'; diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx index 5a88e451fb0..3b7b6cf03d2 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx @@ -31,6 +31,7 @@ export interface AutoGridItemState extends SceneObjectState { variableName?: string; isHidden?: boolean; conditionalRendering?: ConditionalRenderingGroup; + repeatedConditionalRendering?: ConditionalRenderingGroup[]; } export class AutoGridItem extends SceneObjectBase implements DashboardLayoutItem { @@ -130,7 +131,21 @@ export class AutoGridItem extends SceneObjectBase implements } } - this.setState({ repeatedPanels }); + let repeatedConditionalRendering: ConditionalRenderingGroup[] | undefined; + + if (this.state.conditionalRendering) { + repeatedConditionalRendering = repeatedPanels.reduce((acc, panel) => { + const conditionalRendering = this.state.conditionalRendering!.clone(); + conditionalRendering.setTarget(panel); + acc.push(conditionalRendering); + + return acc; + }, []); + + this.state.conditionalRendering.setTarget(panelToRepeat); + } + + this.setState({ repeatedPanels, repeatedConditionalRendering }); this._prevRepeatValues = values; } diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx index a41fb80a233..28fd18768d2 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data/'; import { LazyLoader, SceneComponentProps, VizPanel } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; +import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup'; import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useIsConditionallyHidden'; import { useDashboardState } from '../../utils/utils'; import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext'; @@ -17,8 +18,6 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps getIsLazy(preload), [preload]); @@ -29,18 +28,23 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps - isConditionallyHidden && !isEditing && !renderHidden ? null : ( + }) => { + const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay, renderHidden] = + useIsConditionallyHidden(conditionalRendering); + + return isConditionallyHidden && !isEditing && !renderHidden ? null : (
- ) + ); + } ), - [ - conditionalRenderingClass, - conditionalRenderingOverlay, - isLazy, - key, - model.containerRef, - styles, - isConditionallyHidden, - isEditing, - renderHidden, - ] + [model, isLazy, key, styles, isEditing] ); if (soloPanelContext) { @@ -102,10 +97,18 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps - - {repeatedPanels.map((item) => ( + + {repeatedPanels.map((item, idx) => ( ) { const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState(); const isClone = isRepeatCloneOrChildOf(model); const { isEditing } = useDashboardState(model); - const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = - useIsConditionallyHidden(model); + const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden( + model.state.conditionalRendering + ); const { isSelected, onSelect, isSelectable } = useElementSelection(key); const title = useInterpolatedTitle(model); const { rows } = model.getParentLayout().useState(); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx index 0e18ffd7ef0..7de374bc5cf 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx @@ -112,7 +112,10 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co }); if (!isSourceRow) { + rowClone.state.conditionalRendering?.setTarget(rowClone); clonedRows.push(rowClone); + } else { + row.state.conditionalRendering?.setTarget(row); } } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 49c3e0fd0a9..778aad91757 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -292,6 +292,8 @@ export class RowsLayoutManager extends SceneObjectBase i const conditionalRendering = tab.state.conditionalRendering; conditionalRendering?.clearParent(); + // We need to clear the target since we don't want to point the original tab anymore (if it was set) + conditionalRendering?.setTarget(undefined); rows.push( new RowItem({ diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 6ddd916cad2..c9af2bc439c 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -29,7 +29,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug })); const styles = useStyles2(getStyles); const pointerDistance = usePointerDistance(); - const [isConditionallyHidden] = useIsConditionallyHidden(model); + const [isConditionallyHidden] = useIsConditionallyHidden(model.state.conditionalRendering); const isClone = isRepeatCloneOrChildOf(model); const soloPanelContext = useSoloPanelContext(); @@ -116,7 +116,9 @@ interface TabItemLayoutRendererProps { export function TabItemLayoutRenderer({ tab, isEditing }: TabItemLayoutRendererProps) { const { layout, key } = tab.useState(); const styles = useStyles2(getStyles); - const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(tab); + const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden( + tab.state.conditionalRendering + ); return ( i const conditionalRendering = row.state.conditionalRendering; conditionalRendering?.clearParent(); + // We need to clear the target since we don't want to point the original row anymore (if it was set) + conditionalRendering?.setTarget(undefined); tabs.push( new TabItem({ From cffca379997ec73b70a0c9fcfc1a83b83de8f3d0 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 27 Nov 2025 12:30:48 +0000 Subject: [PATCH 138/423] FS: Check session expiration and rotate if needed (#114433) * FS: Check session expiration and rotate if needed * Remove unused return values --- pkg/services/frontend/index.html | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 2eb4d82dce3..0a589f81a49 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -239,6 +239,30 @@ const CHECK_INTERVAL = 1 * 1000; + function getCookie(name) { + const cookies = document.cookie.split(";").map(c => c.trim()); + + for (const cookie of cookies) { + if (cookie.startsWith(name + "=")) { + return cookie.substring(name.length + 1); + } + } + + return null; + } + + function getSessionExpiration() { + const value = getCookie("grafana_session_expiry") || "0"; + const realExpiresSeconds = parseInt(value, 10); + const expiresSeconds = Math.max(realExpiresSeconds - 10, 0); // Rotate 10s before the real expiration + const expiration = new Date(expiresSeconds * 1000); + return expiration; + } + + async function rotateSession() { + await fetch('/api/user/auth-tokens/rotate', { method: 'POST' }); + } + /** * Fetches boot data from the server. If it returns undefined, it should be retried later. * Will return a rejected promise on unrecoverable errors. @@ -295,6 +319,19 @@ function loadBootData() { return new Promise((resolve, reject) => { const attemptFetch = async () => { + try { + const sessionExpiration = getSessionExpiration(); + const now = new Date(); + + // If the session has expired, don't continue trying to fetch boot data + if (now >= sessionExpiration) { + await rotateSession(); + } + } catch (error) { + // Just ignore any errors in session rotation. The user can just log in again. + console.warn("Failed to rotate session", error); + } + try { const bootData = await fetchBootData(); From 4c869a21a45411278ff5becfeada3afb7eae3620 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Thu, 27 Nov 2025 13:35:49 +0100 Subject: [PATCH 139/423] feat(unified): data migration integration tests (#114418) * feat: unified storage migrations integration tests * chore: add comment and adjust db path name * chore: refactor test cases into interface --- pkg/services/sqlstore/sqlstore.go | 12 +- .../migrations/folders_dashboards_test.go | 208 +++++++++++++++++ .../unified/migrations/migrator_test.go | 210 ++++++++++++++++++ pkg/tests/apis/helper.go | 38 +++- pkg/tests/testinfra/testinfra.go | 17 +- 5 files changed, 476 insertions(+), 9 deletions(-) create mode 100644 pkg/storage/unified/migrations/folders_dashboards_test.go create mode 100644 pkg/storage/unified/migrations/migrator_test.go diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index d4842bee884..1fb8d06d313 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -581,10 +581,16 @@ func TestMain(m *testing.M) { // nolint:staticcheck testSQLStore.cfg.IsFeatureToggleEnabled = features.IsEnabledGlobally - if err := testSQLStore.dialect.TruncateDBTables(testSQLStore.GetEngine()); err != nil { - return nil, err + skipTruncate := false + if skip, present := os.LookupEnv("SKIP_DB_TRUNCATE"); present { + skipTruncate = strings.ToLower(skip) == "true" + } + if !skipTruncate { + if err := testSQLStore.dialect.TruncateDBTables(testSQLStore.GetEngine()); err != nil { + return nil, err + } + testSQLStore.engine.ResetSequenceGenerator() } - testSQLStore.engine.ResetSequenceGenerator() if err := testSQLStore.Reset(); err != nil { return nil, err diff --git a/pkg/storage/unified/migrations/folders_dashboards_test.go b/pkg/storage/unified/migrations/folders_dashboards_test.go new file mode 100644 index 00000000000..27b1c0e8ba6 --- /dev/null +++ b/pkg/storage/unified/migrations/folders_dashboards_test.go @@ -0,0 +1,208 @@ +package migrations_test + +import ( + "fmt" + "net/http" + "testing" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// foldersAndDashboardsTestCase tests the "folders-dashboards" ResourceMigration +type foldersAndDashboardsTestCase struct { + parentFolderUID string + childFolderUID string + dashboardUID string + libPanelUID string +} + +// newFoldersAndDashboardsTestCase creates a test case for the compound folders+dashboards migrator +func newFoldersAndDashboardsTestCase() resourceMigratorTestCase { + return &foldersAndDashboardsTestCase{ + parentFolderUID: "parent-folder-uid", + childFolderUID: "child-folder-uid", + dashboardUID: "", // Will be generated during setup + libPanelUID: "", // Will be generated during setup + } +} + +func (tc *foldersAndDashboardsTestCase) name() string { + return "folders-dashboards" +} + +func (tc *foldersAndDashboardsTestCase) resources() []schema.GroupVersionResource { + return []schema.GroupVersionResource{ + { + Group: "folder.grafana.app", + Version: "v1beta1", + Resource: "folders", + }, + { + Group: "dashboard.grafana.app", + Version: "v1beta1", + Resource: "dashboards", + }, + } +} + +func (tc *foldersAndDashboardsTestCase) setup(t *testing.T, helper *apis.K8sTestHelper) { + t.Helper() + + // Create parent folder + parent := createTestFolder(t, helper, tc.parentFolderUID, "parent-folder", "") + + // Create child folder (nested under parent) + child := createTestFolder(t, helper, tc.childFolderUID, "child-folder", parent.UID) + + // Create library panel in child folder + tc.libPanelUID = createTestLibraryPanel(t, helper, "Test Library Panel", child.UID) + + // Create dashboard with library panel in child folder + tc.dashboardUID = createTestDashboardWithLibraryPanel(t, helper, "dashboard-with-library-panel", + tc.libPanelUID, "Test LP in dashboard", child.UID) +} + +func (tc *foldersAndDashboardsTestCase) verify(t *testing.T, helper *apis.K8sTestHelper, shouldExist bool) { + t.Helper() + + // Build maps of UIDs by resource type + folderUIDs := []string{tc.parentFolderUID, tc.childFolderUID} + dashboardUIDs := []string{tc.dashboardUID} + + expectedFolderCount := 0 + if shouldExist { + expectedFolderCount = len(folderUIDs) + } + orgID := helper.Org1.OrgID + namespace := authlib.OrgNamespaceFormatter(orgID) + + // Verify folders + folderCli := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: namespace, + GVR: schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v1beta1", + Resource: "folders", + }, + }) + verifyResourceCount(t, folderCli, expectedFolderCount) + for _, uid := range folderUIDs { + verifyResource(t, folderCli, uid, shouldExist) + } + + // Verify dashboards + expectedDashboardCount := 0 + if shouldExist { + expectedDashboardCount = len(dashboardUIDs) + } + dashboardCli := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: namespace, + GVR: schema.GroupVersionResource{ + Group: "dashboard.grafana.app", + Version: "v1beta1", + Resource: "dashboards", + }, + }) + verifyResourceCount(t, dashboardCli, expectedDashboardCount) + for _, uid := range dashboardUIDs { + verifyResource(t, dashboardCli, uid, shouldExist) + } +} + +// createTestFolder creates a folder with specified UID and optional parent +func createTestFolder(t *testing.T, helper *apis.K8sTestHelper, uid, title, parentUID string) *folder.Folder { + t.Helper() + + payload := fmt.Sprintf(`{ + "title": "%s", + "uid": "%s"`, title, uid) + + if parentUID != "" { + payload += fmt.Sprintf(`, + "parentUid": "%s"`, parentUID) + } + + payload += "}" + + folderCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), + }, &folder.Folder{}) + + require.NotNil(t, folderCreate.Result) + require.Equal(t, uid, folderCreate.Result.UID) + + return folderCreate.Result +} + +// createTestLibraryPanel creates a library panel in a folder +func createTestLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, name, folderUID string) string { + t.Helper() + + libPanelPayload := fmt.Sprintf(`{ + "kind": 1, + "name": "%s", + "folderUid": "%s", + "model": { + "type": "text", + "title": "%s" + } + }`, name, folderUID, name) + + libCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/library-elements", + Body: []byte(libPanelPayload), + }, &map[string]interface{}{}) + + require.NotNil(t, libCreate.Response) + require.Equal(t, http.StatusOK, libCreate.Response.StatusCode) + + libPanelUID := (*libCreate.Result)["result"].(map[string]interface{})["uid"].(string) + require.NotEmpty(t, libPanelUID) + + return libPanelUID +} + +// createTestDashboardWithLibraryPanel creates a dashboard that uses a library panel +func createTestDashboardWithLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, dashTitle, libPanelUID, libPanelName, folderUID string) string { + t.Helper() + + dashPayload := fmt.Sprintf(`{ + "dashboard": { + "title": "%s", + "panels": [{ + "id": 1, + "libraryPanel": { + "uid": "%s", + "name": "%s" + } + }] + }, + "folderUid": "%s", + "overwrite": false + }`, dashTitle, libPanelUID, libPanelName, folderUID) + + dashCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/dashboards/db", + Body: []byte(dashPayload), + }, &map[string]interface{}{}) + + require.NotNil(t, dashCreate.Response) + require.Equal(t, http.StatusOK, dashCreate.Response.StatusCode) + + dashUID := (*dashCreate.Result)["uid"].(string) + require.NotEmpty(t, dashUID) + return dashUID +} diff --git a/pkg/storage/unified/migrations/migrator_test.go b/pkg/storage/unified/migrations/migrator_test.go new file mode 100644 index 00000000000..e75ed26b9e3 --- /dev/null +++ b/pkg/storage/unified/migrations/migrator_test.go @@ -0,0 +1,210 @@ +package migrations_test + +import ( + "context" + "fmt" + "os" + "testing" + + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" + "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/tests/testsuite" + "github.com/grafana/grafana/pkg/util/testutil" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +// resourceMigratorTestCase defines the interface for testing a resource migrator. +type resourceMigratorTestCase interface { + // name returns the test case name + name() string + // resources returns the GVRs that this migrator handles + resources() []schema.GroupVersionResource + // setup creates test resources in legacy storage (Mode0) + setup(t *testing.T, helper *apis.K8sTestHelper) + // verify checks that resources exist (or don't exist) in unified storage + verify(t *testing.T, helper *apis.K8sTestHelper, shouldExist bool) +} + +// TestIntegrationMigrations verifies that legacy storage data is correctly migrated to unified storage. +// The test follows a three-step process: +// Step 1: inserts legacy data (migration disabled at startup) +// Step 2: verifies that the data is not in unified storage +// Step 3: migration runs at startup, and the test verifies that the data is in unified storage +func TestIntegrationMigrations(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + migrationTestCases := []resourceMigratorTestCase{ + newFoldersAndDashboardsTestCase(), + } + + runMigrationTestSuite(t, migrationTestCases) +} + +// runMigrationTestSuite executes the migration test suite for the given test cases +func runMigrationTestSuite(t *testing.T, testCases []resourceMigratorTestCase) { + if db.IsTestDbSQLite() { + // Share the same SQLite DB file between steps + tmpDir := t.TempDir() + dbPath := tmpDir + "/shared-migration-test-suite.db" + + oldVal := os.Getenv("SQLITE_TEST_DB") + require.NoError(t, os.Setenv("SQLITE_TEST_DB", dbPath)) + t.Cleanup(func() { + if oldVal == "" { + _ = os.Unsetenv("SQLITE_TEST_DB") + } else { + _ = os.Setenv("SQLITE_TEST_DB", oldVal) + } + }) + t.Logf("Using shared database path: %s", dbPath) + } + + // Store UIDs created by each test case + type testCaseState struct { + tc resourceMigratorTestCase + } + testStates := make([]testCaseState, len(testCases)) + for i, tc := range testCases { + testStates[i].tc = tc + } + + // reuse org users throughout the tests + var org1 *apis.OrgUsers + var orgB *apis.OrgUsers + t.Run("Step 1: Create data in legacy", func(t *testing.T) { + // Enforce Mode0 for all migrated resources + unifiedConfig := make(map[string]setting.UnifiedStorageConfig) + for _, tc := range testCases { + for _, gvr := range tc.resources() { + resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group) + unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{ + DualWriterMode: grafanarest.Mode0, + } + } + } + + // Set up test environment with Mode0 (writes only to legacy) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + DisableDataMigrations: true, + DisableDBCleanup: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: unifiedConfig, + }) + t.Cleanup(helper.Shutdown) + org1 = &helper.Org1 + orgB = &helper.OrgB + + for i := range testStates { + state := &testStates[i] + t.Run(state.tc.name(), func(t *testing.T) { + state.tc.setup(t, helper) + // Verify resources were created in legacy storage + state.tc.verify(t, helper, true) + }) + } + }) + + // Set SKIP_DB_TRUNCATE to not truncate the data created in Step 1 + oldSkipTruncate := os.Getenv("SKIP_DB_TRUNCATE") + require.NoError(t, os.Setenv("SKIP_DB_TRUNCATE", "true")) + t.Cleanup(func() { + if oldSkipTruncate == "" { + _ = os.Unsetenv("SKIP_DB_TRUNCATE") + } else { + _ = os.Setenv("SKIP_DB_TRUNCATE", oldSkipTruncate) + } + }) + + t.Run("Step 2: Verify data is NOT in unified storage before the migration", func(t *testing.T) { + // Build unified storage config for Mode5 + unifiedConfig := make(map[string]setting.UnifiedStorageConfig) + for _, tc := range testCases { + for _, gvr := range tc.resources() { + resourceKey := fmt.Sprintf("%s.%s", gvr.Resource, gvr.Group) + unifiedConfig[resourceKey] = setting.UnifiedStorageConfig{ + DualWriterMode: grafanarest.Mode5, + } + } + } + + helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{ + GrafanaOpts: testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + DisableDataMigrations: true, + DisableDBCleanup: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: unifiedConfig, + }, + Org1Users: org1, + OrgBUsers: orgB, + }) + t.Cleanup(helper.Shutdown) + + for _, state := range testStates { + t.Run(state.tc.name(), func(t *testing.T) { + // Verify resources don't exist in unified storage yet + state.tc.verify(t, helper, false) + }) + } + }) + + t.Run("Step 3: verify data is migrated to unified storage", func(t *testing.T) { + // Migrations will run automatically at startup and mode 5 is enforced by the config + helper := apis.NewK8sTestHelperWithOpts(t, apis.K8sTestHelperOpts{ + GrafanaOpts: testinfra.GrafanaOpts{ + // EnableLog: true, + AppModeProduction: true, + DisableAnonymous: true, + DisableDataMigrations: false, // Run migrations at startup + APIServerStorageType: "unified", + }, + Org1Users: org1, + OrgBUsers: orgB, + }) + t.Cleanup(helper.Shutdown) + + for _, state := range testStates { + t.Run(state.tc.name(), func(t *testing.T) { + // Verify resources now exist in unified storage after migration + state.tc.verify(t, helper, true) + }) + } + }) +} + +// verifyResourceCount verifies that the expected number of resources exist in K8s storage +func verifyResourceCount(t *testing.T, client *apis.K8sResourceClient, expectedCount int) { + t.Helper() + + l, err := client.Resource.List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + + resources, err := meta.ExtractList(l) + require.NoError(t, err) + require.Equal(t, expectedCount, len(resources)) +} + +// verifyResource verifies that a resource with the given UID exists in K8s storage +func verifyResource(t *testing.T, client *apis.K8sResourceClient, uid string, shouldExist bool) { + t.Helper() + + _, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + if shouldExist { + require.NoError(t, err) + } else { + require.Error(t, err) + } +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index b69b5e9e1d1..35c349184bf 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -93,7 +93,18 @@ type K8sTestHelper struct { userSvc user.Service } +type K8sTestHelperOpts struct { + testinfra.GrafanaOpts + // If provided, these users will be used instead of creating new ones + Org1Users *OrgUsers + OrgBUsers *OrgUsers +} + func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { + return NewK8sTestHelperWithOpts(t, K8sTestHelperOpts{GrafanaOpts: opts}) +} + +func NewK8sTestHelperWithOpts(t *testing.T, opts K8sTestHelperOpts) *K8sTestHelper { t.Helper() // Use GRPC server when not configured @@ -111,9 +122,12 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { path = opts.DirPath ) if opts.Dir == "" && opts.DirPath == "" { - dir, path = testinfra.CreateGrafDir(t, opts) + dir, path = testinfra.CreateGrafDir(t, opts.GrafanaOpts) + } + listenerAddress, env, testDB := testinfra.StartGrafanaEnvWithDB(t, dir, path) + if !opts.DisableDBCleanup { + t.Cleanup(testDB.Cleanup) } - listenerAddress, env := testinfra.StartGrafanaEnv(t, dir, path) c := &K8sTestHelper{ env: *env, @@ -143,8 +157,24 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { _ = c.CreateOrg(Org1) _ = c.CreateOrg(Org2) - c.Org1 = c.createTestUsers(Org1) - c.OrgB = c.createTestUsers(Org2) + if opts.Org1Users != nil { + c.Org1 = *opts.Org1Users + c.Org1.Admin.baseURL = listenerAddress + c.Org1.Editor.baseURL = listenerAddress + c.Org1.Viewer.baseURL = listenerAddress + c.Org1.None.baseURL = listenerAddress + } else { + c.Org1 = c.createTestUsers(Org1) + } + if opts.OrgBUsers != nil { + c.OrgB = *opts.OrgBUsers + c.OrgB.Admin.baseURL = listenerAddress + c.OrgB.Editor.baseURL = listenerAddress + c.OrgB.Viewer.baseURL = listenerAddress + c.OrgB.None.baseURL = listenerAddress + } else { + c.OrgB = c.createTestUsers(Org2) + } c.loadAPIGroups() diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index d95f108a231..25b2d4b177b 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -49,6 +49,12 @@ func StartGrafana(t *testing.T, grafDir, cfgPath string) (string, db.DB) { } func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.TestEnv) { + addr, env, testDB := StartGrafanaEnvWithDB(t, grafDir, cfgPath) + t.Cleanup(testDB.Cleanup) + return addr, env +} + +func StartGrafanaEnvWithDB(t *testing.T, grafDir, cfgPath string) (string, *server.TestEnv, *sqlutil.TestDB) { t.Helper() ctx := context.Background() @@ -93,7 +99,6 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes // Use proper database type based on the environment variable GRAFANA_TEST_DB in tests testDB, err := sqlutil.GetTestDB(sqlutil.GetTestDBType()) require.NoError(t, err) - t.Cleanup(testDB.Cleanup) dbCfg := cfg.Raw.Section("database") dbCfg.Key("type").SetValue(testDB.DriverName) @@ -169,7 +174,7 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes t.Logf("Grafana is listening on %s", addr) - return addr, env + return addr, env, testDB } // CreateGrafDir creates the Grafana directory. @@ -538,6 +543,12 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = section.NewKey("max_page_size_bytes", fmt.Sprintf("%d", opts.UnifiedStorageMaxPageSizeBytes)) require.NoError(t, err) } + if opts.DisableDataMigrations { + section, err := getOrCreateSection("unified_storage") + require.NoError(t, err) + _, err = section.NewKey("disable_data_migrations", "true") + require.NoError(t, err) + } if opts.PermittedProvisioningPaths != "" { _, err = pathsSect.NewKey("permitted_provisioning_paths", opts.PermittedProvisioningPaths) require.NoError(t, err) @@ -637,6 +648,8 @@ type GrafanaOpts struct { EnableSCIM bool APIServerRuntimeConfig string DisableControllers bool + DisableDBCleanup bool + DisableDataMigrations bool SecretsManagerEnableDBMigrations bool // Allow creating grafana dir beforehand From f872fd7f2f58c8059556de43076f9c038cf5e7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Thu, 27 Nov 2025 14:06:27 +0100 Subject: [PATCH 140/423] Chore: Update `body-parser` to v2.2.1 (#114539) --- yarn.lock | 74 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/yarn.lock b/yarn.lock index 91be1a1621f..abe69dda8b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13051,19 +13051,19 @@ __metadata: linkType: hard "body-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "body-parser@npm:2.2.0" + version: 2.2.1 + resolution: "body-parser@npm:2.2.1" dependencies: bytes: "npm:^3.1.2" content-type: "npm:^1.0.5" - debug: "npm:^4.4.0" + debug: "npm:^4.4.3" http-errors: "npm:^2.0.0" - iconv-lite: "npm:^0.6.3" + iconv-lite: "npm:^0.7.0" on-finished: "npm:^2.4.1" qs: "npm:^6.14.0" - raw-body: "npm:^3.0.0" - type-is: "npm:^2.0.0" - checksum: 10/e9d844b036bd15970df00a16f373c7ed28e1ef870974a0a1d4d6ef60d70e01087cc20a0dbb2081c49a88e3c08ce1d87caf1e2898c615dffa193f63e8faa8a84e + raw-body: "npm:^3.0.1" + type-is: "npm:^2.0.1" + checksum: 10/cab162d62da03058dec8ff4ebf6bf22922b46bf32bd85e59e7fca78d4962aec97b7a7f913dbc3204bb4aa058df03284463ca4c5cc920bf783e591b8de049ffe0 languageName: node linkType: hard @@ -13280,7 +13280,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.1.2": +"bytes@npm:3.1.2, bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388 @@ -15642,15 +15642,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad languageName: node linkType: hard @@ -15899,7 +15899,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10/c0c8ff36079ce5ada64f46cc9d6fd47ebcf38241105b6e0c98f412e8ad91f084bcf906ff644cc3a4bd876ca27a62accb8b0fff72ea6ed1a414b89d8506f4a5ca @@ -20092,7 +20092,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -20105,6 +20105,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^2.0.0, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10/9fe31bc0edf36566c87048aed1d3d0cbe03552564adc3541626a0613f542d753fbcb13bdfcec0a3a530dbe1714bb566c89d46244616b66bddd26ac413b06a207 + languageName: node + linkType: hard + "http-parser-js@npm:>=0.5.1": version: 0.5.6 resolution: "http-parser-js@npm:0.5.6" @@ -20405,7 +20418,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.7.0": +"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0": version: 0.7.0 resolution: "iconv-lite@npm:0.7.0" dependencies: @@ -27881,15 +27894,15 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:^3.0.0": - version: 3.0.0 - resolution: "raw-body@npm:3.0.0" +"raw-body@npm:^3.0.0, raw-body@npm:^3.0.1": + version: 3.0.2 + resolution: "raw-body@npm:3.0.2" dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.6.3" - unpipe: "npm:1.0.0" - checksum: 10/2443429bbb2f9ae5c50d3d2a6c342533dfbde6b3173740b70fa0302b30914ff400c6d31a46b3ceacbe7d0925dc07d4413928278b494b04a65736fc17ca33e30c + bytes: "npm:~3.1.2" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.7.0" + unpipe: "npm:~1.0.0" + checksum: 10/4168c82157bd69175d5bd960e59b74e253e237b358213694946a427a6f750a18b8e150f036fed3421b3e83294b071a4e2bb01037a79ccacdac05360c63d3ebba languageName: node linkType: hard @@ -30302,7 +30315,7 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10/fde1630422502fbbc19e6844346778f99d449986b2f9cdcceb8326730d2f3d9964dbcb03c02aaadaefffecd0f2c063315ebea8b3ad895914bf1afc1747fc172e @@ -31147,13 +31160,20 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1, statuses@npm:^2.0.1": +"statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 10/18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb languageName: node linkType: hard +"statuses@npm:^2.0.1, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 + languageName: node + linkType: hard + "statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -32341,7 +32361,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -32794,7 +32814,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^2.0.0, type-is@npm:^2.0.1": +"type-is@npm:^2.0.1": version: 2.0.1 resolution: "type-is@npm:2.0.1" dependencies: From 8515bcc6b077c067f797a03d638b4d521aa53624 Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 27 Nov 2025 14:57:54 +0100 Subject: [PATCH 141/423] Alerting: Use data source headers when remote writing (#114528) --- .../fakes/fake_datasource_service.go | 5 +- .../ngalert/writer/datasourcewriter.go | 12 ++++ .../ngalert/writer/datasourcewriter_test.go | 64 ++++++++++++++++++- pkg/services/ngalert/writer/testing.go | 3 +- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/pkg/services/datasources/fakes/fake_datasource_service.go b/pkg/services/datasources/fakes/fake_datasource_service.go index 19e7bbb43a5..7637657f7cf 100644 --- a/pkg/services/datasources/fakes/fake_datasource_service.go +++ b/pkg/services/datasources/fakes/fake_datasource_service.go @@ -15,6 +15,9 @@ type FakeDataSourceService struct { lastID int64 DataSources []*datasources.DataSource SimulatePluginFailure bool + + // UID -> Headers + DataSourceHeaders map[string]http.Header } var _ datasources.DataSourceService = &FakeDataSourceService{} @@ -152,5 +155,5 @@ func (s *FakeDataSourceService) DecryptedPassword(ctx context.Context, ds *datas } func (s *FakeDataSourceService) CustomHeaders(ctx context.Context, ds *datasources.DataSource) (http.Header, error) { - return nil, nil + return s.DataSourceHeaders[ds.UID], nil } diff --git a/pkg/services/ngalert/writer/datasourcewriter.go b/pkg/services/ngalert/writer/datasourcewriter.go index 19af0db0a5b..cc7036974e7 100644 --- a/pkg/services/ngalert/writer/datasourcewriter.go +++ b/pkg/services/ngalert/writer/datasourcewriter.go @@ -205,11 +205,23 @@ func (w *DatasourceWriter) makeWriter(ctx context.Context, orgID int64, dsUID st return nil, err } + // We need to add the writer headers (valid for any data source) and any data-source-specific headers. headers := make(http.Header) for k, v := range w.cfg.CustomHeaders { headers.Add(k, v) } + dsHeaders, err := w.datasources.CustomHeaders(ctx, ds) + if err != nil { + return nil, fmt.Errorf("failed to get headers for data source: %w", err) + } + + for k, values := range dsHeaders { + for _, v := range values { + headers.Add(k, v) + } + } + var backend backendType if dsUID == string(grafanaCloudPromType) { backend = grafanaCloudPromType diff --git a/pkg/services/ngalert/writer/datasourcewriter_test.go b/pkg/services/ngalert/writer/datasourcewriter_test.go index f06d2d95de4..349d8b9191a 100644 --- a/pkg/services/ngalert/writer/datasourcewriter_test.go +++ b/pkg/services/ngalert/writer/datasourcewriter_test.go @@ -56,13 +56,14 @@ func (m *mockHTTPClientProvider) New(options ...sdkhttpclient.Options) (*http.Cl type testDataSources struct { dsfakes.FakeDataSourceService - prom1, prom2, prom3 *TestRemoteWriteTarget + prom1, prom2, prom3, prom4 *TestRemoteWriteTarget } func (t *testDataSources) Reset() { t.prom1.Reset() t.prom2.Reset() t.prom3.Reset() + t.prom4.Reset() } func setupDataSources(t *testing.T) *testDataSources { @@ -70,7 +71,9 @@ func setupDataSources(t *testing.T) *testDataSources { prom1: NewTestRemoteWriteTarget(t), prom2: NewTestRemoteWriteTarget(t), prom3: NewTestRemoteWriteTarget(t), + prom4: NewTestRemoteWriteTarget(t), } + res.DataSourceHeaders = make(map[string]http.Header) t.Cleanup(func() { res.prom1.Close() @@ -81,6 +84,9 @@ func setupDataSources(t *testing.T) *testDataSources { t.Cleanup(func() { res.prom3.Close() }) + t.Cleanup(func() { + res.prom4.Close() + }) p1, _ := res.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ Name: "prom-1", @@ -107,7 +113,7 @@ func setupDataSources(t *testing.T) *testDataSources { Type: datasources.DS_LOKI, }) - // Add a third Prometheus datasource that uses PDC + // Add a third Prometheus datasource that uses PDC. p3, _ := res.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ Name: "prom-3", UID: "prom-3", @@ -123,6 +129,21 @@ func setupDataSources(t *testing.T) *testDataSources { require.True(t, p3.IsSecureSocksDSProxyEnabled()) + // Add a fourth Prometheus datasource with headers in the JSON config. + p4, _ := res.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + Name: "prom-4", + UID: "prom-4", + Type: datasources.DS_PROMETHEUS, + JsonData: simplejson.MustJson([]byte(`{"prometheusType":"Prometheus"}`)), + }) + p4.URL = res.prom4.srv.URL + res.prom4.ExpectedPath = "/api/v1/write" + res.DataSourceHeaders["prom-4"] = http.Header{ + "X-Scope-OrgID": []string{"test-user"}, + "X-Test-Header": []string{"test-value"}, + "X-Double-Header": []string{"one", "two", "three"}, + } + return res } @@ -204,6 +225,45 @@ func TestDatasourceWriter(t *testing.T) { assert.Equal(t, headers[header2], testDS.prom1.LastHeaders.Get(header2)) }) + t.Run("when data source headers are configured, they are passed to the request", func(t *testing.T) { + testDS.Reset() + overwrittenHeader := "X-Test-Header" + cHeaders := map[string]string{ + "X-Custom-Header": "test-value", + "X-Another-Header": "another-value", + overwrittenHeader: "overwritten", // Data source headers should be overwritten by custom headers. + } + + cfg = DatasourceWriterConfig{ + Timeout: time.Second * 5, + DefaultDatasourceUID: "prom-1", + CustomHeaders: cHeaders, + } + writer = NewDatasourceWriter(cfg, testDS, httpclient.NewProvider(), pluginContextProvider, clock.New(), log.New("test"), met) + + uid := "prom-4" + err := writer.WriteDatasource(context.Background(), uid, "metric", time.Now(), frames, 1, map[string]string{}) + require.NoError(t, err) + + dsHeaders := testDS.DataSourceHeaders[uid] + require.Len(t, dsHeaders, 3) + + // We're confirming we have a data source header with the same name but different value. + // This one should not be sent in the request. + require.NotEmpty(t, dsHeaders[overwrittenHeader]) + require.NotEqual(t, dsHeaders[overwrittenHeader], cHeaders[overwrittenHeader]) + + // All headers (except for the one that was overwritten) should have been used. + for k, vv := range dsHeaders { + if k != overwrittenHeader { + assert.Equal(t, vv, testDS.prom4.LastHeaders.Values(k)) + } + } + for k, v := range cHeaders { + assert.Equal(t, v, testDS.prom4.LastHeaders.Get(k)) + } + }) + t.Run("when PDC is enabled proxy options are passed to HTTP client provider", func(t *testing.T) { testDS.Reset() diff --git a/pkg/services/ngalert/writer/testing.go b/pkg/services/ngalert/writer/testing.go index 91b1bdb6b10..8b764657d6e 100644 --- a/pkg/services/ngalert/writer/testing.go +++ b/pkg/services/ngalert/writer/testing.go @@ -1,6 +1,7 @@ package writer import ( + "fmt" "io" "net/http" "net/http/httptest" @@ -37,7 +38,7 @@ func NewTestRemoteWriteTarget(t *testing.T) *TestRemoteWriteTarget { handler := func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != target.ExpectedPath { - require.Fail(t, "Received unexpected request for endpoint %s", r.URL.Path) + require.Fail(t, fmt.Sprintf("Received unexpected request for endpoint %s", r.URL.Path)) } target.mtx.Lock() From 80fc87339a6f93f7348a6abc2b7210540bbb1f3d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 27 Nov 2025 15:11:34 +0100 Subject: [PATCH 142/423] Zanzana: Role binding hooks (#114470) * Zanzana: Role bindings hooks WIP * Empty hooks for role bindings * implement hooks for role bindings * add tests * apply review suggestions --- pkg/registry/apis/iam/register.go | 6 + pkg/registry/apis/iam/role_binding_hooks.go | 302 ++++++++++++ .../apis/iam/role_binding_hooks_test.go | 448 ++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 pkg/registry/apis/iam/role_binding_hooks.go create mode 100644 pkg/registry/apis/iam/role_binding_hooks_test.go diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 99c9dda7d8d..2417c84aed8 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -346,6 +346,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge if err != nil { return err } + if enableZanzanaSync { + b.logger.Info("Enabling hooks for RoleBinding to sync to Zanzana") + roleBindingStore.AfterCreate = b.AfterRoleBindingCreate + roleBindingStore.AfterDelete = b.AfterRoleBindingDelete + roleBindingStore.BeginUpdate = b.BeginRoleBindingUpdate + } storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore } //nolint:staticcheck // not yet migrated to OpenFeature diff --git a/pkg/registry/apis/iam/role_binding_hooks.go b/pkg/registry/apis/iam/role_binding_hooks.go new file mode 100644 index 00000000000..c88c46976eb --- /dev/null +++ b/pkg/registry/apis/iam/role_binding_hooks.go @@ -0,0 +1,302 @@ +package iam + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +const resourceType = "rolebinding" + +// AfterRoleBindingCreate is a post-create hook that writes the role binding to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + rb, ok := obj.(*iamv0.RoleBinding) + if !ok { + b.logger.Error("failed to convert object to RoleBinding type", "object", obj) + return + } + + operation := "create" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(rb *iamv0.RoleBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("writing role binding to zanzana", + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs)) + for _, roleRef := range rb.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: string(rb.Spec.Subject.Kind), + SubjectName: rb.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + if len(operations) == 0 { + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rb.Namespace, + Operations: operations, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to write role binding to zanzana", + "err", err, + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + } + }(rb.DeepCopy()) // Pass a copy of the object +} + +// AfterRoleBindingDelete is a post-delete hook that removes the role binding from Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + rb, ok := obj.(*iamv0.RoleBinding) + if !ok { + b.logger.Error("failed to convert object to RoleBinding type", "object", obj) + return + } + + operation := "delete" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(rb *iamv0.RoleBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("deleting role binding from zanzana", + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs)) + for _, roleRef := range rb.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: string(rb.Spec.Subject.Kind), + SubjectName: rb.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + if len(operations) == 0 { + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rb.Namespace, + Operations: operations, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to delete role binding from zanzana", + "err", err, + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + } + }(rb.DeepCopy()) // Pass a copy of the object +} + +// BeginRoleBindingUpdate is a pre-update hook that prepares zanzana updates. +// It performs the zanzana write after K8s update succeeds. +func (b *IdentityAccessManagementAPIBuilder) BeginRoleBindingUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + + // Extract role bindings from both old and new objects + oldRB, ok := oldObj.(*iamv0.RoleBinding) + if !ok { + return nil, nil + } + + newRB, ok := obj.(*iamv0.RoleBinding) + if !ok { + return nil, nil + } + + if oldRB.Spec.Subject.Name == newRB.Spec.Subject.Name && roleRefsEqual(oldRB.Spec.RoleRefs, newRB.Spec.RoleRefs) { + return nil, nil // No changes to the role binding + } + + if newRB.Spec.Subject.Name == "" { + b.logger.Error("invalid role binding", + "namespace", newRB.Namespace, + "name", newRB.Name, + "subject", newRB.Spec.Subject.Name, + "roleRefs", newRB.Spec.RoleRefs, + ) + return nil, nil + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, "update").Observe(time.Since(wait).Seconds()) + + go func() { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, "update", status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("updating role binding in zanzana", + "namespace", newRB.Namespace, + "name", newRB.Name, + "oldSubject", oldRB.Spec.Subject.Name, + "newSubject", newRB.Spec.Subject.Name, + "oldRoleRefs", oldRB.Spec.RoleRefs, + "newRoleRefs", newRB.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(oldRB.Spec.RoleRefs)) + for _, roleRef := range oldRB.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: string(oldRB.Spec.Subject.Kind), + SubjectName: oldRB.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + for _, roleRef := range newRB.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: string(newRB.Spec.Subject.Kind), + SubjectName: newRB.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + // Only make the request if there are deletes or writes + if len(operations) == 0 { + b.logger.Debug("no role bindings to update in zanzana", "namespace", newRB.Namespace, "name", newRB.Name) + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: newRB.Namespace, + Operations: operations, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to update role binding in zanzana", + "err", err, + "namespace", newRB.Namespace, + "name", newRB.Name, + ) + } + }() + }, nil +} + +func roleRefsEqual(oldRoleRefs, newRoleRefs []iamv0.RoleBindingspecRoleRef) bool { + if len(oldRoleRefs) != len(newRoleRefs) { + return false + } + + oldRoleRefsMap := make(map[string]string) + for _, roleRef := range oldRoleRefs { + oldRoleRefsMap[roleRef.Name] = string(roleRef.Kind) + } + for _, roleRef := range newRoleRefs { + refKind, ok := oldRoleRefsMap[roleRef.Name] + if !ok { + return false + } + if refKind != string(roleRef.Kind) { + return false + } + } + return true +} diff --git a/pkg/registry/apis/iam/role_binding_hooks_test.go b/pkg/registry/apis/iam/role_binding_hooks_test.go new file mode 100644 index 00000000000..dd9646282fe --- /dev/null +++ b/pkg/registry/apis/iam/role_binding_hooks_test.go @@ -0,0 +1,448 @@ +package iam + +import ( + "context" + "slices" + "sync" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +func TestAfterRoleBindingCreate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should create zanzana entry for role binding", func(t *testing.T) { + wg.Add(1) + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + testRoleBinding := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) + require.Equal(t, "org-1", req.Namespace) + + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-1", + }, + }, + } + + actualCreate := req.Operations[0].Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding + expectedCreate := expectedOperation.Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding + + require.Equal(t, expectedCreate.SubjectKind, actualCreate.SubjectKind) + require.Equal(t, expectedCreate.SubjectName, actualCreate.SubjectName) + require.Equal(t, expectedCreate.RoleKind, actualCreate.RoleKind) + require.Equal(t, expectedCreate.RoleName, actualCreate.RoleName) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBinding} + b.AfterRoleBindingCreate(&roleBinding, nil) + wg.Wait() + }) + + t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-3", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-3", + }, + }, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterRoleBindingCreate(&roleBinding, nil) + }) +} + +func TestBeginRoleBindingUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should update zanzana entry when role binding changed", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-foo", + }, + { + Kind: "role", + Name: "role-2", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-bar", + }, + }, + }, + } + + testRoleBindingUpdate := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 3) + + // Should write new binding and delete old one + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-foo", + }, + }, + })) + + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-bar", + }, + }, + })) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingUpdate} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should return nil finish func when bindings are identical", func(t *testing.T) { + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + writeCalled := false + testNoWriteOnNoChange := func(ctx context.Context, req *v1.MutateRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when bindings are identical") + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnNoChange} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when bindings are identical + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when bindings are identical") + }) + + t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) { + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "", + Name: "", // Empty name - should cause early return + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + writeCalled := false + testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.MutateRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when new binding has empty subject name") + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnInvalidBinding} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name") + }) +} + +func TestAfterRoleBindingDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should delete zanzana entry for team binding with member permission", func(t *testing.T) { + wg.Add(1) + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + { + Kind: "role", + Name: "role-2", + }, + }, + }, + } + + testRoleBindingDelete := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-1", + }, + }, + })) + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-2", + }, + }, + })) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingDelete} + b.AfterRoleBindingDelete(&roleBinding, nil) + wg.Wait() + }) + + t.Run("should not delete from zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-3", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-3", + }, + }, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterRoleBindingDelete(&roleBinding, nil) + }) +} + +func containsOperation(operations []*v1.MutateOperation, operation *v1.MutateOperation) bool { + return slices.ContainsFunc(operations, func(o *v1.MutateOperation) bool { + switch operation.Operation.(type) { + case *v1.MutateOperation_DeleteRoleBinding: + deleteOperation := operation.Operation.(*v1.MutateOperation_DeleteRoleBinding) + deleteO, ok := o.Operation.(*v1.MutateOperation_DeleteRoleBinding) + if !ok { + return false + } + return deleteO.DeleteRoleBinding.SubjectKind == deleteOperation.DeleteRoleBinding.SubjectKind && + deleteO.DeleteRoleBinding.SubjectName == deleteOperation.DeleteRoleBinding.SubjectName && + deleteO.DeleteRoleBinding.RoleKind == deleteOperation.DeleteRoleBinding.RoleKind && + deleteO.DeleteRoleBinding.RoleName == deleteOperation.DeleteRoleBinding.RoleName + case *v1.MutateOperation_CreateRoleBinding: + createOperation := operation.Operation.(*v1.MutateOperation_CreateRoleBinding) + createO, ok := o.Operation.(*v1.MutateOperation_CreateRoleBinding) + if !ok { + return false + } + return createO.CreateRoleBinding.SubjectKind == createOperation.CreateRoleBinding.SubjectKind && + createO.CreateRoleBinding.SubjectName == createOperation.CreateRoleBinding.SubjectName && + createO.CreateRoleBinding.RoleKind == createOperation.CreateRoleBinding.RoleKind && + createO.CreateRoleBinding.RoleName == createOperation.CreateRoleBinding.RoleName + } + return false + }) +} From 8e73cc2f70609fb99b6fe65104f742666fc260e8 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Thu, 27 Nov 2025 15:14:42 +0100 Subject: [PATCH 143/423] Dashboards: Cover the Switch variable in schema transformations - part 1. (#114293) fix: cover the switch variable when transforming betwen v1 and v2 schemas --- .../src/schema/dashboard/v2_examples.ts | 13 +++++ .../transformSaveModelSchemaV2ToScene.test.ts | 12 ++++- .../api/ResponseTransformers.test.ts | 45 +++++++++++++++++ .../dashboard/api/ResponseTransformers.ts | 49 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 651d858e799..649546e17e1 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -490,5 +490,18 @@ export const handyTestingSchema: Spec = { allowCustomValue: true, }, }, + { + kind: 'SwitchVariable', + spec: { + name: 'switchVar', + label: 'Switch Variable', + description: 'A switch variable', + current: 'false', + enabledValue: 'true', + disabledValue: 'false', + hide: 'dontHide', + skipUrlSync: false, + }, + }, ], }; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 87391044813..f054a9f7cab 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -14,6 +14,7 @@ import { AdHocFiltersVariable, SceneDataTransformer, SceneGridItem, + SwitchVariable, } from '@grafana/scenes'; import { AdhocVariableKind, @@ -27,6 +28,7 @@ import { GroupByVariableKind, IntervalVariableKind, QueryVariableKind, + SwitchVariableKind, TextVariableKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; @@ -204,6 +206,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { sceneVariableClass: AdHocFiltersVariable, index: 7, }); + validateVariable({ + sceneVariable: variables?.state.variables[8], + variableKind: dash.variables[8] as SwitchVariableKind, + scene: scene, + dashSpec: dash, + sceneVariableClass: SwitchVariable, + index: 8, + }); // Annotations expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); @@ -371,7 +381,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(snapshot); // check variables were converted to snapshot variables - expect(scene.state.$variables?.state.variables).toHaveLength(8); + expect(scene.state.$variables?.state.variables).toHaveLength(9); expect(scene.state.$variables?.getByName('customVar')).toBeInstanceOf(SnapshotVariable); expect(scene.state.$variables?.getByName('adhocVar')).toBeInstanceOf(AdHocFiltersVariable); expect(scene.state.$variables?.getByName('intervalVar')).toBeInstanceOf(SnapshotVariable); diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 5ce3e2ffadd..642729d9ed5 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -311,6 +311,31 @@ describe('ResponseTransformers', () => { type: 'query', query: { refId: 'A', query: 'label_values(grafanacloud_org_info{org_slug="$org_slug"}, org_id)' }, }, + { + type: 'switch', + name: 'var9', + label: 'Switch variable', + description: 'Switch variable description', + skipUrlSync: false, + hide: 0, + current: { + value: 'true', + text: 'true', + }, + options: [ + { + selected: true, + text: 'true', + value: 'true', + }, + { + selected: false, + text: 'false', + value: 'false', + }, + ], + query: '', + }, ], }, panels: [ @@ -523,6 +548,7 @@ describe('ResponseTransformers', () => { validateVariablesV1ToV2(spec.variables[6], dashboardV1.templating?.list?.[6]); validateVariablesV1ToV2(spec.variables[7], dashboardV1.templating?.list?.[7]); validateVariablesV1ToV2(spec.variables[8], dashboardV1.templating?.list?.[8]); + validateVariablesV1ToV2(spec.variables[9], dashboardV1.templating?.list?.[9]); }); }); @@ -930,6 +956,7 @@ describe('ResponseTransformers', () => { validateVariablesV1ToV2(dashboardV2.spec.variables[5], dashboard.templating?.list?.[5]); validateVariablesV1ToV2(dashboardV2.spec.variables[6], dashboard.templating?.list?.[6]); validateVariablesV1ToV2(dashboardV2.spec.variables[7], dashboard.templating?.list?.[7]); + validateVariablesV1ToV2(dashboardV2.spec.variables[8], dashboard.templating?.list?.[8]); // annotations validateAnnotation(dashboard.annotations!.list![0], dashboardV2.spec.annotations[0]); validateAnnotation(dashboard.annotations!.list![1], dashboardV2.spec.annotations[1]); @@ -1172,5 +1199,23 @@ describe('ResponseTransformers', () => { expect(v2.group).toEqual(v1.datasource?.type); expect(v2.spec.options).toEqual(v1.options); } + + if (v2.kind === 'SwitchVariable') { + // V1 switch variables have options array with exactly 2 options + // First option is enabledValue, second is disabledValue + const options = v1.options ?? []; + const enabledValueRaw = options[0]?.value ?? 'true'; + const disabledValueRaw = options[1]?.value ?? 'false'; + const enabledValue = Array.isArray(enabledValueRaw) ? enabledValueRaw[0] : enabledValueRaw; + const disabledValue = Array.isArray(disabledValueRaw) ? disabledValueRaw[0] : disabledValueRaw; + + // Current value should be a string (not array) + const currentValueRaw = v1.current?.value ?? disabledValue; + const currentValue = Array.isArray(currentValueRaw) ? currentValueRaw[0] : currentValueRaw; + + expect(v2.spec.current).toBe(currentValue); + expect(v2.spec.enabledValue).toBe(enabledValue); + expect(v2.spec.disabledValue).toBe(disabledValue); + } } }); diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index ca8a48de32d..d4bce12f211 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -34,6 +34,7 @@ import { IntervalVariableKind, TextVariableKind, GroupByVariableKind, + SwitchVariableKind, LibraryPanelKind, PanelKind, GridLayoutItemKind, @@ -809,6 +810,29 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] variables.push(gb); break; + case 'switch': + // V1 switch variables have options array with exactly 2 options + // First option is typically enabledValue, second is disabledValue + const options = v.options ?? []; + const enabledValueRaw = options[0]?.value ?? 'true'; + const disabledValueRaw = options[1]?.value ?? 'false'; + const enabledValue = Array.isArray(enabledValueRaw) ? enabledValueRaw[0] : enabledValueRaw; + const disabledValue = Array.isArray(disabledValueRaw) ? disabledValueRaw[0] : disabledValueRaw; + // Current value should be a string (not array) + const currentValueRaw = v.current?.value ?? disabledValue; + const currentValue = Array.isArray(currentValueRaw) ? currentValueRaw[0] : currentValueRaw; + + const sw: SwitchVariableKind = { + kind: 'SwitchVariable', + spec: { + ...commonProperties, + current: currentValue, + enabledValue, + disabledValue, + }, + }; + variables.push(sw); + break; default: // do not throw error, just log it console.error(`Variable transformation not implemented: ${v.type}`); @@ -997,6 +1021,29 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] { }; variables.push(av); break; + case 'SwitchVariable': + const sv: VariableModel = { + ...commonProperties, + current: { + text: v.spec.current, + value: v.spec.current, + }, + options: [ + { + text: v.spec.enabledValue, + value: v.spec.enabledValue, + selected: v.spec.current === v.spec.enabledValue, + }, + { + text: v.spec.disabledValue, + value: v.spec.disabledValue, + selected: v.spec.current === v.spec.disabledValue, + }, + ], + query: '', + }; + variables.push(sv); + break; default: // do not throw error, just log it console.error(`Variable transformation not implemented: ${v}`); @@ -1256,6 +1303,8 @@ function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableTyp return 'groupby'; case 'AdhocVariable': return 'adhoc'; + case 'SwitchVariable': + return 'switch'; default: throw new Error(`Unknown variable type: ${variable}`); } From 42d3673d048542b444e9c2ea55f080f6b073d29e Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 27 Nov 2025 15:19:38 +0100 Subject: [PATCH 144/423] Alerting: Add rule_limits to rule list requests (#114176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Alerting: Add rule_limits to rule list requests * Unify pagination limits calculation for GMA and DMA rules * Fix limits, add tests * Alerting: Rename filter functions and limit properties for clarity - hasClientSideFilters → hasGrafanaClientSideFilters - hasDatasourceFilters → hasDatasourceClientSideFilters - gmaLimit → grafanaManagedLimit - dmaLimit → datasourceManagedLimit --------- Co-authored-by: Konrad Lalik --- .../alerting/unified/api/prometheusApi.ts | 3 + .../alerting/unified/rule-list/FilterView.tsx | 8 +- .../rule-list/PaginatedGrafanaLoader.tsx | 21 +- .../rule-list/hooks/datasourceFilter.ts | 21 ++ .../rule-list/hooks/filterNormalization.ts | 6 +- .../rule-list/hooks/grafanaFilter.test.ts | 100 +++++----- .../unified/rule-list/hooks/grafanaFilter.ts | 28 +-- .../hooks/prometheusGroupsGenerator.ts | 52 +++-- .../hooks/useFilteredRulesIterator.ts | 12 +- .../rule-list/paginationLimits.test.ts | 181 ++++++++++++++++++ .../unified/rule-list/paginationLimits.ts | 33 ++++ 11 files changed, 362 insertions(+), 103 deletions(-) create mode 100644 public/app/features/alerting/unified/rule-list/paginationLimits.test.ts diff --git a/public/app/features/alerting/unified/api/prometheusApi.ts b/public/app/features/alerting/unified/api/prometheusApi.ts index 565a50e46ad..a8e4279de7f 100644 --- a/public/app/features/alerting/unified/api/prometheusApi.ts +++ b/public/app/features/alerting/unified/api/prometheusApi.ts @@ -39,6 +39,7 @@ export type GrafanaPromRulesOptions = Omit { const currentGenerator = groupsGenerator.current; diff --git a/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts index e6aa3089e55..b2360913581 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/datasourceFilter.ts @@ -22,6 +22,27 @@ import { ruleTypeFilter, } from './filterPredicates'; +/** + * Determines if client-side filtering is needed for data source-managed rules. + */ +export function hasDatasourceClientSideFilters(filterState: Partial): boolean { + // Check if any filter that applies to datasource rules is active + return ( + (filterState.freeFormWords && filterState.freeFormWords.length > 0) || + Boolean(filterState.ruleName) || + Boolean(filterState.ruleState) || + Boolean(filterState.ruleType) || + (filterState.dataSourceNames && filterState.dataSourceNames.length > 0) || + (filterState.labels && filterState.labels.length > 0) || + Boolean(filterState.ruleHealth) || + Boolean(filterState.dashboardUid) || + Boolean(filterState.plugins) || + Boolean(filterState.contactPoint) || + Boolean(filterState.namespace) || + Boolean(filterState.groupName) + ); +} + /** * Builds filter configurations for data source-managed alert rules. * diff --git a/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts b/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts index d8594135463..7bcf22e30dc 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/filterNormalization.ts @@ -32,12 +32,14 @@ export function buildTitleSearch(filterState: RulesFilter): string | undefined { * Normalize filter state for case-insensitive matching * Lowercase free form words, rule name, group name and namespace */ -export function normalizeFilterState(filterState: RulesFilter): RulesFilter { +export function normalizeFilterState(filterState: Partial): RulesFilter { return { ...filterState, - freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()), + freeFormWords: filterState.freeFormWords?.map((word) => word.toLowerCase()) ?? [], ruleName: filterState.ruleName?.toLowerCase(), groupName: filterState.groupName?.toLowerCase(), namespace: filterState.namespace?.toLowerCase(), + dataSourceNames: filterState.dataSourceNames ?? [], + labels: filterState.labels ?? [], }; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts index 56be25ee248..6a714382a96 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts @@ -8,7 +8,7 @@ import { Annotation } from '../../utils/constants'; import { getDatasourceAPIUid } from '../../utils/datasource'; import { getFilter } from '../../utils/search'; -import { getGrafanaFilter, hasClientSideFilters } from './grafanaFilter'; +import { getGrafanaFilter, hasGrafanaClientSideFilters } from './grafanaFilter'; jest.mock('../../utils/datasource'); @@ -670,41 +670,41 @@ describe('grafana-managed rules', () => { }); }); - describe('hasClientSideFilters', () => { + describe('hasGrafanaClientSideFilters', () => { describe('when alertingUIUseBackendFilters is disabled', () => { testWithFeatureToggles({ disable: ['alertingUIUseBackendFilters'] }); it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); }); it('should return true for title-related filters (freeFormWords, ruleName)', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); }); it('should return true for ruleType filter', () => { - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); }); it('should return true for dashboardUid filter', () => { - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); }); it('should return true for groupName filter', () => { - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); }); it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); it('should return false for backend-only filters (state, health, contactPoint)', () => { - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); }); }); @@ -712,36 +712,36 @@ describe('grafana-managed rules', () => { testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] }); it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); }); it('should return false for title-related filters (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); }); it('should return false for ruleType filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); }); it('should return false for dashboardUid filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); }); it('should return false for groupName filter (handled by backend)', () => { - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); }); it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); it('should return false for backend-only filters (state, health, contactPoint)', () => { - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); }); }); @@ -750,20 +750,20 @@ describe('grafana-managed rules', () => { it('should return correct values for all filter types', () => { // Should return false for: empty, backend-handled (ruleType, dashboardUid), and backend-only filters - expect(hasClientSideFilters(getFilter({}))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); // Should return true for: frontend-handled filters - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); }); @@ -772,20 +772,20 @@ describe('grafana-managed rules', () => { it('should return correct values for all filter types', () => { // Should return false for: empty, all backend-handled filters, and backend-only filters - expect(hasClientSideFilters(getFilter({}))).toBe(false); - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); - expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({}))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); // Should return true for: always-frontend filters only - expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); }); }); }); diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts index 5b5ddd81f9b..0cc89ceafcf 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts @@ -24,26 +24,26 @@ import { /** * Determines if client-side filtering is needed for Grafana-managed rules. */ -export function hasClientSideFilters(filterState: RulesFilter): boolean { +export function hasGrafanaClientSideFilters(filterState: Partial): boolean { const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs(); // Check each rule filter: if the config has a non-null handler AND the filter state has a value, we need client-side filtering const hasActiveRuleFilters = - (ruleFilterConfig.freeFormWords !== null && filterState.freeFormWords.length > 0) || - (ruleFilterConfig.ruleName !== null && Boolean(filterState.ruleName)) || - (ruleFilterConfig.ruleState !== null && Boolean(filterState.ruleState)) || - (ruleFilterConfig.ruleType !== null && Boolean(filterState.ruleType)) || - (ruleFilterConfig.dataSourceNames !== null && filterState.dataSourceNames.length > 0) || - (ruleFilterConfig.labels !== null && filterState.labels.length > 0) || - (ruleFilterConfig.ruleHealth !== null && Boolean(filterState.ruleHealth)) || - (ruleFilterConfig.dashboardUid !== null && Boolean(filterState.dashboardUid)) || - (ruleFilterConfig.plugins !== null && Boolean(filterState.plugins)) || - (ruleFilterConfig.contactPoint !== null && Boolean(filterState.contactPoint)); + (ruleFilterConfig.freeFormWords !== null && Boolean(filterState?.freeFormWords?.length)) || + (ruleFilterConfig.ruleName !== null && Boolean(filterState?.ruleName)) || + (ruleFilterConfig.ruleState !== null && Boolean(filterState?.ruleState)) || + (ruleFilterConfig.ruleType !== null && Boolean(filterState?.ruleType)) || + (ruleFilterConfig.dataSourceNames !== null && Boolean(filterState?.dataSourceNames?.length)) || + (ruleFilterConfig.labels !== null && Boolean(filterState?.labels?.length)) || + (ruleFilterConfig.ruleHealth !== null && Boolean(filterState?.ruleHealth)) || + (ruleFilterConfig.dashboardUid !== null && Boolean(filterState?.dashboardUid)) || + (ruleFilterConfig.plugins !== null && Boolean(filterState?.plugins)) || + (ruleFilterConfig.contactPoint !== null && Boolean(filterState?.contactPoint)); // Check each group filter: if the config has a non-null handler AND the filter state has a value, we need client-side filtering const hasActiveGroupFilters = - (groupFilterConfig.namespace !== null && Boolean(filterState.namespace)) || - (groupFilterConfig.groupName !== null && Boolean(filterState.groupName)); + (groupFilterConfig.namespace !== null && Boolean(filterState?.namespace)) || + (groupFilterConfig.groupName !== null && Boolean(filterState?.groupName)); return hasActiveRuleFilters || hasActiveGroupFilters; } @@ -55,7 +55,7 @@ export function hasClientSideFilters(filterState: RulesFilter): boolean { * The backend filter is used for server-side filtering when `shouldUseBackendFilters()` is enabled, * while the frontend filter provides client-side matching functions for rules and groups. */ -export function getGrafanaFilter(filterState: RulesFilter) { +export function getGrafanaFilter(filterState: Partial) { const normalizedFilterState = normalizeFilterState(filterState); const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs(); diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index db2d5df077d..add1097fa0f 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { MergeExclusive } from 'type-fest'; import { DataSourceRulesSourceIdentifier, RuleHealth } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; @@ -16,27 +17,23 @@ interface UseGeneratorHookOptions { limitAlerts?: number; } -interface FetchGroupsOptions { - groupLimit?: number; - groupNextToken?: string; -} - export function usePrometheusGroupsGenerator() { const [getGroups] = useLazyGetGroupsQuery(); return useCallback( async function* (ruleSource: DataSourceRulesSourceIdentifier, groupLimit: number) { - const getRuleSourceGroupsWithCache = async (fetchOptions: FetchGroupsOptions) => { + const getRuleSourceGroupsWithCache = async (fetchOptions: GroupsNextPageOptions) => { const response = await getGroups({ ruleSource: { uid: ruleSource.uid }, notificationOptions: { showErrorAlert: false }, + groupLimit, ...fetchOptions, }).unwrap(); return response; }; - yield* genericGroupsGenerator(getRuleSourceGroupsWithCache, groupLimit); + yield* genericGroupsGenerator(getRuleSourceGroupsWithCache); }, [getGroups] ); @@ -52,8 +49,21 @@ interface GrafanaPromApiFilter { dashboardUid?: string; } -interface GrafanaFetchGroupsOptions extends FetchGroupsOptions { +interface GrafanaFetchGroupsOptions extends GroupsNextPageOptions { filter?: GrafanaPromApiFilter; + groupLimit?: number; + // Limits the number of total rules returned across all groups + // Rounds up to full groups, so the response may contain more rules than the group limit + ruleLimit?: number; +} + +export type GrafanaFetchGroupsLimit = MergeExclusive<{ groupLimit: number }, { ruleLimit: number }>; + +export type DataSourceFetchGroupsLimit = { groupLimit: number }; + +export interface FetchGroupsLimitOptions { + grafanaManagedLimit: GrafanaFetchGroupsLimit; + datasourceManagedLimit: DataSourceFetchGroupsLimit; } export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = {}) { @@ -78,11 +88,16 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = ); return useCallback( - async function* (groupLimit: number, filter?: GrafanaPromApiFilter) { - yield* genericGroupsGenerator( - (fetchOptions) => getGroupsAndProvideCache({ ...fetchOptions, filter }), - groupLimit - ); + async function* (limit: GrafanaFetchGroupsLimit, filter?: GrafanaPromApiFilter) { + const fetchGroups = (fetchOptions: GroupsNextPageOptions) => + getGroupsAndProvideCache({ + ...fetchOptions, + filter, + groupLimit: 'groupLimit' in limit ? limit.groupLimit : undefined, + ruleLimit: 'ruleLimit' in limit ? limit.ruleLimit : undefined, + }); + + yield* genericGroupsGenerator(fetchGroups); }, [getGroupsAndProvideCache] ); @@ -105,21 +120,24 @@ export function toIndividualRuleGroups( })(); } +interface GroupsNextPageOptions { + groupNextToken?: string; +} + // Generator lazily provides groups one by one only when needed // This might look a bit complex but it allows us to have one API for paginated and non-paginated Prometheus data sources // For unpaginated data sources we fetch everything in one go // For paginated we fetch the next page when needed async function* genericGroupsGenerator( - fetchGroups: (options: FetchGroupsOptions) => Promise>, - groupLimit: number + fetchGroups: (options: GroupsNextPageOptions) => Promise> ) { - let response = await fetchGroups({ groupLimit }); + let response = await fetchGroups({ groupNextToken: undefined }); yield response.data.groups; let lastToken: string | undefined = response.data?.groupNextToken; while (lastToken) { - response = await fetchGroups({ groupNextToken: lastToken, groupLimit: groupLimit }); + response = await fetchGroups({ groupNextToken: lastToken }); yield response.data.groups; lastToken = response.data?.groupNextToken; } diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 407426bb3e1..7c5533beea5 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -26,7 +26,11 @@ import { RulePositionHash, createRulePositionHash } from '../rulePositionHash'; import { getDatasourceFilter } from './datasourceFilter'; import { getGrafanaFilter } from './grafanaFilter'; -import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator'; +import { + FetchGroupsLimitOptions, + useGrafanaGroupsGenerator, + usePrometheusGroupsGenerator, +} from './prometheusGroupsGenerator'; export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin; @@ -74,7 +78,7 @@ export function useFilteredRulesIteratorProvider() { const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ limitAlerts: 0 }); - const getFilteredRulesIterable = (filterState: RulesFilter, groupLimit: number): GetIteratorResult => { + const getFilteredRulesIterable = (filterState: RulesFilter, options: FetchGroupsLimitOptions): GetIteratorResult => { /* this is the abort controller that allows us to stop an AsyncIterable */ const abortController = new AbortController(); @@ -83,7 +87,7 @@ export function useFilteredRulesIteratorProvider() { const { backendFilter, frontendFilter } = getGrafanaFilter(filterState); const grafanaRulesGenerator: AsyncIterableX = from( - grafanaGroupsGenerator(groupLimit, backendFilter) + grafanaGroupsGenerator(options.grafanaManagedLimit, backendFilter) ).pipe( withAbort(abortController.signal), concatMap((groups) => @@ -110,7 +114,7 @@ export function useFilteredRulesIteratorProvider() { const dataSourceGenerators: Array> = externalRulesSourcesToFetchFrom.map( (dataSourceIdentifier) => { const promGroupsGenerator: AsyncIterableX = from( - prometheusGroupsGenerator(dataSourceIdentifier, groupLimit) + prometheusGroupsGenerator(dataSourceIdentifier, options.datasourceManagedLimit.groupLimit) ).pipe( withAbort(abortController.signal), concatMap((groups) => diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts new file mode 100644 index 00000000000..5ef8431aced --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts @@ -0,0 +1,181 @@ +import { testWithFeatureToggles } from 'test/test-utils'; + +import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; + +import { RuleHealth, RulesFilter } from '../search/rulesSearchParser'; +import { getFilter } from '../utils/search'; + +import { + FILTERED_GROUPS_LARGE_API_PAGE_SIZE, + FILTERED_GROUPS_SMALL_API_PAGE_SIZE, + RULE_LIMIT_WITH_BACKEND_FILTERS, + getFilteredRulesLimits, +} from './paginationLimits'; + +describe('paginationLimits', () => { + describe('getFilteredRulesLimits', () => { + describe('when backend filters are disabled', () => { + testWithFeatureToggles({ disable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should return small limits when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])('should return small grafana limit + large datasource limit for backend-only filter: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + { dashboardUid: 'test-dashboard' }, + { plugins: 'hide' as const }, + { namespace: 'production' }, + { groupName: 'test-group' }, + { namespace: 'production', freeFormWords: ['cpu'] }, + ])('should return large limits for both when frontend filters are used: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + }); + + describe('when alertingUIUseBackendFilters is enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { groupName: 'test-group' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + { ruleState: PromAlertingRuleState.Firing, namespace: 'production' }, + ])('should return large limits for both when frontend filters are used: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + }); + + describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { groupName: 'test-group' }, + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + ])('should return large limits for both when frontend filters are used: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + }); + + describe('when both backend filter toggles are enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should return rule limit for grafana + default limit for datasource when no filters are applied', () => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter({})); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_SMALL_API_PAGE_SIZE }); + }); + + it.each>([ + { freeFormWords: ['cpu'] }, + { ruleName: 'alert' }, + { ruleType: PromRuleType.Alerting }, + { dashboardUid: 'test-dashboard' }, + { groupName: 'test-group' }, + { ruleState: PromAlertingRuleState.Firing }, + { ruleHealth: RuleHealth.Ok }, + { contactPoint: 'slack' }, + ])( + 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', + (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + } + ); + + it.each>([ + { namespace: 'production' }, + { dataSourceNames: ['prometheus'] }, + { labels: ['severity=critical'] }, + ])('should return large limits for both when frontend filters are used: %p', (filterState) => { + const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); + + expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); + }); + }); + }); +}); diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.ts index a8e03ac5a48..a5d359ded7b 100644 --- a/public/app/features/alerting/unified/rule-list/paginationLimits.ts +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.ts @@ -1,3 +1,10 @@ +import { shouldUseBackendFilters, shouldUseFullyCompatibleBackendFilters } from '../featureToggles'; +import { RulesFilter } from '../search/rulesSearchParser'; + +import { hasDatasourceClientSideFilters } from './hooks/datasourceFilter'; +import { hasGrafanaClientSideFilters } from './hooks/grafanaFilter'; +import { FetchGroupsLimitOptions } from './hooks/prometheusGroupsGenerator'; + export const FRONTEND_LIST_PAGE_SIZE = 100; export const FILTERED_GROUPS_LARGE_API_PAGE_SIZE = 2000; @@ -6,6 +13,8 @@ export const FILTERED_GROUPS_SMALL_API_PAGE_SIZE = 100; export const DEFAULT_GROUPS_API_PAGE_SIZE = 40; export const FRONTED_GROUPED_PAGE_SIZE = DEFAULT_GROUPS_API_PAGE_SIZE; +export const RULE_LIMIT_WITH_BACKEND_FILTERS = 100; + export function getApiGroupPageSize(hasFilters: boolean) { return hasFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE; } @@ -13,3 +22,27 @@ export function getApiGroupPageSize(hasFilters: boolean) { export function getSearchApiGroupPageSize(hasFrontendFilters: boolean) { return hasFrontendFilters ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE : FILTERED_GROUPS_SMALL_API_PAGE_SIZE; } + +export function getFilteredRulesLimits(filterState: RulesFilter): FetchGroupsLimitOptions { + return { + grafanaManagedLimit: getGrafanaFilterLimits(filterState), + datasourceManagedLimit: { + groupLimit: hasDatasourceClientSideFilters(filterState) + ? FILTERED_GROUPS_LARGE_API_PAGE_SIZE + : FILTERED_GROUPS_SMALL_API_PAGE_SIZE, + }, + }; +} + +function getGrafanaFilterLimits(filterState: RulesFilter) { + const backendFiltersEnabled = shouldUseFullyCompatibleBackendFilters() || shouldUseBackendFilters(); + + const frontendFiltersInUse = hasGrafanaClientSideFilters(filterState); + const onlyBackendFiltersInUse = frontendFiltersInUse === false; + + if (backendFiltersEnabled && onlyBackendFiltersInUse) { + return { ruleLimit: RULE_LIMIT_WITH_BACKEND_FILTERS }; + } + + return { groupLimit: getSearchApiGroupPageSize(frontendFiltersInUse) }; +} From eedb613a5ee60d4aae68202f616ed464aaab20e9 Mon Sep 17 00:00:00 2001 From: "Marc M." <146180665+grafakus@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:41:38 +0100 Subject: [PATCH 145/423] Dashboards: Don't store options when saving a dashboard with Query/Custom variables (#114540) --- .../src/schema/dashboard/v2_examples.ts | 13 +------ ...sformSceneToSaveModelSchemaV2.test.ts.snap | 13 +------ .../sceneVariablesSetToVariables.test.ts | 36 ++----------------- .../sceneVariablesSetToVariables.ts | 13 +++---- 4 files changed, 8 insertions(+), 67 deletions(-) diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 649546e17e1..7c542121a7b 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -312,18 +312,7 @@ export const handyTestingSchema: Spec = { label: 'Custom Variable', multi: true, name: 'customVar', - options: [ - { - selected: true, - text: 'option1', - value: 'option1', - }, - { - selected: false, - text: 'option2', - value: 'option2', - }, - ], + options: [], query: 'option1, option2', skipUrlSync: false, allowCustomValue: true, diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 6bddc68fb09..add2123cdad 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -194,18 +194,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "label": "Custom Variable", "multi": true, "name": "customVar", - "options": [ - { - "selected": true, - "text": "option1", - "value": "option1", - }, - { - "selected": false, - "text": "option2", - "value": "option2", - }, - ], + "options": [], "query": "option1, option2", "skipUrlSync": false, }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index 8772646b496..cc49cfadc77 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -371,23 +371,7 @@ describe('sceneVariablesSetToVariables', () => { "label": "test-label", "multi": true, "name": "test", - "options": [ - { - "selected": true, - "text": "test", - "value": "test", - }, - { - "selected": false, - "text": "test1", - "value": "test1", - }, - { - "selected": true, - "text": "test2", - "value": "test2", - }, - ], + "options": [], "query": "test,test1,test2", "type": "custom", } @@ -1161,23 +1145,7 @@ describe('sceneVariablesSetToVariables', () => { "label": "test-label", "multi": true, "name": "test", - "options": [ - { - "selected": true, - "text": "test", - "value": "test", - }, - { - "selected": false, - "text": "test1", - "value": "test1", - }, - { - "selected": true, - "text": "test2", - "value": "test2", - }, - ], + "options": [], "query": "test,test1,test2", "skipUrlSync": false, }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 49b84626f97..fa7c2cc3855 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -66,9 +66,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio if (sceneUtils.isQueryVariable(variable)) { let options: VariableOption[] = []; - // Not sure if we actually have to still support this option given - // that it's not exposed in the UI - if (transformVariableRefreshToEnum(variable.state.refresh) === 'never' || keepQueryOptions) { + if (keepQueryOptions) { options = variableValueOptionsToVariableOptions(variable.state); } variables.push({ @@ -106,7 +104,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio // @ts-expect-error value: variable.state.value, }, - options: variableValueOptionsToVariableOptions(variable.state), + options: [], query: variable.state.query, multi: variable.state.isMulti, allValue: variable.state.allValue, @@ -319,9 +317,7 @@ export function sceneVariablesSetToSchemaV2Variables( // Query variable if (sceneUtils.isQueryVariable(variable)) { - // Not sure if we actually have to still support this option given - // that it's not exposed in the UI - if (transformVariableRefreshToEnum(variable.state.refresh) === 'never' || keepQueryOptions) { + if (keepQueryOptions) { options = variableValueOptionsToVariableOptions(variable.state); } const query = variable.state.query; @@ -385,13 +381,12 @@ export function sceneVariablesSetToSchemaV2Variables( // Custom variable } else if (sceneUtils.isCustomVariable(variable)) { - options = variableValueOptionsToVariableOptions(variable.state); const customVariable: CustomVariableKind = { kind: 'CustomVariable', spec: { ...commonProperties, current: currentVariableOption, - options, + options: [], query: variable.state.query, multi: variable.state.isMulti || false, allValue: variable.state.allValue, From 763067f8e13c167195a344f7c0302d87e1dbdb01 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 27 Nov 2025 07:52:42 -0700 Subject: [PATCH 146/423] Dashboard Schema V2: Force v2 when dashboardNewLayouts or v2DashboardAPI are enabled (#113548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SchemaV2: Convertion from v1beta1 to v2beta1 * Compare backend-frontend v1 convertion * Compare backend-frontend v1 convertion * Fix fe be diff * Resolve DS issues * Fix ds inconsistecnies * fix legacy string value issues * fix ds test * fix layout issue * update test * Fix tests and issue with defaultConfig * Update output * Fix viz config convertion * wip * Fix v1 to v2 dashboard transformation differences Major fixes implemented: - Backend function names in conversion.go - Backend group field logic for queries, annotations, and vizConfig - Backend datasource resolution with map-based lookup - Backend timezone handling (empty string vs browser) - Backend annotation processing (empty array vs default annotation) - Backend default values (editable, liveNow) - Backend variable processing (definition, defaultKeys, refresh, refId) - Backend panel layout (y position calculations) - Backend VizConfig (Kind and Group fields, default values) - Frontend snapshot issue (annotations not processing) - Frontend datasource references (only when original has valid datasource) Test results: - annotation-conversions: PASSING (0 differences) - dashboard-properties: 3 expected architectural differences - panel-conversions: Multiple expected architectural differences - variable-conversions: 7 expected architectural differences All remaining differences are expected architectural choices between backend persistence optimization and frontend UI consumption optimization. * fix issues with panel and annotation queries with no datasource * definition and regex * Use proper v1beta1 resource when testing * remove misc file * fix ds provider test * fix def ds test in response transformer * fix remaining ResponseTransformers test * timesettings, variable refresh, editable, liveNow, definition * fix transformSceneToSaveModelSchemaV2 test * revert legacyRow changes * fix go lint issues * normalize y coordinates when serializing a row * clean up * update tests * use GetStringValue from schemaversion * fix go lint - cyclomatic complexity * update open api snapshot * add migrated dashboards * fix default panel type when panel type is not provided * revert dash link changes for now * fix * fix nested panel issue and default ref in v1 * apply defaults to nested panels too * update snapshots * fix issues with annotations * matchers, showLegend, annotations * when converting also don't process queries that have only a refId * fix issues with text var * fix dash links * default to collapse: false when serializing * fix: filter refId from variable query specs in backend migration - Add buildDataQueryKindForVariable function to filter refId for variables - Remove default refId "A" in transformSingleQuery - Only include __legacyStringValue for non-empty string queries - Remove refId addition in transformSaveModelSchemaV2ToScene.getDataQueryForVariable - Handle undefined queries gracefully in frontend and backend - Ensure backend matches frontend behavior for query variable serialization * fix: default variable refresh to 'never' to match frontend behavior Change backend default for missing refresh field from 'onDashboardLoad' to 'never' to match frontend defaultVariableRefresh() schema default * fix: only include iconColor in annotations when it exists - Frontend: Use defaultAnnotationQuerySpec().iconColor as fallback to match schema defaults - Backend: Only set iconColor if it exists in v1 input (not using GetStringValue) - Ensures iconColor is only included when present in original dashboard * fix: use schema defaults for annotation enable, hide, and iconColor - Use defaultAnnotationQuerySpec() to get schema defaults instead of hardcoded values - Default enable to false (schema default) to match frontend behavior - Use schema default for iconColor and hide fields - Ensures consistency with frontend which uses defaultAnnotationQuerySpec() defaults * fix: set collapse for hidden-header rows to match first explicit row - When panels appear before the first explicit row, the hidden-header row's collapse should match the first explicit row's collapsed value - Matches frontend behavior where collapse: panel.collapsed uses the next row panel's collapsed value - Ensures consistency between frontend and backend when converting rows layout * fix: handle constant variables with missing query value - Frontend: Fix bug where undefined value was converted to string 'undefined' - Now defaults to empty string when value is undefined: value ? String(value) : '' - Backend: Match frontend fix - default to empty string for text/value when query is missing - Ensures consistency when constant variable query is missing from v1 dashboard * Fix interval variable handling when query is missing - Extract intervals from options when query is missing/empty (matches backend behavior) - Handle undefined/null query in getIntervalsFromQueryString - Handle missing current object/value in getCurrentValueForOldIntervalModel - Update interval variable refresh to use literal 'onTimeRangeChanged' in schema - Use defaultIntervalVariableSpec() for interval variable serialization - Backend: Generate query string from options when query is missing * Fix corrupted dashboard with systemRef override * don't resolve types for template variables in datasource refs on the backend * fix annotation and ds issues * fix range and special mappings * fix datasource var pluginId and regex * add __systemRef to schema * update v15 migration annotation to have a ds type because v2 keeps track of if type is in the initial save model, and if it's not it removes it, but for frontendOuput we are running transformSaveModelToScene which will then assign the type * add migration fields since the backend applies automigrations in collapsed rows * filter out queries in ResponseTransformer that only have refId field * lint * v2: add default query if queries are empty to match v1 behavior * fix single migration test * tracking test should have a defined spec otherwise datasource is removed and won't be tracked * initialize default with default ds ref * wip * Do not assign DS if ds group is empty * cleanup * revert change in setupTests.ts * clean up TODO * query with only refId should not expect to have a group * refactor: extract v0alpha1 to v1beta1 conversion logic into atomic function - Extract ConvertDashboard_V0_to_V1beta1 into v0alpha1_to_v1beta1.go - Extract prepareV0ConversionContext and migrateV0Dashboard helper functions - Standardize v0.go to match v1.go pattern with inline multi-step conversions - Implement Convert_V0_to_V2alpha1 using atomic functions (v0->v1beta1->v2alpha1) - Implement Convert_V0_to_V2beta1 using atomic functions (v0->v1beta1->v2alpha1->v2beta1) - Remove non-atomic v0alpha1_to_v2alpha1.go file * test: add version-specific test files for conversion error handling - Extract v0 conversion tests into v0_test.go - Extract v1 conversion tests into v1_test.go - Add v2 conversion tests in v2_test.go - Ensure all error handling paths in conversion functions are covered - Add tests for Convert_V0_to_V2alpha1 and Convert_V0_to_V2beta1 error paths - Add tests for Convert_V1beta1_to_V2alpha1 and Convert_V1beta1_to_V2beta1 error paths - Add tests for Convert_V2alpha1_to_V2beta1 error handling * Fix tests * Fix linter * Clean up * feat(dashboard): Add automatic data loss detection for dashboard conversions Implements comprehensive data loss detection for all dashboard API version conversions. Components Tracked: • Panels (visualization + library panels) • Queries (data source queries, excludes row panel queries) • Annotations • Links • Variables (template variables) Features: • Automatic detection via withConversionMetrics wrapper (zero code changes) • Error type: 'conversion_data_loss_error' • Logs: panelsLost, queriesLost, annotationsLost, linksLost, variablesLost Bugs Found: • Fixed critical bug: metrics.go was silently swallowing ALL errors (return nil → return err) Testing: • TestDataLossDetectionOnAllInputFiles - runs all conversions with detailed logging • V2→V0/V1 downgrades write output for debugging then skip (not yet implemented) • All tests passing * Run dashboards on schema v2 E2Es * reveret unintended changes * cleanup * Reset active manager correctly according to toggles config * Fix new dashboard being serialized as v1 * Rename toggle --------- Co-authored-by: Ivan Ortega Co-authored-by: Dominik Prokop --- .../dashboard-browse-nested.spec.ts | 3 +- .../dashboards-suite/dashboard-browse.spec.ts | 3 +- .../dashboard-export-image.spec.ts | 3 +- .../dashboard-export-json.spec.ts | 3 +- .../dashboard-keybindings.spec.ts | 3 +- .../dashboard-links-without-slug.spec.ts | 3 +- .../dashboard-live-streaming.spec.ts | 3 +- .../dashboard-public-create.spec.ts | 3 +- .../dashboard-public-templating.spec.ts | 3 +- .../dashboard-share-externally-create.spec.ts | 3 +- .../dashboard-share-internally.spec.ts | 3 +- .../dashboard-share-snapshot-create.spec.ts | 3 +- .../dashboard-templating.spec.ts | 3 +- .../dashboard-time-zone.spec.ts | 3 +- .../dashboard-timepicker.spec.ts | 3 +- .../embedded-dashboard.spec.ts | 3 +- .../general-dashboards.spec.ts | 3 +- .../dashboards-suite/import-dashboard.spec.ts | 3 +- .../load-options-from-url.spec.ts | 3 +- .../new-constant-variable.spec.ts | 3 +- .../new-custom-variable.spec.ts | 3 +- .../new-datasource-variable.spec.ts | 3 +- .../new-interval-variable.spec.ts | 3 +- .../new-query-variable.spec.ts | 3 +- .../new-text-box-variable.spec.ts | 3 +- .../repeating-a-panel-horizontally.spec.ts | 3 +- .../repeating-a-panel-vertically.spec.ts | 3 +- .../repeating-an-empty-row.spec.ts | 3 +- .../set-options-from-ui.spec.ts | 3 +- .../dashboards-suite/snapshot-create.spec.ts | 3 +- ...ting-dashboard-links-and-variables.spec.ts | 3 +- .../textbox-variables.spec.ts | 3 +- .../src/types/featureToggles.gen.ts | 4 +++ pkg/registry/apis/dashboard/register.go | 2 +- pkg/services/featuremgmt/registry.go | 7 +++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 +++ pkg/services/featuremgmt/toggles_gen.json | 30 ++++++++++++++++++- .../pages/DashboardScenePageStateManager.ts | 10 +++++-- .../transformSaveModelToScene.ts | 3 +- public/app/features/dashboard/api/utils.ts | 5 +++- 41 files changed, 123 insertions(+), 39 deletions(-) diff --git a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts index 48a3c00359f..6765d299e53 100644 --- a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @@ -9,7 +9,8 @@ const NUM_NESTED_DASHBOARDS = 60; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts index af0d65adc29..8ae318bcefa 100644 --- a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @@ -4,7 +4,8 @@ import testDashboard from '../dashboards/TestDashboard.json'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts index 3b3c6d26fac..a97a04a14b8 100644 --- a/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-export-image.spec.ts @@ -6,7 +6,8 @@ test.use({ featureToggles: { scenes: true, sharingDashboardImage: true, // Enable the export image feature - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts index 18145a6c739..428193ab5fa 100644 --- a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts @@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts index dd420093696..b0ecf44f9f1 100644 --- a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts @@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts index 530c74c2485..0a982e148b5 100644 --- a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -4,7 +4,8 @@ import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts index 07aacd5b3fb..20f455ea3a8 100644 --- a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts @@ -4,7 +4,8 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts index 5c52138ee08..fd5dc979d81 100644 --- a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts @@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts index 82a2670ef9e..c59e323076d 100644 --- a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts @@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts index d0a6652ccc6..3398e9aaa35 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -3,7 +3,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { scenes: true, - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts index 98d394ccc91..26f8b85d13e 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts @@ -3,7 +3,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { scenes: true, - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts index 3ac515e36e6..1a7e03d6243 100644 --- a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -5,7 +5,8 @@ import { SnapshotCreateResponse } from '../../public/app/features/dashboard/serv test.use({ featureToggles: { scenes: true, - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts index ded0abd61ef..78c35dc5de7 100644 --- a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -5,7 +5,8 @@ const DASHBOARD_UID = 'HYaGDGIMk'; test.use({ timezoneId: 'Pacific/Easter', featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts index 2d56cd14512..937224290b0 100644 --- a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts @@ -7,7 +7,8 @@ const TIMEZONE_DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts index f7a5c4b9dec..4ffd65b83a3 100644 --- a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -16,7 +16,8 @@ test.use({ origins: [], }, featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts index 342b912a48a..f457eddf19e 100644 --- a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts +++ b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts @@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts index 3cdd27ed954..99f84cb6d9f 100644 --- a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts +++ b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts index 7ec014c728b..5fdca6954aa 100644 --- a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts +++ b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts @@ -4,7 +4,8 @@ import testDashboard from '../dashboards/TestDashboard.json'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts index c2f45459431..ca06e31528a 100644 --- a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts index 3e28a6f49bc..fa0b5fc1bfd 100644 --- a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts @@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts index cc3bda551d9..c14a952e1d9 100644 --- a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts @@ -52,7 +52,8 @@ async function assertPreviewValues( test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts index 988d79f78ca..cc19e67cda4 100644 --- a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts @@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts index 0310f6c3500..d76b5291c42 100644 --- a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts @@ -18,7 +18,8 @@ async function assertPreviewValues( test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts index 212f0ecd018..088f4bd9b12 100644 --- a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Templating - Nested Template Variables'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts index 5c49254284f..c669dc563c4 100644 --- a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts @@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts index 920bc343275..a55f14b8643 100644 --- a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts index 3dc9cfcbcc1..bb188c87e8d 100644 --- a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts index 31a60079444..e31c5792062 100644 --- a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts +++ b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts index 556ec524713..53290345e73 100644 --- a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts @@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts index 78c6a6a44c9..123aa7f3279 100644 --- a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts +++ b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts @@ -4,7 +4,8 @@ const DASHBOARD_UID = 'ZqZnVvFZz'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off }, }); diff --git a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 813815f7f39..1d8fd32ff06 100644 --- a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -4,7 +4,8 @@ const DASHBOARD_UID = 'yBCC3aKGk'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts index c992456a1bc..4fb56ef8b8e 100644 --- a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts +++ b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts @@ -6,7 +6,8 @@ const PAGE_UNDER_TEST = 'AejrN1AMz'; test.use({ featureToggles: { - kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true', + kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true', + kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true', }, }); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 33750126afa..900f86ba33f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -361,6 +361,10 @@ export interface FeatureToggles { */ dashboardNewLayouts?: boolean; /** + * Use the v2 kubernetes API in the frontend for dashboards + */ + kubernetesDashboardsV2?: boolean; + /** * Enables undo/redo in dynamic dashboards */ dashboardUndoRedo?: boolean; diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index fb42543de33..ad598f62f9f 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -198,7 +198,7 @@ func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, } func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion { - if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) { + if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts, featuremgmt.FlagKubernetesDashboardsV2) { // If dashboards v2 is enabled, we want to use v2beta1 as the default API version. return []schema.GroupVersion{ dashv2beta1.DashboardResourceInfo.GroupVersion(), diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 37ee3001e99..4fe92fe90d6 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -579,6 +579,13 @@ var ( FrontendOnly: false, // The restore backend feature changes behavior based on this flag Owner: grafanaDashboardsSquad, }, + { + Name: "kubernetesDashboardsV2", + Description: "Use the v2 kubernetes API in the frontend for dashboards", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaDashboardsSquad, + }, { Name: "dashboardUndoRedo", Description: "Enables undo/redo in dynamic dashboards", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f66bc50bbc7..9caa916283d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -80,6 +80,7 @@ dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false +kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 1217d1f5a28..89a171a76ba 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -259,6 +259,10 @@ const ( // Enables experimental new dashboard layouts FlagDashboardNewLayouts = "dashboardNewLayouts" + // FlagKubernetesDashboardsV2 + // Use the v2 kubernetes API in the frontend for dashboards + FlagKubernetesDashboardsV2 = "kubernetesDashboardsV2" + // FlagPdfTables // Enables generating table data as PDF in reporting FlagPdfTables = "pdfTables" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 46e5eda90a7..aeaf5a408af 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1911,6 +1911,18 @@ "expression": "true" } }, + { + "metadata": { + "name": "kubernetesDashboardsV2", + "resourceVersion": "1764236054307", + "creationTimestamp": "2025-11-27T09:34:14Z" + }, + "spec": { + "description": "Use the v2 kubernetes API in the frontend for dashboards", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad" + } + }, { "metadata": { "name": "kubernetesExternalGroupMapping", @@ -3547,6 +3559,22 @@ "expression": "true" } }, + { + "metadata": { + "name": "v2DashboardAPIVersion", + "resourceVersion": "1762457740470", + "creationTimestamp": "2025-11-06T19:22:05Z", + "deletionTimestamp": "2025-11-27T09:34:14Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 19:35:40.470587 +0000 UTC" + } + }, + "spec": { + "description": "Enables the v2 dashboard API version", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad" + } + }, { "metadata": { "name": "vizActionsAuth", @@ -3589,4 +3617,4 @@ } } ] -} \ No newline at end of file +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index a563c17db65..7f8173f9cd9 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -958,6 +958,10 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan } } +export function shouldForceV2API(): boolean { + return Boolean(config.featureToggles.kubernetesDashboardsV2 || config.featureToggles.dashboardNewLayouts); +} + export class UnifiedDashboardScenePageStateManager extends DashboardScenePageStateManagerBase< DashboardDTO | DashboardWithAccessInfo > { @@ -970,7 +974,7 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta this.v1Manager = new DashboardScenePageStateManager(initialState); this.v2Manager = new DashboardScenePageStateManagerV2(initialState); - this.activeManager = config.featureToggles.dashboardNewLayouts ? this.v2Manager : this.v1Manager; + this.activeManager = shouldForceV2API() ? this.v2Manager : this.v1Manager; } private async withVersionHandling( @@ -1075,7 +1079,7 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta public async loadDashboard(options: LoadDashboardOptions): Promise { if (options.route === DashboardRoutes.New) { - const newDashboardVersion = config.featureToggles.dashboardNewLayouts ? 'v2' : 'v1'; + const newDashboardVersion = shouldForceV2API() ? 'v2' : 'v1'; this.setActiveManager(newDashboardVersion); } return this.withVersionHandling((manager) => manager.loadDashboard.call(this, options)); @@ -1089,7 +1093,7 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta } } public resetActiveManager() { - this.setActiveManager('v1'); + this.activeManager = shouldForceV2API() ? this.v2Manager : this.v1Manager; } } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 41dcc938222..9194adf6a7b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -33,6 +33,7 @@ import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior'; +import { shouldForceV2API } from '../pages/DashboardScenePageStateManager'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -258,7 +259,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, let annotationLayers: SceneDataLayerProvider[] = []; let alertStatesLayer: AlertStatesDataLayer | undefined; const uid = oldModel.uid; - const serializerVersion = config.featureToggles.dashboardNewLayouts && !oldModel.meta.isSnapshot ? 'v2' : 'v1'; + const serializerVersion = shouldForceV2API() && !oldModel.meta.isSnapshot ? 'v2' : 'v1'; if (oldModel.meta.isSnapshot) { variables = createVariablesForSnapshot(oldModel); diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index b0f582869a1..688218de024 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -16,6 +16,9 @@ export function isV2StoredVersion(version: string | undefined): boolean { export function getDashboardsApiVersion(responseFormat?: 'v1' | 'v2') { const isDashboardSceneEnabled = config.featureToggles.dashboardScene; const isKubernetesDashboardsEnabled = config.featureToggles.kubernetesDashboards; + const isV2DashboardAPIVersionEnabled = config.featureToggles.kubernetesDashboardsV2; + const isDashboardNewLayoutsEnabled = config.featureToggles.dashboardNewLayouts; + const forcingOldDashboardArch = locationService.getSearch().get('scenes') === 'false'; // Force legacy API when dashboard scene is disabled or explicitly forced @@ -32,7 +35,7 @@ export function getDashboardsApiVersion(responseFormat?: 'v1' | 'v2') { if (responseFormat === 'v1') { return 'v1'; } - if (responseFormat === 'v2') { + if (responseFormat === 'v2' || isV2DashboardAPIVersionEnabled || isDashboardNewLayoutsEnabled) { return 'v2'; } return 'unified'; From 8e4be891c507cca4519be61653ba5b31f9983570 Mon Sep 17 00:00:00 2001 From: Daniele Stefano Ferru Date: Thu, 27 Nov 2025 16:06:03 +0100 Subject: [PATCH 147/423] Provisioning: add URL and Path in setting response (#114534) * Provisioning: add URL and Path in setting response * linting * marking fields as non-required --- .../pkg/apis/provisioning/v0alpha1/settings.go | 6 ++++++ .../provisioning/v0alpha1/zz_generated.openapi.go | 14 ++++++++++++++ .../rtkq/provisioning/v0alpha1/endpoints.gen.ts | 4 ++++ pkg/registry/apis/provisioning/routes.go | 4 ++++ .../provisioning.grafana.app-v0alpha1.json | 8 ++++++++ pkg/tests/apis/provisioning/repository_test.go | 13 +++++++++++++ 6 files changed, 49 insertions(+) diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go index ee5d264fd83..3031bde77cb 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/settings.go @@ -43,6 +43,12 @@ type RepositoryView struct { // For git, this is the target branch Branch string `json:"branch,omitempty"` + // For git, this is the target URL + URL string `json:"url,omitempty"` + + // For git, this is the target path + Path string `json:"path,omitempty"` + // The supported workflows Workflows []Workflow `json:"workflows"` } diff --git a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 18c385ce59c..9a4a99d703a 100644 --- a/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/apps/provisioning/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -1690,6 +1690,20 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCa Format: "", }, }, + "url": { + SchemaProps: spec.SchemaProps{ + Description: "For git, this is the target URL", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "For git, this is the target path", + Type: []string{"string"}, + Format: "", + }, + }, "workflows": { SchemaProps: spec.SchemaProps{ Description: "The supported workflows", diff --git a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts index 67bdca12d27..7ef1fc4fc91 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts @@ -1581,6 +1581,8 @@ export type RepositoryView = { branch?: string; /** The k8s name for this repository */ name: string; + /** For git, this is the target path */ + path?: string; /** When syncing, where values are saved Possible enum values: @@ -1598,6 +1600,8 @@ export type RepositoryView = { - `"gitlab"` - `"local"` */ type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local'; + /** For git, this is the target URL */ + url?: string; /** The supported workflows */ workflows: ('branch' | 'write')[]; }; diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go index 4ffefcdfdbb..494d47d16e7 100644 --- a/pkg/registry/apis/provisioning/routes.go +++ b/pkg/registry/apis/provisioning/routes.go @@ -172,6 +172,8 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { for i, val := range all { branch := val.Branch() + url := val.URL() + path := val.Path() settings.Items[i] = provisioning.RepositoryView{ Name: val.Name, @@ -179,6 +181,8 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { Type: val.Spec.Type, Target: val.Spec.Sync.Target, Branch: branch, + URL: url, + Path: path, Workflows: val.Spec.Workflows, } } diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index bdfa6ce9490..f99b8f60738 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -4309,6 +4309,10 @@ "type": "string", "default": "" }, + "path": { + "description": "For git, this is the target path", + "type": "string" + }, "target": { "description": "When syncing, where values are saved\n\nPossible enum values:\n - `\"folder\"` Resources will be saved into a folder managed by this repository It will contain a copy of everything from the remote The folder k8s name will be the same as the repository k8s name\n - `\"instance\"` Resources are saved in the global context Only one repository may specify the `instance` target When this exists, the UI will promote writing to the instance repo rather than the grafana database (where possible)", "type": "string", @@ -4335,6 +4339,10 @@ "local" ] }, + "url": { + "description": "For git, this is the target URL", + "type": "string" + }, "workflows": { "description": "The supported workflows", "type": "array", diff --git a/pkg/tests/apis/provisioning/repository_test.go b/pkg/tests/apis/provisioning/repository_test.go index 273d78c4e99..d7850c52d0a 100644 --- a/pkg/tests/apis/provisioning/repository_test.go +++ b/pkg/tests/apis/provisioning/repository_test.go @@ -136,6 +136,19 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { return } + for _, i := range settings.Items { + switch i.Type { + case provisioning.LocalRepositoryType: + assert.Equal(collect, i.Path, helper.ProvisioningPath) + case provisioning.GitHubRepositoryType: + assert.Equal(collect, i.URL, "https://github.com/grafana/grafana-git-sync-demo") + assert.Equal(collect, i.Path, "grafana/") + default: + assert.NotEmpty(collect, i.Path) + assert.NotEmpty(collect, i.URL) + } + } + assert.ElementsMatch(collect, []provisioning.RepositoryType{ provisioning.LocalRepositoryType, provisioning.GitHubRepositoryType, From c7ea3d17cc288b8468012b81bd4d726b454dd858 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:09:33 -0500 Subject: [PATCH 148/423] Docs: Fix alias for next and latest docs (#114547) --- .../query-transform-data/sql-expressions/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md index ded072448ce..a0ba0e55fef 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md @@ -1,6 +1,6 @@ --- aliases: - - ../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/ + - ../../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/ labels: products: - cloud From cd797b678958c35aa063b04b65b48214cfd6e5ec Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Thu, 27 Nov 2025 16:37:09 +0100 Subject: [PATCH 149/423] Alerting: Refactor api_ruler_history.go to allow code re-use. (#114548) --- pkg/services/ngalert/api/api_ruler_history.go | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler_history.go b/pkg/services/ngalert/api/api_ruler_history.go index 8e32b776274..51c96b2591f 100644 --- a/pkg/services/ngalert/api/api_ruler_history.go +++ b/pkg/services/ngalert/api/api_ruler_history.go @@ -4,11 +4,14 @@ import ( "context" "fmt" "net/http" + "net/url" + "strconv" "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/eval" @@ -24,55 +27,64 @@ type HistorySrv struct { hist Historian } -const labelQueryPrefix = "labels_" - func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) response.Response { - from := c.QueryInt64("from") - to := c.QueryInt64("to") - limit := c.QueryInt("limit") - ruleUID := c.Query("ruleUID") - dashUID := c.Query("dashboardUID") - panelID := c.QueryInt64("panelID") - - previous := c.Query("previous") - if previous != "" { - _, err := eval.ParseStateString(previous) - if err != nil { - return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid previous state filter: %w", err), "") - } + query, err := ParseHistoryQuery(c.OrgID, c.SignedInUser, c.Req.URL.Query()) + if err != nil { + return ErrResp(http.StatusBadRequest, err, "") } - current := c.Query("current") - if current != "" { - _, err := eval.ParseStateString(current) - if err != nil { - return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid current state filter: %w", err), "") - } - } - - labels := make(map[string]string) - for k, v := range c.Req.URL.Query() { - if strings.HasPrefix(k, labelQueryPrefix) { - labels[k[len(labelQueryPrefix):]] = v[0] - } - } - - query := models.HistoryQuery{ - RuleUID: ruleUID, - OrgID: c.GetOrgID(), - DashboardUID: dashUID, - PanelID: panelID, - Previous: previous, - Current: current, - SignedInUser: c.SignedInUser, - From: time.Unix(from, 0), - To: time.Unix(to, 0), - Limit: limit, - Labels: labels, - } frame, err := srv.hist.Query(c.Req.Context(), query) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") } return response.JSON(http.StatusOK, frame) } + +const labelQueryPrefix = "labels_" + +// ParseHistoryQuery parses a HistoryQuery from request parameters. +func ParseHistoryQuery(orgID int64, user identity.Requester, query url.Values) (models.HistoryQuery, error) { + from, _ := strconv.ParseInt(query.Get("from"), 10, 64) + to, _ := strconv.ParseInt(query.Get("to"), 10, 64) + limit, _ := strconv.Atoi(query.Get("limit")) + ruleUID := query.Get("ruleUID") + dashUID := query.Get("dashboardUID") + panelID, _ := strconv.ParseInt(query.Get("panelID"), 10, 64) + + previous := query.Get("previous") + if previous != "" { + _, err := eval.ParseStateString(previous) + if err != nil { + return models.HistoryQuery{}, fmt.Errorf("invalid previous state filter: %w", err) + } + } + + current := query.Get("current") + if current != "" { + _, err := eval.ParseStateString(current) + if err != nil { + return models.HistoryQuery{}, fmt.Errorf("invalid current state filter: %w", err) + } + } + + labels := make(map[string]string) + for k, v := range query { + if strings.HasPrefix(k, labelQueryPrefix) { + labels[k[len(labelQueryPrefix):]] = v[0] + } + } + + return models.HistoryQuery{ + RuleUID: ruleUID, + OrgID: orgID, + DashboardUID: dashUID, + PanelID: panelID, + Previous: previous, + Current: current, + SignedInUser: user, + From: time.Unix(from, 0), + To: time.Unix(to, 0), + Limit: limit, + Labels: labels, + }, nil +} From 8daa228083a3643adad8e5d8d3effbdb2fd93f37 Mon Sep 17 00:00:00 2001 From: "Marc M." <146180665+grafakus@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:38:42 +0100 Subject: [PATCH 150/423] MetricFindValue: add missing "properties" field to the TS interface (#114486) --- packages/grafana-data/src/types/datasource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index e5bf0e10a21..538e7a051e8 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -643,6 +643,7 @@ export interface MetricFindValue { value?: string | number; group?: string; expandable?: boolean; + properties?: Record; } export interface DataSourceGetDrilldownsApplicabilityOptions { From f12cc5411d00601f76a106e97cf9dbd38d2ed0b7 Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:09:04 +0100 Subject: [PATCH 151/423] Docs: Add feature request guide for contributors (#114538) * Docs: Add feature request guide for contributors * prettier * redo self contrib section * all pretty no pity * removed duplicate li --- CONTRIBUTING.md | 2 +- contribute/README.md | 1 + contribute/create-feature-request.md | 160 +++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 contribute/create-feature-request.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c62e18c754e..f11c15a3bef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,7 +110,7 @@ If you believe you've found a security vulnerability, please read our [security ### Suggest features -If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). +If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). To learn how to write an effective feature request, refer to [Create a feature request](contribute/create-feature-request.md). We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?template=2-accessibility.md) to help us understand what we can improve. diff --git a/contribute/README.md b/contribute/README.md index b5e8ec1222d..d1855d04a04 100644 --- a/contribute/README.md +++ b/contribute/README.md @@ -5,6 +5,7 @@ We're excited that you're considering making a contribution to the Grafana proje These are some good resources to explore for developers: - [Create a pull request](create-pull-request.md) +- [Create a feature request](create-feature-request.md) - [Developer guide](developer-guide.md) - [Triage issues](triage-issues.md) - [Merge a pull request](merge-pull-request.md) diff --git a/contribute/create-feature-request.md b/contribute/create-feature-request.md new file mode 100644 index 00000000000..5ee47bc66e3 --- /dev/null +++ b/contribute/create-feature-request.md @@ -0,0 +1,160 @@ +# Create a feature request + +Feature requests help us understand what you need from Grafana. This document guides you through writing effective feature requests that help maintainers understand your needs and prioritize improvements. + +## Before you begin + +We're excited to hear your ideas! Before you submit a feature request, consider these resources: + +- Read the [Code of Conduct](../CODE_OF_CONDUCT.md) to understand our community guidelines. +- Search [existing feature requests](https://github.com/grafana/grafana/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Ffeature-request) to see if someone already suggested something similar. +- Discuss your idea in the [Grafana community forums](https://community.grafana.com/) to refine it and gather feedback. + +## Your first feature request + +When you're ready to submit a feature request, use the [feature request template](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). The template has three sections that help maintainers understand what you need and why. + +Here's an [example of how all three sections work together in an actual feature request](https://github.com/grafana/grafana/issues/105298) from the Grafana community. We'll analyze each section based on this example feature request. + +### Why is this needed + +This section describes the real problem or limitation you're facing. + +Explain what's difficult, inefficient, or impossible with the current implementation. Focus on the problem rather than proposing a solution. This helps maintainers understand your use case and potentially find better solutions. + +**What to include:** + +- The specific problem or pain point you're experiencing +- How the current behavior falls short for your workflow +- Why this matters to you and your work +- A concrete example that clarifies the issue (optional but helpful) + +**What to avoid:** + +- Jumping directly to the solution (save that for the next section) +- Vague statements like "it would be nice if..." +- Assuming maintainers know your context or workflow + +**Example of a strong answer:** + +``` +When using a datasource variable in dashboards and using the "Export" feature in a dashboard, +this will automatically create an input for the datasource(s) being used, but it will also +effectively override the use of the datasource variable in all panels. + +This makes a confusing +experience when importing the dashboard, because users are prompted for an input, but the +selected datasource won't be reflected in the datasource variable, and any changes to the +datasource variable will not have any effect on the dashboard. +``` + +**Example of a weak answer:** + +``` +Dashboard export doesn't work well with variables. +``` + +The first example clearly explains what's broken, why it's confusing, and what the specific consequences are. The second example is too vague and doesn't explain the actual problem. + +### What would you like to be added + +This section describes what you want Grafana to do differently. + +Be specific and concrete about the expected behavior. If you're suggesting a UI change, describe the interaction or include a screenshot or sketch. If it's data or API related, provide an example query or expected output. + +**What to include:** + +- Exactly what behavior you expect +- How the feature should work in practice +- Examples, screenshots, or code snippets that illustrate your idea +- Expected output or results + +**What to avoid:** + +- Vague or abstract descriptions +- Multiple unrelated features in one request (create separate requests instead) +- Implementation details unless they're critical to your request + +**Example of a strong answer:** + +``` +Ideal behavior here would be that when using the export feature, either: + +1. No inputs section is created for datasource types that are used as datasource variables. +2. IF an input is created, it should only be used to replace the currently selected value of + the datasource variable, rather than override the datasource in panels. +``` + +**Example of a weak answer:** + +``` +Fix the dashboard export feature. +``` + +The first example provides clear, actionable options for how the feature should work. The second example is too vague and doesn't specify what the fix should do. + +### Who is this feature for? + +This section describes who benefits from this feature and in what context. + +Help maintainers understand the scope and impact of your request. Be specific about user types, workflows, or scenarios where this feature matters. + +**What to include:** + +- The type of user who needs this (for example, Tempo users, dashboard editors, plugin developers) +- Whether this affects all Grafana users or only those using specific features or data sources +- The workflow or use case this feature improves (optional but helpful) + +**What to avoid:** + +- Saying "everyone" without clarifying who actually needs it +- Being overly narrow if the feature has broader appeal + +**Example of a strong answer:** + +``` +Any Grafana Dashboard users or authors that use datasource variables. +``` + +**Example of a weak answer:** + +``` +Dashboard users. +``` + +The first example identifies the specific users and the feature they use (datasource variables). The second example is too generic and doesn't clarify which users or workflow are affected. + +## Best practices for feature requests + +Follow these guidelines to increase the chances of your feature request being accepted: + +### Keep it focused + +Request one feature at a time. If you have multiple ideas, create separate feature requests for each one. This makes it easier to discuss, prioritize, and implement each feature independently. + +### Research first + +Before submitting, search for similar requests. If you find an existing request that's close to your idea, add your use case and context to that discussion instead of creating a duplicate. + +### Provide context + +The more context you provide, the better maintainers can understand your needs. Include: + +- Your environment or setup (which data sources, plugins, or features you're using) +- Your workflow or process +- Why this matters to you +- Any workarounds you've tried + +### Be open to alternatives + +Maintainers might suggest different approaches to solve your problem. Be open to these alternatives as they might be easier to implement or more maintainable in the long term. + +### Stay engaged + +After submitting your feature request, monitor the discussion. Answer questions from maintainers and provide clarification when needed. This helps move your request forward. + +## Contributing the feature yourself + +If you want to implement the feature yourself, feel free to create a pull request following the [pull request guidelines](create-pull-request.md). + +We welcome community contributions and appreciate your help making Grafana better! From ba58506ffdd78d13cdf4d09ddc51ec0fc3f9eb2b Mon Sep 17 00:00:00 2001 From: Alan Martin <53958929+Alan-eMartin@users.noreply.github.com> Date: Thu, 27 Nov 2025 11:12:26 -0500 Subject: [PATCH 152/423] Notifications: Prevent triggering duplicate notifications (#114497) * fix(notifications): prevent event listener re-registration on route changes * refactor(notifications): rename alert handling functions for clarity * refactor(notifications): simplify alert handling by using spread operator for payloads * refactor(events): address feedback - update LegacyEmitter and LegacyEventHandler interfaces for improved type safety * fix(events): ensure event handlers handle undefined events gracefully in tests * test(notifications): add tests for event listener registration and cleanup in AppNotificationList --- .../grafana-data/src/events/EventBus.test.ts | 10 +++- packages/grafana-data/src/events/types.ts | 4 +- .../AppNotificationList.test.tsx | 43 ++++++++++++++ .../AppNotifications/AppNotificationList.tsx | 57 +++++++++++++++---- 4 files changed, 98 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/src/events/EventBus.test.ts b/packages/grafana-data/src/events/EventBus.test.ts index 6de48b5d54e..3708b47d78b 100644 --- a/packages/grafana-data/src/events/EventBus.test.ts +++ b/packages/grafana-data/src/events/EventBus.test.ts @@ -91,8 +91,10 @@ describe('EventBus', () => { it('Supports legacy events', () => { const bus = new EventBusSrv(); const events: LegacyEventPayload[] = []; - const handler = (event: LegacyEventPayload) => { - events.push(event); + const handler = (event?: LegacyEventPayload) => { + if (event) { + events.push(event); + } }; bus.on(legacyEvent, handler); @@ -111,7 +113,9 @@ describe('EventBus', () => { const newEvents: AlertSuccessEvent[] = []; bus.on(legacyEvent, (event) => { - legacyEvents.push(event); + if (event) { + legacyEvents.push(event); + } }); bus.subscribe(AlertSuccessEvent, (event) => { diff --git a/packages/grafana-data/src/events/types.ts b/packages/grafana-data/src/events/types.ts index 514b980d1b4..ce5499d00ed 100644 --- a/packages/grafana-data/src/events/types.ts +++ b/packages/grafana-data/src/events/types.ts @@ -133,12 +133,12 @@ export interface LegacyEmitter { /** * @deprecated use $on */ - off(event: AppEvent | string, handler: (payload?: T) => void): void; + off(event: AppEvent | string, handler: LegacyEventHandler): void; } /** @public */ export interface LegacyEventHandler { - (payload: T): void; + (payload?: T): void; wrapper?: (event: BusEvent) => void; } diff --git a/public/app/core/components/AppNotifications/AppNotificationList.test.tsx b/public/app/core/components/AppNotifications/AppNotificationList.test.tsx index 61500e0cd4c..3c3eb04a44d 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.test.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.test.tsx @@ -98,6 +98,49 @@ describe('AppNotificationList', () => { }); }); + describe('Event listener cleanup', () => { + let onSpy: jest.SpyInstance; + let offSpy: jest.SpyInstance; + + const eventTypes = [AppEvents.alertWarning, AppEvents.alertSuccess, AppEvents.alertError, AppEvents.alertInfo]; + + beforeEach(() => { + onSpy = jest.spyOn(appEvents, 'on'); + offSpy = jest.spyOn(appEvents, 'off'); + }); + + afterEach(() => { + onSpy.mockRestore(); + offSpy.mockRestore(); + }); + + it('should register event listeners on mount', () => { + renderWithContext(); + + expect(onSpy).toHaveBeenCalledTimes(4); + eventTypes.forEach((eventType) => { + expect(onSpy).toHaveBeenCalledWith(eventType, expect.any(Function)); + }); + }); + + it('should unregister event listeners on unmount', () => { + const { unmount } = renderWithContext(); + + const handlers = eventTypes.map((eventType) => { + const handler = onSpy.mock.calls.find((call) => call[0] === eventType)?.[1]; + expect(handler).toBeDefined(); + return { eventType, handler }; + }); + + unmount(); + + expect(offSpy).toHaveBeenCalledTimes(4); + handlers.forEach(({ eventType, handler }) => { + expect(offSpy).toHaveBeenCalledWith(eventType, handler); + }); + }); + }); + describe('Edge cases', () => { it('should show error on dashboard page with uid and slug', async () => { renderWithContext(undefined, '/d/test-uid/test-slug'); diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index 29cc9a4b0e3..8d5a1d3161e 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; -import { AlertErrorPayload, AppEvents, GrafanaTheme2 } from '@grafana/data'; +import { AlertErrorPayload, AlertPayload, AppEvents, GrafanaTheme2 } from '@grafana/data'; import { useStyles2, Stack } from '@grafana/ui'; import { notifyApp, hideAppNotification } from 'app/core/actions'; import { appEvents } from 'app/core/app_events'; @@ -26,25 +26,60 @@ export function AppNotificationList() { const { chrome } = useGrafana(); const location = useLocation(); + // Store location ref to avoid re-registering listeners on route changes + const locationRef = useRef(location); + useEffect(() => { + locationRef.current = location; + }, [location]); + useEffect(() => { // Suppress error notifications in kiosk mode on dashboards. // Kiosk mode is typically used for TV displays which are non-interactive. // Backend errors like "Failed to fetch" cannot be dismissed and would remain visible, // degrading the viewing experience. Other notification types (success, warning, info) // are still shown as they indicate successful operations or important information. - const handleErrorAlert = (payload: AlertErrorPayload) => { - const isKioskDashboard = chrome.state.getValue().kioskMode && location.pathname.startsWith('/d/'); - - if (!isKioskDashboard) { - dispatch(notifyApp(createErrorNotification(...payload))); + const handleErrorAlert = (payload?: AlertErrorPayload) => { + const isKioskDashboard = chrome.state.getValue().kioskMode && locationRef.current.pathname.startsWith('/d/'); + if (isKioskDashboard || !payload) { + return; } + dispatch(notifyApp(createErrorNotification(...payload))); }; - appEvents.on(AppEvents.alertWarning, (payload) => dispatch(notifyApp(createWarningNotification(...payload)))); - appEvents.on(AppEvents.alertSuccess, (payload) => dispatch(notifyApp(createSuccessNotification(...payload)))); + const handleWarningAlert = (payload?: AlertPayload) => { + if (!payload) { + return; + } + dispatch(notifyApp(createWarningNotification(...payload))); + }; + + const handleSuccessAlert = (payload?: AlertPayload) => { + if (!payload) { + return; + } + dispatch(notifyApp(createSuccessNotification(...payload))); + }; + + const handleInfoAlert = (payload?: AlertPayload) => { + if (!payload) { + return; + } + dispatch(notifyApp(createInfoNotification(...payload))); + }; + + appEvents.on(AppEvents.alertWarning, handleWarningAlert); + appEvents.on(AppEvents.alertSuccess, handleSuccessAlert); appEvents.on(AppEvents.alertError, handleErrorAlert); - appEvents.on(AppEvents.alertInfo, (payload) => dispatch(notifyApp(createInfoNotification(...payload)))); - }, [dispatch, chrome, location.pathname]); + appEvents.on(AppEvents.alertInfo, handleInfoAlert); + + return () => { + // Unsubscribe from events on unmount to avoid memory leaks + appEvents.off(AppEvents.alertWarning, handleWarningAlert); + appEvents.off(AppEvents.alertSuccess, handleSuccessAlert); + appEvents.off(AppEvents.alertError, handleErrorAlert); + appEvents.off(AppEvents.alertInfo, handleInfoAlert); + }; + }, [dispatch, chrome]); const onClearAppNotification = (id: string) => { dispatch(hideAppNotification(id)); From 7b8191ba4264216243a759533ca77e422e978b04 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Thu, 27 Nov 2025 17:14:16 +0100 Subject: [PATCH 153/423] Alerting: Add kubernetesAlertingHistorian feature toggle. (#114551) --- .../grafana-data/src/types/featureToggles.gen.ts | 4 ++++ pkg/services/featuremgmt/registry.go | 7 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 15 ++++++++++++++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 900f86ba33f..35cccd78375 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1193,4 +1193,8 @@ export interface FeatureToggles { * @default false */ rudderstackUpgrade?: boolean; + /** + * Adds support for Kubernetes alerting historian APIs + */ + kubernetesAlertingHistorian?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4fe92fe90d6..b4b34f578fa 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1970,6 +1970,13 @@ var ( RequiresRestart: false, HideFromDocs: false, }, + { + Name: "kubernetesAlertingHistorian", + Description: "Adds support for Kubernetes alerting historian APIs", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + RequiresRestart: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9caa916283d..29af9d445f7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -267,3 +267,4 @@ transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true 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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 89a171a76ba..3183c587efd 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -761,4 +761,8 @@ const ( // FlagAwsDatasourcesHttpProxy // Enables http proxy settings for aws datasources FlagAwsDatasourcesHttpProxy = "awsDatasourcesHttpProxy" + + // FlagKubernetesAlertingHistorian + // Adds support for Kubernetes alerting historian APIs + FlagKubernetesAlertingHistorian = "kubernetesAlertingHistorian" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index aeaf5a408af..fd29fa544ee 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1790,6 +1790,19 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "kubernetesAlertingHistorian", + "resourceVersion": "1764257713773", + "creationTimestamp": "2025-11-27T15:35:13Z" + }, + "spec": { + "description": "Adds support for Kubernetes alerting historian APIs", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true + } + }, { "metadata": { "name": "kubernetesAlertingRules", @@ -3617,4 +3630,4 @@ } } ] -} +} \ No newline at end of file From df2f5286121fc9e6cc8fbc56465e4b5f33d2397f Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Thu, 27 Nov 2025 10:29:16 -0600 Subject: [PATCH 154/423] Unified Storage: Adds overrides service to resource server (#113794) * first pass of adding quotas service resource server * passes prom reg as param init quota service as part of server params * init quota service as part of server params * adds config and only creates quota service when overrides file path is defined * when quota service enabled, check quota on create and log result * update log message * adds tests for quota service * adds tests for config reloading when the file changes * fix linter errors * fix comment * use startAndAwaitRunning * Simplifies quotas service. Call manager.GetConfig() when getting quota instead of watching for changes. * adds tracing to quotas service * adds nsr attributes to traces when getting quotas and resource stats * update comment * update comment remove check for nil overrides since it will (should) never happen * fix linter error * refactors naming to overrides service checks quotas in separate function * fix quotas naming * fixes more quotas -> overrides naming * use logger from ctx * linter - remove trailing whitespace * log FromContext() when checking quotas * adds events to spans instead of create new spans updates tenant -> namespace naming few other minor fixes --- pkg/setting/setting.go | 2 + pkg/setting/setting_unified_storage.go | 4 + pkg/storage/unified/client.go | 13 + pkg/storage/unified/resource/quotas.go | 127 ++++++ pkg/storage/unified/resource/quotas_test.go | 427 ++++++++++++++++++++ pkg/storage/unified/resource/server.go | 80 +++- pkg/storage/unified/sql/backend.go | 7 +- pkg/storage/unified/sql/server.go | 28 +- pkg/storage/unified/sql/service.go | 12 + 9 files changed, 673 insertions(+), 27 deletions(-) create mode 100644 pkg/storage/unified/resource/quotas.go create mode 100644 pkg/storage/unified/resource/quotas_test.go diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 470b6910751..f6c0b3d3f19 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -615,6 +615,8 @@ type Cfg struct { HttpsSkipVerify bool ResourceServerJoinRingTimeout time.Duration EnableSearch bool + OverridesFilePath string + OverridesReloadInterval time.Duration // Secrets Management SecretsManagement SecretsManagerSettings diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 29cb5d0270a..3623228fe8b 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -94,6 +94,10 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.HttpsSkipVerify = section.Key("https_skip_verify").MustBool(false) cfg.ResourceServerJoinRingTimeout = section.Key("resource_server_join_ring_timeout").MustDuration(10 * time.Second) + // quotas/limits config + cfg.OverridesFilePath = section.Key("overrides_path").String() + cfg.OverridesReloadInterval = section.Key("overrides_reload_period").MustDuration(30 * time.Second) + cfg.MaxFileIndexAge = section.Key("max_file_index_age").MustDuration(0) cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("") } diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 07165937199..45130c8a6a6 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -211,6 +211,19 @@ func newClient(opts options.StorageOptions, serverOptions.QOSQueue = queue } + // only enable if an overrides file path is provided + if cfg.OverridesFilePath != "" { + overridesSvc, err := resource.NewOverridesService(ctx, cfg.Logger, reg, tracer, resource.ReloadOptions{ + FilePath: cfg.OverridesFilePath, + ReloadPeriod: cfg.OverridesReloadInterval, + }) + if err != nil { + return nil, err + } + + serverOptions.OverridesService = overridesSvc + } + server, err := sql.NewResourceServer(serverOptions) if err != nil { return nil, err diff --git a/pkg/storage/unified/resource/quotas.go b/pkg/storage/unified/resource/quotas.go new file mode 100644 index 00000000000..d956a6da017 --- /dev/null +++ b/pkg/storage/unified/resource/quotas.go @@ -0,0 +1,127 @@ +package resource + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/grafana/dskit/runtimeconfig" + "github.com/grafana/dskit/services" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" + "go.yaml.in/yaml/v3" +) + +const DEFAULT_RESOURCE_LIMIT = 1000 + +type OverridesService struct { + manager *runtimeconfig.Manager + logger log.Logger + tracer trace.Tracer +} + +type ReloadOptions struct { + FilePath string + ReloadPeriod time.Duration +} + +// ResourceQuota represents quota limits for a specific resource +type ResourceQuota struct { + Limit int `yaml:"limit"` +} + +// NamespaceOverrides represents all overrides for a tenant +type NamespaceOverrides struct { + Quotas map[string]ResourceQuota `yaml:"quotas"` +} + +// Overrides represents the entire overrides configuration file +type Overrides struct { + Namespaces map[string]NamespaceOverrides +} + +/* +This service loads overrides (currently just quotas) from a YAML file with the following yaml structure: + +"123": + + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 1500 +*/ +func NewOverridesService(_ context.Context, logger log.Logger, reg prometheus.Registerer, tracer trace.Tracer, opts ReloadOptions) (*OverridesService, error) { + // shouldn't be empty since we use file path existence to determine if we should enable the service + if opts.FilePath == "" { + return nil, fmt.Errorf("overrides file path is required") + } + if opts.ReloadPeriod == 0 { + opts.ReloadPeriod = time.Second * 30 + } + + // Check if file exists + if _, err := os.Stat(opts.FilePath); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("overrides file does not exist: %s", opts.FilePath) + } + return nil, fmt.Errorf("failed to stat overrides file: %w", err) + } + + config := runtimeconfig.Config{ + ReloadPeriod: opts.ReloadPeriod, + LoadPath: []string{opts.FilePath}, + Loader: func(r io.Reader) (interface{}, error) { + var tenants map[string]NamespaceOverrides + decoder := yaml.NewDecoder(r) + if err := decoder.Decode(&tenants); err != nil { + return nil, err + } + return &Overrides{Namespaces: tenants}, nil + }, + } + + manager, err := runtimeconfig.New(config, "tenant-overrides", reg, logger) + if err != nil { + return nil, err + } + + return &OverridesService{ + manager: manager, + logger: logger, + tracer: tracer, + }, nil +} + +func (q *OverridesService) init(ctx context.Context) error { + return services.StartAndAwaitRunning(ctx, q.manager) +} + +func (q *OverridesService) stop(ctx context.Context) error { + return services.StopAndAwaitTerminated(ctx, q.manager) +} + +func (q *OverridesService) GetQuota(_ context.Context, nsr NamespacedResource) (ResourceQuota, error) { + if nsr.Namespace == "" || nsr.Resource == "" || nsr.Group == "" { + return ResourceQuota{}, fmt.Errorf("invalid namespaced resource: %+v", nsr) + } + + overrides, ok := q.manager.GetConfig().(*Overrides) + if !ok { + return ResourceQuota{}, fmt.Errorf("failed to get quota overrides from config manager") + } + + tenantId := strings.TrimPrefix(nsr.Namespace, "stacks-") + groupResource := nsr.Group + "/" + nsr.Resource + if tenantOverrides, ok := overrides.Namespaces[tenantId]; ok { + if resourceQuota, ok := tenantOverrides.Quotas[groupResource]; ok { + return resourceQuota, nil + } + } + + return ResourceQuota{Limit: DEFAULT_RESOURCE_LIMIT}, nil +} diff --git a/pkg/storage/unified/resource/quotas_test.go b/pkg/storage/unified/resource/quotas_test.go new file mode 100644 index 00000000000..97f9f355af6 --- /dev/null +++ b/pkg/storage/unified/resource/quotas_test.go @@ -0,0 +1,427 @@ +package resource + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewQuotaService(t *testing.T) { + tests := []struct { + name string + opts ReloadOptions + setupFile func(t *testing.T) string + expectError bool + errorMsg string + }{ + { + name: "success with valid file", + opts: ReloadOptions{}, + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + expectError: false, + }, + { + name: "success with custom reload period", + opts: ReloadOptions{ + ReloadPeriod: time.Minute, + }, + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + require.NoError(t, os.WriteFile(tmpFile, []byte{}, 0644)) + return tmpFile + }, + expectError: false, + }, + { + name: "error when file path is empty", + opts: ReloadOptions{ + FilePath: "", + }, + setupFile: func(t *testing.T) string { return "" }, + expectError: true, + errorMsg: "overrides file path is required", + }, + { + name: "error when file does not exist", + opts: ReloadOptions{ + FilePath: "/nonexistent/path/overrides.yaml", + }, + setupFile: func(t *testing.T) string { return "/nonexistent/path/overrides.yaml" }, + expectError: true, + errorMsg: "overrides file does not exist", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + logger := log.NewNopLogger() + reg := prometheus.NewRegistry() + tcr := tracing.NewNoopTracerService() + + filePath := tt.setupFile(t) + if filePath != "" && tt.opts.FilePath == "" { + tt.opts.FilePath = filePath + } + + service, err := NewOverridesService(ctx, logger, reg, tcr, tt.opts) + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + assert.Nil(t, service) + } else { + require.NoError(t, err) + assert.NotNil(t, service) + assert.NotNil(t, service.manager) + assert.NotNil(t, service.logger) + } + }) + } +} + +func TestQuotaService_ConfigReload(t *testing.T) { + ctx := context.Background() + logger := log.NewNopLogger() + reg := prometheus.NewRegistry() + tcr := tracing.NewNoopTracerService() + + // Create a temporary config file + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + initialConfig := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(initialConfig), 0644)) + + // Create service with a very short reload period + service, err := NewOverridesService(ctx, logger, reg, tcr, ReloadOptions{ + FilePath: tmpFile, + ReloadPeriod: 100 * time.Millisecond, // Very short reload period for testing + }) + require.NoError(t, err) + require.NotNil(t, service) + + // Initialize the service + err = service.init(ctx) + require.NoError(t, err) + defer func(service *OverridesService, ctx context.Context) { + err := service.stop(ctx) + require.NoError(t, err) + }(service, ctx) + + // Verify initial config + nsr := NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.dashboard.app", + Resource: "dashboards", + } + quota, err := service.GetQuota(ctx, nsr) + require.NoError(t, err) + assert.Equal(t, 1500, quota.Limit, "initial quota should be 1500") + + // Update the config file with new values + updatedConfig := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 2500 +"456": + quotas: + grafana.folder.app/folders: + limit: 3000 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(updatedConfig), 0644)) + + // Wait for the config to be reloaded (wait longer than reload period) + time.Sleep(500 * time.Millisecond) + + // Verify the config was updated for existing tenant + quota, err = service.GetQuota(ctx, nsr) + require.NoError(t, err) + assert.Equal(t, 2500, quota.Limit, "quota should be updated to 2500") + + // Verify new tenant config is also loaded + nsr2 := NamespacedResource{ + Namespace: "stacks-456", + Group: "grafana.folder.app", + Resource: "folders", + } + quota2, err := service.GetQuota(ctx, nsr2) + require.NoError(t, err) + assert.Equal(t, 3000, quota2.Limit, "new tenant quota should be 3000") +} + +func TestQuotaService_GetQuota(t *testing.T) { + tests := []struct { + name string + setupFile func(t *testing.T) string + nsr NamespacedResource + expectedLimit int + expectError bool + errorMsg string + description string + }{ + { + name: "returns custom quota for matching tenant and resource", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.dashboard.app", + Resource: "dashboards", + }, + expectedLimit: 1500, + expectError: false, + description: "should return custom limit for matching tenant", + }, + { + name: "returns default quota when tenant not found", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-456", + Group: "grafana.dashboard.app", + Resource: "dashboards", + }, + expectedLimit: DEFAULT_RESOURCE_LIMIT, + expectError: false, + description: "should return default limit when tenant not found", + }, + { + name: "returns default quota when resource not found", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.folder.app", + Resource: "folders", + }, + expectedLimit: DEFAULT_RESOURCE_LIMIT, + expectError: false, + description: "should return default limit when resource not found", + }, + { + name: "handles namespace without stacks- prefix", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "123", + Group: "grafana.dashboard.app", + Resource: "dashboards", + }, + expectedLimit: 1500, + expectError: false, + description: "should handle namespace without stacks- prefix", + }, + { + name: "returns default quota when config is empty", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := "" + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.dashboard.app", + Resource: "dashboards", + }, + expectedLimit: DEFAULT_RESOURCE_LIMIT, + expectError: false, + description: "should return default limit when config is empty", + }, + { + name: "handles multiple resources for same tenant", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.folder.app", + Resource: "folders", + }, + expectedLimit: 2500, + expectError: false, + description: "should return correct limit for specific resource", + }, + { + name: "returns error when namespace is empty", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "", + Group: "grafana.dashboard.app", + Resource: "dashboards", + }, + expectError: true, + errorMsg: "invalid namespaced resource", + description: "should return error when namespace is empty", + }, + { + name: "returns error when group is empty", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "", + Resource: "dashboards", + }, + expectError: true, + errorMsg: "invalid namespaced resource", + description: "should return error when group is empty", + }, + { + name: "returns error when resource is empty", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "stacks-123", + Group: "grafana.dashboard.app", + Resource: "", + }, + expectError: true, + errorMsg: "invalid namespaced resource", + description: "should return error when resource is empty", + }, + { + name: "returns error when all fields are empty", + setupFile: func(t *testing.T) string { + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `"123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + return tmpFile + }, + nsr: NamespacedResource{ + Namespace: "", + Group: "", + Resource: "", + }, + expectError: true, + errorMsg: "invalid namespaced resource", + description: "should return error when all fields are empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + logger := log.NewNopLogger() + reg := prometheus.NewRegistry() + tcr := tracing.NewNoopTracerService() + opts := ReloadOptions{ + FilePath: tt.setupFile(t), + } + + service, err := NewOverridesService(ctx, logger, reg, tcr, opts) + require.NoError(t, err, "failed to create quota service") + err = service.init(ctx) + require.NoError(t, err, "failed to initialize quota service") + + quota, err := service.GetQuota(ctx, tt.nsr) + + if tt.expectError { + require.Error(t, err, tt.description) + assert.Contains(t, err.Error(), tt.errorMsg, tt.description) + assert.Equal(t, ResourceQuota{}, quota, "should return empty quota on error") + } else { + require.NoError(t, err, tt.description) + assert.Equal(t, tt.expectedLimit, quota.Limit, tt.description) + } + }) + } +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 4c0adddf3c8..dd091c6e2d4 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -14,6 +14,8 @@ import ( "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -220,6 +222,9 @@ type ResourceServerOptions struct { // Search options Search SearchOptions + // Quota service + OverridesService *OverridesService + // Diagnostics Diagnostics resourcepb.DiagnosticsServer @@ -342,6 +347,7 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { reg: opts.Reg, queue: opts.QOSQueue, queueConfig: opts.QOSConfig, + overridesService: opts.OverridesService, artificialSuccessfulWriteDelay: opts.Search.IndexMinUpdateInterval, } @@ -366,19 +372,20 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { var _ ResourceServer = &server{} type server struct { - log log.Logger - backend StorageBackend - blob BlobSupport - secure secrets.InlineSecureValueSupport - search *searchSupport - diagnostics resourcepb.DiagnosticsServer - access claims.AccessClient - writeHooks WriteAccessHooks - lifecycle LifecycleHooks - now func() int64 - mostRecentRV atomic.Int64 // The most recent resource version seen by the server - storageMetrics *StorageMetrics - indexMetrics *BleveIndexMetrics + log log.Logger + backend StorageBackend + blob BlobSupport + secure secrets.InlineSecureValueSupport + search *searchSupport + diagnostics resourcepb.DiagnosticsServer + access claims.AccessClient + writeHooks WriteAccessHooks + lifecycle LifecycleHooks + now func() int64 + mostRecentRV atomic.Int64 // The most recent resource version seen by the server + storageMetrics *StorageMetrics + indexMetrics *BleveIndexMetrics + overridesService *OverridesService // Background watch task -- this has permissions for everything ctx context.Context @@ -411,6 +418,11 @@ func (s *server) Init(ctx context.Context) error { } } + // initialize tenant overrides service + if s.initErr == nil && s.overridesService != nil { + s.initErr = s.overridesService.init(ctx) + } + // initialize the search index if s.initErr == nil && s.search != nil { s.initErr = s.search.init(ctx) @@ -444,6 +456,13 @@ func (s *server) Stop(ctx context.Context) error { s.search.stop() } + if s.overridesService != nil { + if err := s.overridesService.stop(ctx); err != nil { + stopFailed = true + s.initErr = fmt.Errorf("service stopeed with error: %w", err) + } + } + // Stops the streaming s.cancel() @@ -647,6 +666,13 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re ctx, span := tracer.Start(ctx, "resource.server.Create") defer span.End() + // check quotas and log for now + s.checkQuota(ctx, NamespacedResource{ + Namespace: req.Key.Namespace, + Group: req.Key.Group, + Resource: req.Key.Resource, + }) + if r := verifyRequestKey(req.Key); r != nil { return nil, fmt.Errorf("invalid request key: %s", r.Message) } @@ -1549,3 +1575,31 @@ func (s *server) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildInde return s.search.RebuildIndexes(ctx, req) } + +func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { + span := trace.SpanFromContext(ctx) + span.AddEvent("checkQuota", trace.WithAttributes( + attribute.String("namespace", nsr.Namespace), + attribute.String("group", nsr.Group), + attribute.String("resource", nsr.Resource), + )) + + if s.overridesService == nil { + return + } + + quota, err := s.overridesService.GetQuota(ctx, nsr) + if err != nil { + s.log.FromContext(ctx).Error("failed to get quota for resource", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err) + return + } + + stats, err := s.backend.GetResourceStats(ctx, nsr, 0) + if err != nil { + s.log.FromContext(ctx).Error("failed to get resource stats for quota checking", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err) + return + } + if len(stats) > 0 && stats[0].Count >= int64(quota.Limit) { + s.log.FromContext(ctx).Info("Quota exceeded on create", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "quota", quota.Limit, "count", stats[0].Count, "stats_resource", stats[0].Resource) + } +} diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 8de43f318b0..0abc2fd7329 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -14,6 +14,7 @@ import ( "github.com/jackc/pgx/v5/pgconn" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "go.uber.org/atomic" @@ -263,7 +264,11 @@ func (b *backend) Stop(_ context.Context) error { // GetResourceStats implements Backend. func (b *backend) GetResourceStats(ctx context.Context, nsr resource.NamespacedResource, minCount int) ([]resource.ResourceStats, error) { - ctx, span := b.tracer.Start(ctx, tracePrefix+"GetResourceStats") + ctx, span := b.tracer.Start(ctx, tracePrefix+"GetResourceStats", trace.WithAttributes( + attribute.String("namespace", nsr.Namespace), + attribute.String("group", nsr.Group), + attribute.String("resource", nsr.Resource), + )) defer span.End() req := &sqlStatsRequest{ diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 1c2b02d8637..f91baa7a695 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -30,19 +30,20 @@ type QOSEnqueueDequeuer interface { // ServerOptions contains the options for creating a new ResourceServer type ServerOptions struct { - Backend resource.StorageBackend - DB infraDB.DB - Cfg *setting.Cfg - Tracer trace.Tracer - Reg prometheus.Registerer - AccessClient types.AccessClient - SearchOptions resource.SearchOptions - StorageMetrics *resource.StorageMetrics - IndexMetrics *resource.BleveIndexMetrics - Features featuremgmt.FeatureToggles - QOSQueue QOSEnqueueDequeuer - SecureValues secrets.InlineSecureValueSupport - OwnsIndexFn func(key resource.NamespacedResource) (bool, error) + Backend resource.StorageBackend + OverridesService *resource.OverridesService + DB infraDB.DB + Cfg *setting.Cfg + Tracer trace.Tracer + Reg prometheus.Registerer + AccessClient types.AccessClient + SearchOptions resource.SearchOptions + StorageMetrics *resource.StorageMetrics + IndexMetrics *resource.BleveIndexMetrics + Features featuremgmt.FeatureToggles + QOSQueue QOSEnqueueDequeuer + SecureValues secrets.InlineSecureValueSupport + OwnsIndexFn func(key resource.NamespacedResource) (bool, error) } func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) { @@ -119,6 +120,7 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) { serverOptions.IndexMetrics = opts.IndexMetrics serverOptions.QOSQueue = opts.QOSQueue serverOptions.OwnsIndexFn = opts.OwnsIndexFn + serverOptions.OverridesService = opts.OverridesService return resource.NewResourceServer(serverOptions) } diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 00e2ae73750..334c2dfee76 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -279,6 +279,18 @@ func (s *service) starting(ctx context.Context) error { QOSQueue: s.queue, OwnsIndexFn: s.OwnsIndex, } + + if s.cfg.OverridesFilePath != "" { + overridesSvc, err := resource.NewOverridesService(context.Background(), s.log, s.reg, s.tracing, resource.ReloadOptions{ + FilePath: s.cfg.OverridesFilePath, + ReloadPeriod: s.cfg.OverridesReloadInterval, + }) + if err != nil { + return err + } + serverOptions.OverridesService = overridesSvc + } + server, err := NewResourceServer(serverOptions) if err != nil { return err From eea50c8e9b3841fdcca73519ac399318a627d9ad Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Thu, 27 Nov 2025 08:58:43 -0800 Subject: [PATCH 155/423] Elasticsearch: Update codeowner for elasticsearchImprovedParsing feature toggle (#114556) --- pkg/services/featuremgmt/registry.go | 2 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b4b34f578fa..950476d1a8f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1322,7 +1322,7 @@ var ( Name: "elasticsearchImprovedParsing", Description: "Enables less memory intensive Elasticsearch result parsing", Stage: FeatureStageExperimental, - Owner: awsDatasourcesSquad, + Owner: grafanaPartnerPluginsSquad, }, { Name: "datasourceConnectionsTab", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 29af9d445f7..a7423d583ab 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -182,7 +182,7 @@ k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false grafanaAdvisor,privatePreview,@grafana/plugins-platform-backend,false,false,false -elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false +elasticsearchImprovedParsing,experimental,@grafana/partner-datasources,false,false,false datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false newLogsPanel,GA,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index fd29fa544ee..59ad50e79e2 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1214,13 +1214,16 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1763734583253", - "creationTimestamp": "2025-01-15T17:05:54Z" + "resourceVersion": "1764260048941", + "creationTimestamp": "2025-01-15T17:05:54Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-27 16:14:08.941633 +0000 UTC" + } }, "spec": { "description": "Enables less memory intensive Elasticsearch result parsing", "stage": "experimental", - "codeowner": "@grafana/aws-datasources" + "codeowner": "@grafana/partner-datasources" } }, { From 5626dc50f86039aeab62bae9ef764c9bcc6244df Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Thu, 27 Nov 2025 18:42:01 +0100 Subject: [PATCH 156/423] feat(unified-storage): Add adaptive backoff to event notifier polling (#114401) * use exponential backoff in notifier * Enhance BadgerDB configuration in REST options with memory table size and number of memtables * Enhance BadgerDB configuration in REST options by adding value threshold for LSM vs value log storage --- pkg/storage/unified/apistore/restoptions.go | 3 ++ pkg/storage/unified/resource/notifier.go | 40 +++++++++++++++---- pkg/storage/unified/resource/notifier_test.go | 16 +++++--- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/pkg/storage/unified/apistore/restoptions.go b/pkg/storage/unified/apistore/restoptions.go index b48fbc6deaf..d880c663f29 100644 --- a/pkg/storage/unified/apistore/restoptions.go +++ b/pkg/storage/unified/apistore/restoptions.go @@ -54,6 +54,9 @@ func NewRESTOptionsGetterMemory(originalStorageConfig storagebackend.Config, sec // Create BadgerDB with in-memory mode db, err := badger.Open(badger.DefaultOptions(""). WithInMemory(true). + WithMemTableSize(256 << 10). // 256KB memtable size + WithValueThreshold(16 << 10). // 16KB threshold for storing values in LSM vs value log + WithNumMemtables(2). // Keep only 2 memtables in memory WithLogger(nil)) if err != nil { return nil, err diff --git a/pkg/storage/unified/resource/notifier.go b/pkg/storage/unified/resource/notifier.go index f55db60623a..5dd6a17ad29 100644 --- a/pkg/storage/unified/resource/notifier.go +++ b/pkg/storage/unified/resource/notifier.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/grafana/dskit/backoff" "github.com/grafana/grafana-app-sdk/logging" gocache "github.com/patrickmn/go-cache" @@ -13,8 +14,8 @@ import ( const ( defaultLookbackPeriod = 30 * time.Second - defaultPollInterval = 100 * time.Millisecond - defaultEventCacheSize = 10000 + defaultMinBackoff = 100 * time.Millisecond + defaultMaxBackoff = 5 * time.Second defaultBufferSize = 10000 ) @@ -29,15 +30,17 @@ type notifierOptions struct { type watchOptions struct { LookbackPeriod time.Duration // How far back to look for events - PollInterval time.Duration // How often to poll for new events BufferSize int // How many events to buffer + MinBackoff time.Duration // Minimum interval between polling requests + MaxBackoff time.Duration // Maximum interval between polling requests } func defaultWatchOptions() watchOptions { return watchOptions{ LookbackPeriod: defaultLookbackPeriod, - PollInterval: defaultPollInterval, BufferSize: defaultBufferSize, + MinBackoff: defaultMinBackoff, + MaxBackoff: defaultMaxBackoff, } } @@ -62,9 +65,13 @@ func (n *notifier) cacheKey(evt Event) string { } func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { - if opts.PollInterval <= 0 { - opts.PollInterval = defaultPollInterval + if opts.MinBackoff <= 0 { + opts.MinBackoff = defaultMinBackoff } + if opts.MaxBackoff <= 0 || opts.MaxBackoff <= opts.MinBackoff { + opts.MaxBackoff = defaultMaxBackoff + } + cacheTTL := opts.LookbackPeriod cacheCleanupInterval := 2 * opts.LookbackPeriod @@ -81,11 +88,21 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { go func() { defer close(events) + // Initialize backoff with minimum backoff interval + currentInterval := opts.MinBackoff + backoffConfig := backoff.Config{ + MinBackoff: opts.MinBackoff, + MaxBackoff: opts.MaxBackoff, + MaxRetries: 0, // infinite retries + } + bo := backoff.New(ctx, backoffConfig) + for { select { case <-ctx.Done(): return - case <-time.After(opts.PollInterval): + case <-time.After(currentInterval): + foundEvents := false for evt, err := range n.eventStore.ListSince(ctx, subtractDurationFromSnowflake(lastRV, opts.LookbackPeriod)) { if err != nil { n.log.Error("Failed to list events since", "error", err) @@ -102,6 +119,7 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { continue } + foundEvents = true if evt.ResourceVersion > lastRV { lastRV = evt.ResourceVersion + 1 } @@ -113,6 +131,14 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { return } } + + // Apply backoff logic: reset to min when events are found, increase when no events + if foundEvents { + bo.Reset() + currentInterval = opts.MinBackoff + } else { + currentInterval = bo.NextDelay() + } } } }() diff --git a/pkg/storage/unified/resource/notifier_test.go b/pkg/storage/unified/resource/notifier_test.go index a4272a36698..7b201f47420 100644 --- a/pkg/storage/unified/resource/notifier_test.go +++ b/pkg/storage/unified/resource/notifier_test.go @@ -32,7 +32,6 @@ func TestDefaultWatchOptions(t *testing.T) { opts := defaultWatchOptions() assert.Equal(t, defaultLookbackPeriod, opts.LookbackPeriod) - assert.Equal(t, defaultPollInterval, opts.PollInterval) assert.Equal(t, defaultBufferSize, opts.BufferSize) } @@ -158,8 +157,9 @@ func TestNotifier_Watch_NoEvents(t *testing.T) { opts := watchOptions{ LookbackPeriod: 100 * time.Millisecond, - PollInterval: 50 * time.Millisecond, BufferSize: 10, + MinBackoff: 50 * time.Millisecond, + MaxBackoff: 500 * time.Millisecond, } events := notifier.Watch(ctx, opts) @@ -210,8 +210,9 @@ func TestNotifier_Watch_WithExistingEvents(t *testing.T) { opts := watchOptions{ LookbackPeriod: 100 * time.Millisecond, - PollInterval: 50 * time.Millisecond, BufferSize: 10, + MinBackoff: 50 * time.Millisecond, + MaxBackoff: 500 * time.Millisecond, } // Start watching @@ -265,8 +266,9 @@ func TestNotifier_Watch_EventDeduplication(t *testing.T) { opts := watchOptions{ LookbackPeriod: time.Second, - PollInterval: 20 * time.Millisecond, BufferSize: 10, + MinBackoff: 20 * time.Millisecond, + MaxBackoff: 200 * time.Millisecond, } // Start watching @@ -326,8 +328,9 @@ func TestNotifier_Watch_ContextCancellation(t *testing.T) { opts := watchOptions{ LookbackPeriod: 100 * time.Millisecond, - PollInterval: 20 * time.Millisecond, BufferSize: 10, + MinBackoff: 20 * time.Millisecond, + MaxBackoff: 200 * time.Millisecond, } events := notifier.Watch(ctx, opts) @@ -369,8 +372,9 @@ func TestNotifier_Watch_MultipleEvents(t *testing.T) { opts := watchOptions{ LookbackPeriod: time.Second, - PollInterval: 20 * time.Millisecond, BufferSize: 10, + MinBackoff: 20 * time.Millisecond, + MaxBackoff: 200 * time.Millisecond, } // Start watching From 62d83a1ba93257d168f75e57ab94199061cec500 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Thu, 27 Nov 2025 15:50:47 -0400 Subject: [PATCH 157/423] Histogram: Fix runaway bucket densification with extremely sparse + large datasets (#114557) * test(histogram): failing test for runaway densification * fix(histogram): maximum bucket densification avoids OOM error * fix(histogram): handle multiple densified buckets --- .../transformers/histogram.test.ts | 36 +++++++++++++++++++ .../transformations/transformers/histogram.ts | 13 +++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/histogram.test.ts b/packages/grafana-data/src/transformations/transformers/histogram.test.ts index d7be8281adf..5597eb06787 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.test.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.test.ts @@ -896,6 +896,42 @@ describe('getHistogramFields', () => { } `); }); + + it('should prevent excessive densification when sparse histogram has large gaps', () => { + const result = getHistogramFields( + toDataFrame({ + meta: { + type: DataFrameType.HeatmapCells, + }, + fields: [ + { name: 'yMin', type: FieldType.number, values: [0.001, 1000] }, + { name: 'yMax', type: FieldType.number, values: [0.00101, 1010] }, + { name: 'count', type: FieldType.number, values: [10, 20] }, + ], + }) + ); + + expect(result).toBeDefined(); + expect(result!.counts[0].values.length).toBeLessThanOrEqual(1001); + }); + + it('should handle multiple observed buckets when hitting densification limit', () => { + const result = getHistogramFields( + toDataFrame({ + meta: { + type: DataFrameType.HeatmapCells, + }, + fields: [ + { name: 'yMin', type: FieldType.number, values: [0.001, 1000, 2000] }, + { name: 'yMax', type: FieldType.number, values: [0.00101, 1010, 2020] }, + { name: 'count', type: FieldType.number, values: [10, 20, 30] }, + ], + }) + ); + + expect(result).toBeDefined(); + expect(result!.counts[0].values.every((v) => !isNaN(v))).toBe(true); + }); }); describe('joinHistograms', () => { diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index 723fb07b0d8..dfd095921d2 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -210,6 +210,8 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine let denseMins: number[] = []; let denseMaxs: number[] = []; + const MAX_DENSIFIED_BUCKETS = 1000; + for (let i = 0; i < uniqueMaxs.length; i++) { let curMax = uniqueMaxs[i]; let curMin = uniqueMins[i]; @@ -223,13 +225,17 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine curMax = curMax * bucketFactor; curMin = curMin * bucketFactor; - while (curMax < nextMax * 0.999999) { + while (curMax < nextMax * 0.999999 && denseMaxs.length < MAX_DENSIFIED_BUCKETS) { denseMaxs.push(curMax); denseMins.push(curMin); curMax = curMax * bucketFactor; curMin = curMin * bucketFactor; } + + if (denseMaxs.length >= MAX_DENSIFIED_BUCKETS) { + break; + } } } @@ -238,7 +244,10 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine for (let i = 0; i < yMaxField.values.length; i++) { let max = yMaxField.values[i]; - countsByMax.set(max, countsByMax.get(max) + countField.values[i]); + let currentCount = countsByMax.get(max); + if (currentCount !== undefined) { + countsByMax.set(max, currentCount + countField.values[i]); + } } let fields = { From 48a8d54794cc5d6bbdab4f5f4e8944cb2b8ee976 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:40:11 +0000 Subject: [PATCH 158/423] I18n: Download translations from Crowdin (#114565) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 33 +++++++++++------------------ public/locales/de-DE/grafana.json | 33 +++++++++++------------------ public/locales/es-ES/grafana.json | 33 +++++++++++------------------ public/locales/fr-FR/grafana.json | 33 +++++++++++------------------ public/locales/hu-HU/grafana.json | 33 +++++++++++------------------ public/locales/id-ID/grafana.json | 33 +++++++++++------------------ public/locales/it-IT/grafana.json | 33 +++++++++++------------------ public/locales/ja-JP/grafana.json | 33 +++++++++++------------------ public/locales/ko-KR/grafana.json | 33 +++++++++++------------------ public/locales/nl-NL/grafana.json | 33 +++++++++++------------------ public/locales/pl-PL/grafana.json | 33 +++++++++++------------------ public/locales/pt-BR/grafana.json | 33 +++++++++++------------------ public/locales/pt-PT/grafana.json | 33 +++++++++++------------------ public/locales/ru-RU/grafana.json | 33 +++++++++++------------------ public/locales/sv-SE/grafana.json | 33 +++++++++++------------------ public/locales/tr-TR/grafana.json | 33 +++++++++++------------------ public/locales/zh-Hans/grafana.json | 33 +++++++++++------------------ public/locales/zh-Hant/grafana.json | 33 +++++++++++------------------ 18 files changed, 216 insertions(+), 378 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 5a27d2da0e2..73a8378bd20 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -11803,13 +11803,6 @@ "saving": "Ukládání", "title-error-loading-file": "Chyba při načítání souboru" }, - "files-view": { - "columns": { - "history": "Historie", - "view": "Zobrazit" - }, - "placeholder-search": "Hledat" - }, "finish-step": { "description-enable-previews": "Přidá náhled obrázku změn nástěnky v pull requestech. Obrázky nástěnek Grafana budou sdíleny ve vašem úložišti Git a uvidí je každý, kdo má přístup k úložišti.", "description-generate-dashboard-previews": "Vytvořte odkazy na náhledy pro žádosti o stažení", @@ -12024,9 +12017,6 @@ "source-code": "Zdrojový kód" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Jen pro čtení", "settings": "Nastavení", "view": "Zobrazit" @@ -12063,14 +12053,6 @@ "webhook-last-event": "Poslední událost:", "webhook-url": "Zobrazit webhook" }, - "repository-resources": { - "columns": { - "history": "Historie", - "view-dashboard": "Zobrazit", - "view-folder": "Zobrazit" - }, - "placeholder-search": "Hledat" - }, "repository-status-page": { "back-to-repositories": "Zpět na úložiště", "cleaning-up-resources": "Čištění zdrojů úložiště", @@ -12078,12 +12060,10 @@ "not-found": "nenalezeno", "not-found-message": "Úložiště nebylo nalezeno", "repository-config-exists-configuration": "Ujistěte se, že konfigurace úložiště existuje v konfiguračním souboru.", - "tab-files": "Soubory", - "tab-files-title": "Seznam nezpracovaných souborů z úložiště", "tab-overview": "Přehled", "tab-overview-title": "Přehled úložiště", "tab-resources": "Zdroje", - "tab-resources-title": "Zdroje uložené v databázi Grafany", + "tab-resources-title": "", "title": "Stav úložiště", "title-legacy-storage": "Starší verze úložiště", "title-queued-for-deletion": "Zařazeno do fronty k odstranění" @@ -12106,6 +12086,17 @@ "pure-git": "Pouze Git", "pure-git-description": "Připojit k jakémukoli úložišti Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Základ", "dashboard-preview": "Náhled nástěnky", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 983ccc5868b..469a0ddfef8 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Einsparen", "title-error-loading-file": "Fehler beim Laden der Datei" }, - "files-view": { - "columns": { - "history": "Verlauf", - "view": "Anzeigen" - }, - "placeholder-search": "Suche" - }, "finish-step": { "description-enable-previews": "Fügt eine Bildvorschau der Dashboard-Änderungen bei Pull-Requests hinzu. Bilder Ihrer Grafana-Dashboards werden in Ihrem Git-Repository bereitgestellt und sind für jede Person mit Repository-Zugriff sichtbar.", "description-generate-dashboard-previews": "Erstellen Sie Vorschau-Links für Pull-Requests", @@ -11920,9 +11913,6 @@ "source-code": "Quellcode" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Schreibgeschützt", "settings": "Einstellungen", "view": "Anzeigen" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Letztes Ereignis:", "webhook-url": "Webhook anzeigen" }, - "repository-resources": { - "columns": { - "history": "Verlauf", - "view-dashboard": "Anzeigen", - "view-folder": "Anzeigen" - }, - "placeholder-search": "Suche" - }, "repository-status-page": { "back-to-repositories": "Zurück zu den Repositorys", "cleaning-up-resources": "Bereinigen von Repository-Ressourcen", @@ -11974,12 +11956,10 @@ "not-found": "nicht gefunden", "not-found-message": "Repository nicht gefunden", "repository-config-exists-configuration": "Achten Sie darauf, dass die Repository-config in der Konfigurationsdatei vorhanden ist.", - "tab-files": "Dateien", - "tab-files-title": "Die Raw-Datei-Liste aus dem Repository", "tab-overview": "Übersicht", "tab-overview-title": "Repository-Übersicht", "tab-resources": "Ressourcen", - "tab-resources-title": "In der Grafana-Datenbank gespeicherte Ressourcen", + "tab-resources-title": "", "title": "Repository-Status", "title-legacy-storage": "Veralteter Speicher", "title-queued-for-deletion": "Zum Löschen in die Warteschlange gestellt" @@ -12002,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Mit beliebigem Git-Repository verbinden" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Basis", "dashboard-preview": "Dashboard-Vorschau", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index bca3dd9a0df..db55d7dbe2b 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Guardando", "title-error-loading-file": "Error al cargar el archivo" }, - "files-view": { - "columns": { - "history": "Historial", - "view": "Vista" - }, - "placeholder-search": "Buscar" - }, "finish-step": { "description-enable-previews": "Añade una vista previa de la imagen de los cambios del dashboard en las solicitudes de extracción. Las imágenes de tus paneles de Grafana se compartirán en tu repositorio Git y serán visibles para cualquier persona con acceso al repositorio.", "description-generate-dashboard-previews": "Crear enlaces de vista previa para las solicitudes de extracción", @@ -11920,9 +11913,6 @@ "source-code": "Código fuente" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Solo lectura", "settings": "Configuración", "view": "Vista" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Último evento:", "webhook-url": "Ver webhook" }, - "repository-resources": { - "columns": { - "history": "Historial", - "view-dashboard": "Vista", - "view-folder": "Vista" - }, - "placeholder-search": "Buscar" - }, "repository-status-page": { "back-to-repositories": "Volver a los repositorios", "cleaning-up-resources": "Limpiando los recursos del repositorio", @@ -11974,12 +11956,10 @@ "not-found": "no ha sido encontrado", "not-found-message": "Repositorio no encontrado", "repository-config-exists-configuration": "Asegúrate de que la configuración del repositorio exista en el archivo de configuración.", - "tab-files": "Archivos", - "tab-files-title": "La lista de archivos sin procesar del repositorio", "tab-overview": "Resumen", "tab-overview-title": "Resumen del repositorio", "tab-resources": "Recursos", - "tab-resources-title": "Recursos guardados en la base de datos de Grafana", + "tab-resources-title": "", "title": "Estado del repositorio", "title-legacy-storage": "Almacenamiento heredado", "title-queued-for-deletion": "En cola para su eliminación" @@ -12002,6 +11982,17 @@ "pure-git": "Git puro", "pure-git-description": "Conectar a cualquier repositorio Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Base", "dashboard-preview": "Vista previa del dashboard", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index c24f8a94696..e0b0cd18363 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Enregistrement en cours", "title-error-loading-file": "Erreur lors du chargement du fichier" }, - "files-view": { - "columns": { - "history": "Historique", - "view": "Afficher" - }, - "placeholder-search": "Rechercher" - }, "finish-step": { "description-enable-previews": "Ajoute un aperçu des images des modifications apportées au tableau de bord dans les demandes de fusion. Les images de vos tableaux de bord Grafana seront partagées dans votre référentiel Git et visibles par toute personne ayant accès au référentiel.", "description-generate-dashboard-previews": "Créer des liens de prévisualisation pour les demandes de tirage", @@ -11920,9 +11913,6 @@ "source-code": "Code source" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Lecture seule", "settings": "Paramètres", "view": "Afficher" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Dernier événement :", "webhook-url": "Voir le webhook" }, - "repository-resources": { - "columns": { - "history": "Historique", - "view-dashboard": "Afficher", - "view-folder": "Afficher" - }, - "placeholder-search": "Rechercher" - }, "repository-status-page": { "back-to-repositories": "Retour aux référentiels", "cleaning-up-resources": "Nettoyage des ressources du référentiel", @@ -11974,12 +11956,10 @@ "not-found": "introuvable", "not-found-message": "Référentiel introuvable", "repository-config-exists-configuration": "Assurez-vous que la configuration du référentiel existe dans le fichier de configuration.", - "tab-files": "Fichiers", - "tab-files-title": "La liste des fichiers bruts du référentiel", "tab-overview": "Vue d’ensemble", "tab-overview-title": "Vue d’ensemble du référentiel", "tab-resources": "Ressources", - "tab-resources-title": "Ressources enregistrées dans la base de données Grafana", + "tab-resources-title": "", "title": "Statut du référentiel", "title-legacy-storage": "Stockage hérité", "title-queued-for-deletion": "Mis en attente pour suppression" @@ -12002,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Se connecter à un quelconque dépôt Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Base", "dashboard-preview": "Aperçu du tableau de bord", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 287f64d1b3d..3ff7e30485b 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Mentés", "title-error-loading-file": "Hiba a fájl betöltésekor" }, - "files-view": { - "columns": { - "history": "Előzmények", - "view": "Nézet" - }, - "placeholder-search": "Keresés" - }, "finish-step": { "description-enable-previews": "Hozzáadja az irányítópult változásainak előnézetét a lekérésekben. A Grafana-irányítópultok képei meg lesznek osztva a Git-tárban, és bárki számára láthatóak lesznek, aki hozzáféréssel rendelkezik az adattárhoz.", "description-generate-dashboard-previews": "Előnézeti hivatkozások létrehozása az összefésülési kérelmekhez", @@ -11920,9 +11913,6 @@ "source-code": "Forráskód" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webkapocs" - }, "read-only-badge": "Csak olvasható", "settings": "Beállítások", "view": "Nézet" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Legutóbbi esemény:", "webhook-url": "Webkapocs megtekintése" }, - "repository-resources": { - "columns": { - "history": "Előzmények", - "view-dashboard": "Nézet", - "view-folder": "Nézet" - }, - "placeholder-search": "Keresés" - }, "repository-status-page": { "back-to-repositories": "Vissza az adattárakhoz", "cleaning-up-resources": "Adattári erőforrások tisztítása", @@ -11974,12 +11956,10 @@ "not-found": "nem található", "not-found-message": "Nem található adattár", "repository-config-exists-configuration": "Győződjön meg arról, hogy a tároló konfigurációja létezik a konfigurációs fájlban.", - "tab-files": "Fájlok", - "tab-files-title": "Nyers fájllista az adattárból", "tab-overview": "Áttekintés", "tab-overview-title": "Adattár áttekintése", "tab-resources": "Erőforrások", - "tab-resources-title": "Grafana-adatbázisba mentett erőforrások", + "tab-resources-title": "", "title": "Adattár állapota", "title-legacy-storage": "Örökölt tárolás", "title-queued-for-deletion": "Törlésre vár" @@ -12002,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Csatlakozás bármely Git-adattárhoz" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Alap", "dashboard-preview": "Irányítópult előnézete", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index cb248ff1494..a6708a09472 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -11653,13 +11653,6 @@ "saving": "Menyimpan", "title-error-loading-file": "Kesalahan saat memuat file" }, - "files-view": { - "columns": { - "history": "Sejarah", - "view": "Lihat" - }, - "placeholder-search": "Cari" - }, "finish-step": { "description-enable-previews": "Menambahkan pratinjau gambar dari perubahan dasbor di permintaan pull. Gambar dasbor Grafana Anda akan dibagikan di repositori Git Anda dan dapat dilihat oleh siapa saja yang memiliki akses repositori.", "description-generate-dashboard-previews": "Buat tautan pratinjau untuk permintaan penarikan", @@ -11868,9 +11861,6 @@ "source-code": "Kode sumber" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Hanya baca", "settings": "Pengaturan", "view": "Lihat" @@ -11907,14 +11897,6 @@ "webhook-last-event": "Peristiwa Terakhir:", "webhook-url": "Lihat Webhook" }, - "repository-resources": { - "columns": { - "history": "Sejarah", - "view-dashboard": "Lihat", - "view-folder": "Lihat" - }, - "placeholder-search": "Cari" - }, "repository-status-page": { "back-to-repositories": "Kembali ke repositori", "cleaning-up-resources": "Membersihkan sumber daya repositori", @@ -11922,12 +11904,10 @@ "not-found": "tidak ditemukan", "not-found-message": "Repositori tidak ditemukan", "repository-config-exists-configuration": "Pastikan konfigurasi repositori ada dalam file konfigurasi.", - "tab-files": "File", - "tab-files-title": "Daftar file mentah dari repositori", "tab-overview": "Gambaran Umum", "tab-overview-title": "Gambaran umum repositori", "tab-resources": "Sumber Daya", - "tab-resources-title": "Sumber daya disimpan dalam database grafana", + "tab-resources-title": "", "title": "Status Repositori", "title-legacy-storage": "Penyimpanan Lama", "title-queued-for-deletion": "Diantrekan untuk dihapus" @@ -11950,6 +11930,17 @@ "pure-git": "Pure Git", "pure-git-description": "Hubungkan ke repositori Git mana pun" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Dasar", "dashboard-preview": "Pratinjau Dasbor", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 731218c4f16..e5fc8647a20 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Salvataggio in corso", "title-error-loading-file": "Errore nel caricamento del file" }, - "files-view": { - "columns": { - "history": "Cronologia", - "view": "Visualizza" - }, - "placeholder-search": "Cerca" - }, "finish-step": { "description-enable-previews": "Aggiunge un'anteprima dell'immagine delle modifiche alla dashboard nelle richieste di pull. Le immagini delle dashboard Grafana verranno condivise nel repository Git e saranno visibili a chiunque abbia accesso al repository.", "description-generate-dashboard-previews": "Crea collegamenti di anteprima per le richieste di pull", @@ -11920,9 +11913,6 @@ "source-code": "Codice sorgente" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Solo lettura", "settings": "Impostazioni", "view": "Visualizza" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Ultimo evento:", "webhook-url": "Visualizza webhook" }, - "repository-resources": { - "columns": { - "history": "Cronologia", - "view-dashboard": "Visualizza", - "view-folder": "Visualizza" - }, - "placeholder-search": "Cerca" - }, "repository-status-page": { "back-to-repositories": "Torna ai repository", "cleaning-up-resources": "Pulizia delle risorse del repository", @@ -11974,12 +11956,10 @@ "not-found": "non trovato", "not-found-message": "Repository non trovato", "repository-config-exists-configuration": "Assicurati che la configurazione del repository esista nel file di configurazione.", - "tab-files": "File", - "tab-files-title": "L'elenco dei file non elaborati dal repository", "tab-overview": "Panoramica", "tab-overview-title": "Panoramica del repository", "tab-resources": "Risorse", - "tab-resources-title": "Risorse salvate nel database di Grafana", + "tab-resources-title": "", "title": "Stato del repository", "title-legacy-storage": "Memoria esistente", "title-queued-for-deletion": "In coda per l'eliminazione" @@ -12002,6 +11982,17 @@ "pure-git": "Git puro", "pure-git-description": "Connetti a qualsiasi repository Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Base", "dashboard-preview": "Anteprima dashboard", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 5eb117faa50..fa3f87eabf4 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -11653,13 +11653,6 @@ "saving": "保存中", "title-error-loading-file": "ファイル読み込み時のエラー" }, - "files-view": { - "columns": { - "history": "履歴", - "view": "表示" - }, - "placeholder-search": "検索" - }, "finish-step": { "description-enable-previews": "プルリクエストによるダッシュボードの変更の画像プレビューを追加します。Grafanaダッシュボードの画像はGitリポジトリで共有され、リポジトリにアクセスできる全ユーザーが閲覧できます。", "description-generate-dashboard-previews": "プルリクエストのプレビューリンクを作成する", @@ -11868,9 +11861,6 @@ "source-code": "ソースコード" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "読み取り専用", "settings": "設定", "view": "表示" @@ -11907,14 +11897,6 @@ "webhook-last-event": "最新イベント:", "webhook-url": "Webhookを表示" }, - "repository-resources": { - "columns": { - "history": "履歴", - "view-dashboard": "表示", - "view-folder": "表示" - }, - "placeholder-search": "検索" - }, "repository-status-page": { "back-to-repositories": "リポジトリに戻る", "cleaning-up-resources": "リポジトリリソースのクリーンアップ中", @@ -11922,12 +11904,10 @@ "not-found": "見つかりません", "not-found-message": "リポジトリが見つかりません", "repository-config-exists-configuration": "リポジトリ設定が設定ファイルに含まれていることを確認してください。", - "tab-files": "ファイル", - "tab-files-title": "リポジトリからのRawファイルリスト", "tab-overview": "概要", "tab-overview-title": "リポジトリの概要", "tab-resources": "リソース", - "tab-resources-title": "Grafanaデータベースに保存されたリソース", + "tab-resources-title": "", "title": "リポジトリの状態", "title-legacy-storage": "レガシーストレージ", "title-queued-for-deletion": "削除待ちリストに追加されました" @@ -11950,6 +11930,17 @@ "pure-git": "Pure Git", "pure-git-description": "任意のGitリポジトリに接続" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "ベース", "dashboard-preview": "ダッシュボードプレビュー", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 1cc6b2ab0a1..cf9773184cd 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -11653,13 +11653,6 @@ "saving": "저장 중", "title-error-loading-file": "파일 로딩 중 오류 발생" }, - "files-view": { - "columns": { - "history": "이력", - "view": "보기" - }, - "placeholder-search": "검색" - }, "finish-step": { "description-enable-previews": "풀 요청에서 대시보드 변경 사항의 이미지 미리 보기를 추가합니다. Grafana 대시보드의 이미지는 Git 리포지토리에서 공유되며 리포지토리 액세스 권한이 있는 모든 사용자가 볼 수 있습니다.", "description-generate-dashboard-previews": "풀 요청에 대한 미리 보기 링크 생성", @@ -11868,9 +11861,6 @@ "source-code": "소스 코드" }, "repository-card": { - "get-repository-meta": { - "webhook": "웹훅" - }, "read-only-badge": "읽기 전용", "settings": "설정", "view": "보기" @@ -11907,14 +11897,6 @@ "webhook-last-event": "마지막 이벤트:", "webhook-url": "웹훅 보기" }, - "repository-resources": { - "columns": { - "history": "이력", - "view-dashboard": "보기", - "view-folder": "보기" - }, - "placeholder-search": "검색" - }, "repository-status-page": { "back-to-repositories": "리포지토리로 돌아가기", "cleaning-up-resources": "리포지토리 리소스 정리 및 삭제 중", @@ -11922,12 +11904,10 @@ "not-found": "찾을 수 없음", "not-found-message": "리포지토리를 찾을 수 없습니다", "repository-config-exists-configuration": "리포지토리 구성이 구성 파일에 있는지 확인하세요.", - "tab-files": "파일", - "tab-files-title": "리포지토리의 원시 파일 목록", "tab-overview": "개요", "tab-overview-title": "리포지토리 개요", "tab-resources": "리소스", - "tab-resources-title": "Grafana 데이터베이스에 저장된 리소스", + "tab-resources-title": "", "title": "리포지토리 상태", "title-legacy-storage": "레거시 스토리지", "title-queued-for-deletion": "삭제 대기열에 추가됨" @@ -11950,6 +11930,17 @@ "pure-git": "Git으로만 구성", "pure-git-description": "모든 Git 리포지토리에 연결" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "베이스", "dashboard-preview": "대시보드 미리 보기", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index b66136b8183..8a7dcc3b2d0 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Opslaan", "title-error-loading-file": "Er is een fout opgetreden bij het laden van het bestand" }, - "files-view": { - "columns": { - "history": "Geschiedenis", - "view": "Weergave" - }, - "placeholder-search": "Zoeken" - }, "finish-step": { "description-enable-previews": "Voegt een afbeeldingsvoorbeeld toe van dashboardwijzigingen in pull requests. Afbeeldingen van je Grafana-dashboards worden gedeeld in je Git-repository en zijn zichtbaar voor iedereen met toegang tot de repository.", "description-generate-dashboard-previews": "Voorbeeldlinks maken voor pull-verzoeken", @@ -11920,9 +11913,6 @@ "source-code": "Broncode" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Alleen lezen", "settings": "Instellingen", "view": "Weergave" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Laatste gebeurtenis:", "webhook-url": "Webhook bekijken" }, - "repository-resources": { - "columns": { - "history": "Geschiedenis", - "view-dashboard": "Weergave", - "view-folder": "Weergave" - }, - "placeholder-search": "Zoeken" - }, "repository-status-page": { "back-to-repositories": "Terug naar repositories", "cleaning-up-resources": "Bronnen van repository opschonen", @@ -11974,12 +11956,10 @@ "not-found": "niet gevonden", "not-found-message": "Repository niet gevonden", "repository-config-exists-configuration": "Zorg ervoor dat de repository-configuratie in het configuratiebestand bestaat.", - "tab-files": "Bestanden", - "tab-files-title": "De lijst met onbewerkte bestanden uit de repository", "tab-overview": "Overzicht", "tab-overview-title": "Repository-overzicht", "tab-resources": "Bronnen", - "tab-resources-title": "Bronnen opgeslagen in Grafana-database", + "tab-resources-title": "", "title": "Repository-status", "title-legacy-storage": "Legacy-opslag", "title-queued-for-deletion": "In de wachtrij voor verwijdering" @@ -12002,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Verbinden met elke Git-repository" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Basis", "dashboard-preview": "Dashboardvoorbeeld", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 233b9ed0a50..d988de6fdee 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -11803,13 +11803,6 @@ "saving": "Zapisywanie", "title-error-loading-file": "Błąd podczas ładowania pliku" }, - "files-view": { - "columns": { - "history": "Historia", - "view": "Wyświetl" - }, - "placeholder-search": "Szukaj" - }, "finish-step": { "description-enable-previews": "Dodaje podgląd obrazu zmian pulpitu w żądaniach pull. Obrazy pulpitów Grafany zostaną udostępnione w repozytorium Git i będą widoczne dla każdego, kto ma do niego dostęp.", "description-generate-dashboard-previews": "Utwórz linki podglądu dla żądań pull", @@ -12024,9 +12017,6 @@ "source-code": "Kod źródłowy" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Tylko do odczytu", "settings": "Ustawienia", "view": "Wyświetl" @@ -12063,14 +12053,6 @@ "webhook-last-event": "Ostatnie zdarzenie:", "webhook-url": "Wyświetl element webhook" }, - "repository-resources": { - "columns": { - "history": "Historia", - "view-dashboard": "Wyświetl", - "view-folder": "Wyświetl" - }, - "placeholder-search": "Szukaj" - }, "repository-status-page": { "back-to-repositories": "Wróć do repozytoriów", "cleaning-up-resources": "Sprzątanie zasobów repozytorium", @@ -12078,12 +12060,10 @@ "not-found": "nie znaleziono", "not-found-message": "Nie znaleziono repozytorium", "repository-config-exists-configuration": "Upewnij się, że konfiguracja repozytorium istnieje w pliku konfiguracyjnym.", - "tab-files": "Pliki", - "tab-files-title": "Lista nieprzetworzonych plików z repozytorium", "tab-overview": "Przegląd", "tab-overview-title": "Przegląd repozytorium", "tab-resources": "Zasoby", - "tab-resources-title": "Zasoby zapisane w bazie danych Grafany", + "tab-resources-title": "", "title": "Status repozytorium", "title-legacy-storage": "Starsza pamięć masowa", "title-queued-for-deletion": "Dodano do kolejki do usunięcia" @@ -12106,6 +12086,17 @@ "pure-git": "Czysty Git", "pure-git-description": "Połącz z dowolnym repozytorium Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Podstawa", "dashboard-preview": "Podgląd pulpitu", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 411c26f63fb..e17ed3dade4 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Salvando", "title-error-loading-file": "Erro ao carregar o arquivo" }, - "files-view": { - "columns": { - "history": "Histórico", - "view": "Visualizar" - }, - "placeholder-search": "Pesquisar" - }, "finish-step": { "description-enable-previews": "Adiciona uma pré-visualização em imagem das alterações do painel nas solicitações de extração. As imagens dos seus painéis da Grafana serão compartilhadas no seu repositório Git e estarão disponíveis para qualquer pessoa com acesso ao repositório.", "description-generate-dashboard-previews": "Criar links de prévia para solicitações de extração", @@ -11920,9 +11913,6 @@ "source-code": "Código fonte" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Somente leitura", "settings": "Configurações", "view": "Visualizar" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Último evento:", "webhook-url": "Visualizar Webhook" }, - "repository-resources": { - "columns": { - "history": "Histórico", - "view-dashboard": "Visualizar", - "view-folder": "Visualizar" - }, - "placeholder-search": "Pesquisar" - }, "repository-status-page": { "back-to-repositories": "Voltar para os repositórios", "cleaning-up-resources": "Limpando recursos do repositório", @@ -11974,12 +11956,10 @@ "not-found": "não encontrado", "not-found-message": "Repositório não encontrado", "repository-config-exists-configuration": "Verifique se a configuração do repositório existe no arquivo de configuração.", - "tab-files": "Arquivos", - "tab-files-title": "A lista de arquivos brutos do repositório", "tab-overview": "Visão geral", "tab-overview-title": "Visão geral do repositório", "tab-resources": "Fontes", - "tab-resources-title": "Recursos salvos no banco de dados da Grafana", + "tab-resources-title": "", "title": "Status do repositório", "title-legacy-storage": "Armazenamento legado", "title-queued-for-deletion": "Na fila para exclusão" @@ -12002,6 +11982,17 @@ "pure-git": "Git puro", "pure-git-description": "Conectar a qualquer repositório do Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Base", "dashboard-preview": "Pré-visualização do painel", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index a97f0c759f6..34cc8be7717 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -11703,13 +11703,6 @@ "saving": "A guardar", "title-error-loading-file": "Erro ao carregar o ficheiro" }, - "files-view": { - "columns": { - "history": "Histórico", - "view": "Ver" - }, - "placeholder-search": "Pesquisar" - }, "finish-step": { "description-enable-previews": "Adicionar uma pré-visualização de imagem das alterações do painel de controlo nos pedidos de extração. As imagens dos seus painéis de controlo Grafana serão partilhadas no seu repositório Git e visíveis para qualquer pessoa com acesso ao repositório.", "description-generate-dashboard-previews": "Criar links de pré-visualização para pedidos de extração", @@ -11920,9 +11913,6 @@ "source-code": "Código-fonte" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Apenas para leitura", "settings": "Definições", "view": "Ver" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Último evento:", "webhook-url": "Visualizar Webhook" }, - "repository-resources": { - "columns": { - "history": "Histórico", - "view-dashboard": "Ver", - "view-folder": "Ver" - }, - "placeholder-search": "Pesquisar" - }, "repository-status-page": { "back-to-repositories": "Voltar aos repositórios", "cleaning-up-resources": "A limpar recursos do repositório", @@ -11974,12 +11956,10 @@ "not-found": "não encontrado", "not-found-message": "Repositório não encontrado", "repository-config-exists-configuration": "Certifique-se de que a configuração do repositório existe no ficheiro de configuração.", - "tab-files": "Ficheiros", - "tab-files-title": "A lista de ficheiros sem processar do repositório", "tab-overview": "Visão geral", "tab-overview-title": "Vista geral do repositório", "tab-resources": "Recursos", - "tab-resources-title": "Recursos guardados na base de dados da Grafana", + "tab-resources-title": "", "title": "Estado do repositório", "title-legacy-storage": "Armazenamento herdado", "title-queued-for-deletion": "Em fila para eliminação" @@ -12002,6 +11982,17 @@ "pure-git": "Git puro", "pure-git-description": "Ligar a qualquer repositório Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Base", "dashboard-preview": "Pré-visualização do painel de controlo", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index e916e740995..3a8724d3098 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -11803,13 +11803,6 @@ "saving": "Сохранение", "title-error-loading-file": "Ошибка при загрузке файла" }, - "files-view": { - "columns": { - "history": "История", - "view": "Просмотр" - }, - "placeholder-search": "Поиск" - }, "finish-step": { "description-enable-previews": "Обеспечивает возможность просмотра изображений с изменениями дашборда в запросах на включение изменений. Изображения дашбордов Grafana будут публиковаться в вашем репозитории Git и видны всем, у кого есть доступ к репозиторию.", "description-generate-dashboard-previews": "Создать ссылки для предварительного просмотра запросов на включение изменений", @@ -12024,9 +12017,6 @@ "source-code": "Исходный код" }, "repository-card": { - "get-repository-meta": { - "webhook": "Веб-перехватчик" - }, "read-only-badge": "Только для чтения", "settings": "Параметры", "view": "Просмотр" @@ -12063,14 +12053,6 @@ "webhook-last-event": "Последнее событие:", "webhook-url": "Просмотр веб-перехватчика" }, - "repository-resources": { - "columns": { - "history": "История", - "view-dashboard": "Просмотр", - "view-folder": "Просмотр" - }, - "placeholder-search": "Поиск" - }, "repository-status-page": { "back-to-repositories": "Назад к репозиториям", "cleaning-up-resources": "Очистка ресурсов репозитория", @@ -12078,12 +12060,10 @@ "not-found": "не найдено", "not-found-message": "Репозиторий не найден", "repository-config-exists-configuration": "Убедитесь, что конфигурация репозитория существует в файле конфигурации.", - "tab-files": "Файлы", - "tab-files-title": "Список необработанных файлов из репозитория", "tab-overview": "Обзор", "tab-overview-title": "Обзор репозитория", "tab-resources": "Ресурсы", - "tab-resources-title": "Ресурсы, сохраненные в базе данных Grafana", + "tab-resources-title": "", "title": "Состояние репозитория", "title-legacy-storage": "Устаревшее хранилище", "title-queued-for-deletion": "В очереди на удаление" @@ -12106,6 +12086,17 @@ "pure-git": "Pure Git", "pure-git-description": "Подключиться к любому репозиторию Git" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Основа", "dashboard-preview": "Просмотр дашборда", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 38758ccf40e..c0bd748a996 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Sparar", "title-error-loading-file": "Ett fel uppstod när en fil laddades" }, - "files-view": { - "columns": { - "history": "Historik", - "view": "Visa" - }, - "placeholder-search": "Sök" - }, "finish-step": { "description-enable-previews": "Lägger till en förhandsgranskning av ändringar i instrumentpanelen för pull-förfrågningar. Bilder av dina Grafana-instrumentpaneler kommer att delas på din Git-lagringsplats och vara synliga för alla med lagringsåtkomst.", "description-generate-dashboard-previews": "Skapa förhandsgranskningslänkar för pull-begäranden", @@ -11920,9 +11913,6 @@ "source-code": "Källkod" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Skrivskyddad", "settings": "Inställningar", "view": "Visa" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Senaste händelsen:", "webhook-url": "Visa webhook" }, - "repository-resources": { - "columns": { - "history": "Historik", - "view-dashboard": "Visa", - "view-folder": "Visa" - }, - "placeholder-search": "Sök" - }, "repository-status-page": { "back-to-repositories": "Tillbaka till lagringsplatserna", "cleaning-up-resources": "Rensa lagringsplatsresurser", @@ -11974,12 +11956,10 @@ "not-found": "hittades inte", "not-found-message": "Lagringsplatsen hittades inte", "repository-config-exists-configuration": "Verifiera att lagringsplatskonfigurationen finns i konfigurationsfilen.", - "tab-files": "Filer", - "tab-files-title": "Rådatafillistan från lagringsplatsen", "tab-overview": "Översikt", "tab-overview-title": "Översikt över lagringsplats", "tab-resources": "Resurser", - "tab-resources-title": "Resurser sparade i Grafana-databasen", + "tab-resources-title": "", "title": "Status för lagringsplats", "title-legacy-storage": "Äldre lagring", "title-queued-for-deletion": "Köad för radering" @@ -12002,6 +11982,17 @@ "pure-git": "Ren Git", "pure-git-description": "Anslut till valfri Git-lagringsplats" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Bas", "dashboard-preview": "Förhandsgranskning av instrumentpanel", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index c727847d792..cfeb21bd5aa 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -11703,13 +11703,6 @@ "saving": "Kaydediliyor", "title-error-loading-file": "Dosya yüklenirken hata oluştu" }, - "files-view": { - "columns": { - "history": "Geçmiş", - "view": "Görüntüle" - }, - "placeholder-search": "Ara" - }, "finish-step": { "description-enable-previews": "Çekme isteklerinde pano değişikliklerinin görsel ön izlemesini ekler. Grafana panolarınızın görselleri Git deponuzda paylaşılacak ve depo erişimi olan herkes tarafından görülebilecektir.", "description-generate-dashboard-previews": "Çekme istekleri için ön izleme bağlantıları oluşturun", @@ -11920,9 +11913,6 @@ "source-code": "Kaynak kodu" }, "repository-card": { - "get-repository-meta": { - "webhook": "Web kancası" - }, "read-only-badge": "", "settings": "Ayarlar", "view": "Görüntüle" @@ -11959,14 +11949,6 @@ "webhook-last-event": "Son Olay:", "webhook-url": "Web Kancasını Görüntüle" }, - "repository-resources": { - "columns": { - "history": "Geçmiş", - "view-dashboard": "Görüntüle", - "view-folder": "Görüntüle" - }, - "placeholder-search": "Ara" - }, "repository-status-page": { "back-to-repositories": "Depolara geri dön", "cleaning-up-resources": "Depo kaynakları temizleniyor", @@ -11974,12 +11956,10 @@ "not-found": "bulunamadı", "not-found-message": "Depo bulunamadı", "repository-config-exists-configuration": "Depo yapılandırmasının yapılandırma dosyasında mevcut olduğundan emin olun.", - "tab-files": "Dosyalar", - "tab-files-title": "Depodan ham dosya listesi", "tab-overview": "Genel Bakış", "tab-overview-title": "Depoya genel bakış", "tab-resources": "Kaynaklar", - "tab-resources-title": "Grafana veri tabanına kaydedilen kaynaklar", + "tab-resources-title": "", "title": "Depo Durumu", "title-legacy-storage": "Eski Depolama", "title-queued-for-deletion": "Silinmek üzere kuyruğa alındı" @@ -12002,6 +11982,17 @@ "pure-git": "Pure Git", "pure-git-description": "Herhangi bir Git deposuna bağlanın" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "Temel", "dashboard-preview": "Pano Ön İzlemesi", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 9e5952f913f..e96f271b082 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -11653,13 +11653,6 @@ "saving": "正在保存", "title-error-loading-file": "加载文件时出错" }, - "files-view": { - "columns": { - "history": "历史记录", - "view": "查看" - }, - "placeholder-search": "搜索" - }, "finish-step": { "description-enable-previews": "在拉取请求中添加数据面板更改的图像预览。Grafana 数据面板的图像将在 Git 存储库中共享,并对具有存储库访问权限的任何人可见。", "description-generate-dashboard-previews": "创建拉取请求的预览链接", @@ -11868,9 +11861,6 @@ "source-code": "源代码" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "只读", "settings": "设置", "view": "查看" @@ -11907,14 +11897,6 @@ "webhook-last-event": "最后一个事件:", "webhook-url": "查看 Webhook" }, - "repository-resources": { - "columns": { - "history": "历史记录", - "view-dashboard": "查看", - "view-folder": "查看" - }, - "placeholder-search": "搜索" - }, "repository-status-page": { "back-to-repositories": "回到存储库", "cleaning-up-resources": "清理存储库资源", @@ -11922,12 +11904,10 @@ "not-found": "找不到", "not-found-message": "找不到存储库", "repository-config-exists-configuration": "确保存储库配置存在于配置文件中。", - "tab-files": "文件", - "tab-files-title": "来自存储库的原始文件列表", "tab-overview": "概述", "tab-overview-title": "存储库概览", "tab-resources": "资源", - "tab-resources-title": "保存在 Grafana 数据库中的资源", + "tab-resources-title": "", "title": "存储库状态", "title-legacy-storage": "传统存储", "title-queued-for-deletion": "已进入队列等待删除" @@ -11950,6 +11930,17 @@ "pure-git": "纯 Git", "pure-git-description": "连接到任何 Git 存储库" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "基本", "dashboard-preview": "数据面板预览", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 1d2f8294d87..561d6b53c3a 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -11653,13 +11653,6 @@ "saving": "正在儲存", "title-error-loading-file": "載入檔案時發生錯誤" }, - "files-view": { - "columns": { - "history": "歷史紀錄", - "view": "檢視" - }, - "placeholder-search": "搜尋" - }, "finish-step": { "description-enable-previews": "在拉取請求中新增儀表板變更的圖片預覽。您的 Grafana 儀表板圖像將在您的 Git 儲存庫中共用,並且任何具有儲存庫存取權限者都可以看見。", "description-generate-dashboard-previews": "建立拉取請求的預覽連結", @@ -11868,9 +11861,6 @@ "source-code": "原始碼" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "唯讀", "settings": "設定", "view": "檢視" @@ -11907,14 +11897,6 @@ "webhook-last-event": "上次事件:", "webhook-url": "檢視 Webhook" }, - "repository-resources": { - "columns": { - "history": "歷史紀錄", - "view-dashboard": "檢視", - "view-folder": "檢視" - }, - "placeholder-search": "搜尋" - }, "repository-status-page": { "back-to-repositories": "返回至儲存庫", "cleaning-up-resources": "清理儲存庫資源", @@ -11922,12 +11904,10 @@ "not-found": "找不到", "not-found-message": "找不到儲存庫", "repository-config-exists-configuration": "請確認儲存庫設定存在於設定檔案中。", - "tab-files": "檔案", - "tab-files-title": "儲存庫中的原始檔案清單", "tab-overview": "概覽", "tab-overview-title": "儲存庫概覽", "tab-resources": "資源", - "tab-resources-title": "資源儲存在 grafana 資料庫中", + "tab-resources-title": "", "title": "儲存庫狀態", "title-legacy-storage": "舊版儲存空間", "title-queued-for-deletion": "已排入刪除佇列" @@ -11950,6 +11930,17 @@ "pure-git": "純 Git", "pure-git-description": "連接到任何 Git 儲存庫" }, + "resource-tree": { + "header-hash": "", + "header-status": "", + "header-title": "", + "header-type": "", + "search-placeholder": "", + "source": "", + "status-pending": "", + "status-synced": "", + "view": "" + }, "resource-view": { "base": "基數", "dashboard-preview": "儀表板預覽", From 34e3c20250577be86dc3994065e4631fa03b9041 Mon Sep 17 00:00:00 2001 From: Eric Shields Date: Thu, 27 Nov 2025 17:19:38 -0800 Subject: [PATCH 159/423] Chore: `tree` is never undefined, so set type to non-optional (#114518) --- .../scopes/selector/ScopesSelectorService.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 9dea729ff88..ede86a50135 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -46,7 +46,7 @@ export interface ScopesSelectorServiceState { // Simple tree structure for the scopes categories. Each node in a tree has a scopeNodeId which keys the nodes cache // map. - tree: TreeNode | undefined; + tree: TreeNode; } export class ScopesSelectorService extends ScopesServiceBase { @@ -116,9 +116,6 @@ export class ScopesSelectorService extends ScopesServiceBase => { - if (!tree) { - throw new Error('Tree is required'); - } const nodePath = await this.getNodePath(scopeNodeId); const newTree = insertPathNodesIntoTree(tree, nodePath); @@ -133,7 +130,7 @@ export class ScopesSelectorService extends ScopesServiceBase { + const newTree = modifyTreeNodeAtPath(this.state.tree, path, (treeNode) => { treeNode.expanded = !nodeToToggle.expanded; treeNode.query = ''; }); @@ -152,7 +149,7 @@ export class ScopesSelectorService extends ScopesServiceBase { + const newTree = modifyTreeNodeAtPath(this.state.tree, path, (treeNode) => { treeNode.expanded = true; treeNode.query = query; }); @@ -209,7 +206,7 @@ export class ScopesSelectorService extends ScopesServiceBase { + const newTree = modifyTreeNodeAtPath(this.state.tree, path, (treeNode) => { // Set parent query only when filtering within existing children treeNode.children = {}; for (const node of childNodes) { @@ -455,12 +452,12 @@ export class ScopesSelectorService extends ScopesServiceBase { - if (!this.state.tree?.children || Object.keys(this.state.tree?.children).length === 0) { + if (!this.state.tree.children || Object.keys(this.state.tree.children).length === 0) { await this.filterNode('', ''); } // First close all nodes - let newTree = closeNodes(this.state.tree!); + let newTree = closeNodes(this.state.tree); if (this.state.selectedScopes.length && this.state.selectedScopes[0].parentNodeId) { let path = getPathOfNode(this.state.selectedScopes[0].parentNodeId, this.state.nodes); From 646fb2aa35dfb13d033087c6baf1538fa861c735 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 28 Nov 2025 07:47:59 +0200 Subject: [PATCH 160/423] Provisioning: Add source link to provisioned dashboards (#114552) * Provisioning: Add dashboard source link * Fix type * Refactor * Simplify code * more fixes * Extract utils * Switch to object params * Fix types * Move to existing file --- public/app/features/dashboard/api/v1.ts | 23 ++++-- public/app/features/dashboard/api/v2.ts | 7 ++ .../Repository/ResourceTreeView.tsx | 15 +++- .../Shared/PreviewBannerViewPR.test.tsx | 4 +- .../components/Shared/PreviewBannerViewPR.tsx | 10 +-- public/app/features/provisioning/guards.ts | 9 +++ public/app/features/provisioning/utils/git.ts | 57 +++++++------- .../features/provisioning/utils/sourceLink.ts | 75 +++++++++++++++++++ public/locales/en-US/grafana.json | 4 + 9 files changed, 159 insertions(+), 45 deletions(-) create mode 100644 public/app/features/provisioning/utils/sourceLink.ts diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index b8c57653923..9414d3531a6 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -6,21 +6,22 @@ import { getFolderByUidFacade } from 'app/api/clients/folder/v1beta1/hooks'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; import { ScopedResourceClient } from 'app/features/apiserver/client'; import { - ResourceClient, - ResourceForCreate, - AnnoKeyMessage, AnnoKeyFolder, AnnoKeyGrantPermissions, - Resource, - DeprecatedInternalId, - AnnoKeyManagerKind, - AnnoKeySourcePath, AnnoKeyManagerAllowsEdits, - ManagerKind, + AnnoKeyManagerKind, + AnnoKeyMessage, + AnnoKeySourcePath, AnnoReloadOnParamsChange, + DeprecatedInternalId, + ManagerKind, + Resource, + ResourceClient, + ResourceForCreate, } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; +import { buildSourceLink } from 'app/features/provisioning/utils/sourceLink'; import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -160,6 +161,12 @@ export class K8sDashboardAPI implements DashboardAPI { result.meta.provisionedExternalId = annotations[AnnoKeySourcePath]; } + // Inject source link for repo-managed dashboards + const sourceLink = await buildSourceLink(annotations); + if (sourceLink) { + result.dashboard.links = [sourceLink, ...(result.dashboard.links || [])]; + } + if (dash.metadata.labels?.[DeprecatedInternalId]) { result.dashboard.id = parseInt(dash.metadata.labels[DeprecatedInternalId], 10); } diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 33ec2323786..ddf85da7338 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -18,6 +18,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; +import { buildSourceLink } from 'app/features/provisioning/utils/sourceLink'; import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -75,6 +76,12 @@ export class K8sDashboardV2API dashboard.metadata.annotations[AnnoKeyFolder] = ''; } + // Inject source link for repo-managed dashboards + const sourceLink = await buildSourceLink(dashboard.metadata.annotations); + if (sourceLink) { + dashboard.spec.links = [sourceLink, ...(dashboard.spec.links || [])]; + } + return dashboard; } catch (e) { const status = getStatusFromError(e); diff --git a/public/app/features/provisioning/Repository/ResourceTreeView.tsx b/public/app/features/provisioning/Repository/ResourceTreeView.tsx index 58282803841..33471a77af6 100644 --- a/public/app/features/provisioning/Repository/ResourceTreeView.tsx +++ b/public/app/features/provisioning/Repository/ResourceTreeView.tsx @@ -140,7 +140,20 @@ export function ResourceTreeView({ repo }: ResourceTreeViewProps) { } const viewLink = getGrafanaLink(item); - const sourceLink = item.hasFile ? getRepoFileUrl(repo.spec, item.path) : undefined; + let sourceLink: string | undefined = undefined; + if (item.hasFile && repo.spec?.type) { + const spec = repo.spec; + const config = spec.github || spec.gitlab || spec.bitbucket; + if (config) { + sourceLink = getRepoFileUrl({ + repoType: spec.type, + url: config.url, + branch: config.branch, + filePath: item.path, + pathPrefix: config.path, + }); + } + } if (!viewLink && !sourceLink) { return null; diff --git a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.test.tsx b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.test.tsx index b742368dbce..2b854ad92d5 100644 --- a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.test.tsx +++ b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.test.tsx @@ -5,7 +5,9 @@ import { textUtil } from '@grafana/data'; import { RepoType } from 'app/features/provisioning/Wizard/types'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { isValidRepoType, PreviewBannerViewPR } from './PreviewBannerViewPR'; +import { isValidRepoType } from '../../guards'; + +import { PreviewBannerViewPR } from './PreviewBannerViewPR'; jest.mock('@grafana/data', () => ({ ...jest.requireActual('@grafana/data'), diff --git a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx index 15b814a14db..932f7152dac 100644 --- a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx +++ b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx @@ -1,7 +1,8 @@ import { textUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, Box, Icon, Stack, TextLink } from '@grafana/ui'; -import { RepoTypeDisplay, RepoType } from 'app/features/provisioning/Wizard/types'; +import { RepoTypeDisplay } from 'app/features/provisioning/Wizard/types'; +import { isValidRepoType } from 'app/features/provisioning/guards'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; import { commonAlertProps } from '../Dashboards/DashboardPreviewBanner'; @@ -121,13 +122,6 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl, b ); } -export function isValidRepoType(repoType: string | undefined): repoType is RepoType { - if (typeof repoType !== 'string') { - return false; - } - return repoType in RepoTypeDisplay; -} - function showBranchInfo(repoType: string | undefined, branchInfo?: PreviewBranchInfo): boolean { const { targetBranch, configuredBranch, repoBaseUrl } = branchInfo || {}; return repoType !== 'local' && !!targetBranch && !!configuredBranch && !!repoBaseUrl; diff --git a/public/app/features/provisioning/guards.ts b/public/app/features/provisioning/guards.ts index 1ac4e9cfdae..c9a6f16cbe2 100644 --- a/public/app/features/provisioning/guards.ts +++ b/public/app/features/provisioning/guards.ts @@ -1,3 +1,5 @@ +import { RepoType, RepoTypeDisplay } from './Wizard/types'; + export interface HttpError extends Error { status?: number; } @@ -9,3 +11,10 @@ export function isSupportedGitProvider(provider: string): provider is 'github' | export function isHttpError(err: unknown): err is HttpError { return err instanceof Error && 'status' in err; } + +export function isValidRepoType(repoType: string | undefined): repoType is RepoType { + if (typeof repoType !== 'string') { + return false; + } + return repoType in RepoTypeDisplay; +} diff --git a/public/app/features/provisioning/utils/git.ts b/public/app/features/provisioning/utils/git.ts index 4fec609c388..ac16e715a70 100644 --- a/public/app/features/provisioning/utils/git.ts +++ b/public/app/features/provisioning/utils/git.ts @@ -101,51 +101,54 @@ export function getHasTokenInstructions(type: RepoType): type is InstructionAvai return type === 'github' || type === 'gitlab' || type === 'bitbucket'; } -export function getRepoFileUrl(spec?: RepositorySpec, filePath?: string) { - if (!spec || !spec.type || !filePath) { +type GetRepoFileUrlParams = { + repoType: RepoType; + url: string | undefined; + branch?: string | undefined; + filePath: string | undefined; + pathPrefix?: string | null; +}; + +/** + * Build a URL to a specific source file in a repository. + * Only works for git providers (GitHub, GitLab, Bitbucket). + */ +export function getRepoFileUrl({ + repoType, + url, + branch, + filePath, + pathPrefix, +}: GetRepoFileUrlParams): string | undefined { + if (!url || !filePath) { return undefined; } - switch (spec.type) { - case 'github': { - const { url, branch, path } = spec.github ?? {}; - if (!url) { - return undefined; - } - const fullPath = path ? `${path}${filePath}` : filePath; + const effectiveBranch = branch || 'main'; + const fullPath = pathPrefix ? `${pathPrefix}${filePath}` : filePath; + + switch (repoType) { + case 'github': return buildRepoUrl({ baseUrl: url, - branch: branch || 'main', + branch: effectiveBranch, providerSegments: ['blob'], path: fullPath, }); - } - case 'gitlab': { - const { url, branch, path } = spec.gitlab ?? {}; - if (!url) { - return undefined; - } - const fullPath = path ? `${path}${filePath}` : filePath; + case 'gitlab': return buildRepoUrl({ baseUrl: url, - branch: branch || 'main', + branch: effectiveBranch, providerSegments: ['-', 'blob'], path: fullPath, }); - } - case 'bitbucket': { - const { url, branch, path } = spec.bitbucket ?? {}; - if (!url) { - return undefined; - } - const fullPath = path ? `${path}${filePath}` : filePath; + case 'bitbucket': return buildRepoUrl({ baseUrl: url, - branch: branch || 'main', + branch: effectiveBranch, providerSegments: ['src'], path: fullPath, }); - } default: return undefined; } diff --git a/public/app/features/provisioning/utils/sourceLink.ts b/public/app/features/provisioning/utils/sourceLink.ts new file mode 100644 index 00000000000..6018f463eab --- /dev/null +++ b/public/app/features/provisioning/utils/sourceLink.ts @@ -0,0 +1,75 @@ +import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { DashboardLink } from '@grafana/schema'; +import { provisioningAPIv0alpha1, RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; +import { + AnnoKeyManagerIdentity, + AnnoKeyManagerKind, + AnnoKeySourcePath, + ManagerKind, + ObjectMeta, +} from 'app/features/apiserver/types'; +import { dispatch } from 'app/store/store'; + +import { RepoTypeDisplay } from '../Wizard/types'; +import { isValidRepoType } from '../guards'; + +import { getHasTokenInstructions, getRepoFileUrl } from './git'; + +/** + * Build a source link for a repo-managed dashboard. + * Returns undefined if the dashboard is not repo-managed or if the repository is not a git provider. + */ +export async function buildSourceLink(annotations: ObjectMeta['annotations']): Promise { + if (!annotations || !config.featureToggles.provisioning || annotations[AnnoKeyManagerKind] !== ManagerKind.Repo) { + return undefined; + } + + const managerIdentity = annotations[AnnoKeyManagerIdentity]; + const sourcePath = annotations[AnnoKeySourcePath]; + if (!managerIdentity || !sourcePath) { + return undefined; + } + + try { + const settingsResult = await dispatch(provisioningAPIv0alpha1.endpoints.getFrontendSettings.initiate()); + const repository = settingsResult.data?.items.find((repo: RepositoryView) => repo.name === managerIdentity); + + if (!repository) { + return undefined; + } + + const repoType = repository.type; + if (!getHasTokenInstructions(repoType) || !isValidRepoType(repoType)) { + return undefined; + } + + const sourceUrl = getRepoFileUrl({ + repoType, + url: repository.url, + branch: repository.branch, + filePath: sourcePath, + pathPrefix: repository.path, + }); + if (!sourceUrl) { + return undefined; + } + + const providerName = RepoTypeDisplay[repoType]; + return { + title: t('dashboard.source-link.title', 'Source ({{provider}})', { provider: providerName }), + type: 'link', + url: sourceUrl, + icon: 'external link', + tooltip: t('dashboard.source-link.tooltip', 'View source file in repository'), + targetBlank: true, + tags: [], + asDropdown: false, + includeVars: false, + keepTime: false, + }; + } catch (e) { + console.warn('Failed to fetch repository info for source link:', e); + return undefined; + } +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a8ed1bdf15c..f2d2366b9cd 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Loading & initializing dashboard", "title-not-found": "Panel with id {{panelId}} not found" }, + "source-link": { + "title": "Source ({{provider}})", + "tooltip": "View source file in repository" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Template variables" }, From 8e7ba60b9333ea6d2728e58ce7b23947d0df8741 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 28 Nov 2025 10:12:50 +0100 Subject: [PATCH 161/423] Zanzana: Team bindings write APIs (#114493) * Zanzana: Team bindings write APIs * Update pkg/services/authz/zanzana/server/server_mutate_teambindings.go Co-authored-by: Gabriel MABILLE * fix missing import * fix linter --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authz/proto/v1/extention.pb.go | 588 ++++++++++++------ pkg/services/authz/proto/v1/extention.proto | 20 + .../authz/zanzana/server/server_mutate.go | 7 + .../server/server_mutate_teambindings.go | 98 +++ .../server/server_mutate_teambindings_test.go | 74 +++ .../authz/zanzana/server/server_test.go | 4 + 6 files changed, 593 insertions(+), 198 deletions(-) create mode 100644 pkg/services/authz/zanzana/server/server_mutate_teambindings.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 358b6a0df95..41be2aaf0c0 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -125,6 +125,8 @@ type MutateOperation struct { // *MutateOperation_AddUserOrgRole // *MutateOperation_CreateRoleBinding // *MutateOperation_DeleteRoleBinding + // *MutateOperation_CreateTeamBinding + // *MutateOperation_DeleteTeamBinding Operation isMutateOperation_Operation `protobuf_oneof:"operation"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -248,6 +250,24 @@ func (x *MutateOperation) GetDeleteRoleBinding() *DeleteRoleBindingOperation { return nil } +func (x *MutateOperation) GetCreateTeamBinding() *CreateTeamBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreateTeamBinding); ok { + return x.CreateTeamBinding + } + } + return nil +} + +func (x *MutateOperation) GetDeleteTeamBinding() *DeleteTeamBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteTeamBinding); ok { + return x.DeleteTeamBinding + } + } + return nil +} + type isMutateOperation_Operation interface { isMutateOperation_Operation() } @@ -288,6 +308,14 @@ type MutateOperation_DeleteRoleBinding struct { DeleteRoleBinding *DeleteRoleBindingOperation `protobuf:"bytes,9,opt,name=delete_role_binding,json=deleteRoleBinding,proto3,oneof"` } +type MutateOperation_CreateTeamBinding struct { + CreateTeamBinding *CreateTeamBindingOperation `protobuf:"bytes,10,opt,name=create_team_binding,json=createTeamBinding,proto3,oneof"` +} + +type MutateOperation_DeleteTeamBinding struct { + DeleteTeamBinding *DeleteTeamBindingOperation `protobuf:"bytes,11,opt,name=delete_team_binding,json=deleteTeamBinding,proto3,oneof"` +} + func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} @@ -306,6 +334,10 @@ func (*MutateOperation_CreateRoleBinding) isMutateOperation_Operation() {} func (*MutateOperation_DeleteRoleBinding) isMutateOperation_Operation() {} +func (*MutateOperation_CreateTeamBinding) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteTeamBinding) isMutateOperation_Operation() {} + type SetFolderParentOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // UID of the folder @@ -843,6 +875,132 @@ func (x *DeleteRoleBindingOperation) GetRoleName() string { return "" } +type CreateTeamBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // uid of the identity + SubjectName string `protobuf:"bytes,1,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // uid of the team + TeamName string `protobuf:"bytes,2,opt,name=team_name,json=teamName,proto3" json:"team_name,omitempty"` + // permission of the identity in the team (admin/member) + Permission string `protobuf:"bytes,3,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTeamBindingOperation) Reset() { + *x = CreateTeamBindingOperation{} + mi := &file_extention_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTeamBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTeamBindingOperation) ProtoMessage() {} + +func (x *CreateTeamBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[12] + 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 CreateTeamBindingOperation.ProtoReflect.Descriptor instead. +func (*CreateTeamBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{12} +} + +func (x *CreateTeamBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *CreateTeamBindingOperation) GetTeamName() string { + if x != nil { + return x.TeamName + } + return "" +} + +func (x *CreateTeamBindingOperation) GetPermission() string { + if x != nil { + return x.Permission + } + return "" +} + +type DeleteTeamBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // uid of the identity + SubjectName string `protobuf:"bytes,1,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // uid of the team + TeamName string `protobuf:"bytes,2,opt,name=team_name,json=teamName,proto3" json:"team_name,omitempty"` + // permission of the identity in the team (admin/member) + Permission string `protobuf:"bytes,3,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTeamBindingOperation) Reset() { + *x = DeleteTeamBindingOperation{} + mi := &file_extention_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTeamBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTeamBindingOperation) ProtoMessage() {} + +func (x *DeleteTeamBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[13] + 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 DeleteTeamBindingOperation.ProtoReflect.Descriptor instead. +func (*DeleteTeamBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{13} +} + +func (x *DeleteTeamBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *DeleteTeamBindingOperation) GetTeamName() string { + if x != nil { + return x.TeamName + } + return "" +} + +func (x *DeleteTeamBindingOperation) GetPermission() string { + if x != nil { + return x.Permission + } + return "" +} + type Resource struct { state protoimpl.MessageState `protogen:"open.v1"` // group of the resource (e.g: "dashboard.grafana.app") @@ -857,7 +1015,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -869,7 +1027,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -882,7 +1040,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{12} + return file_extention_proto_rawDescGZIP(), []int{14} } func (x *Resource) GetGroup() string { @@ -920,7 +1078,7 @@ type Permission struct { func (x *Permission) Reset() { *x = Permission{} - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -932,7 +1090,7 @@ func (x *Permission) String() string { func (*Permission) ProtoMessage() {} func (x *Permission) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -945,7 +1103,7 @@ func (x *Permission) ProtoReflect() protoreflect.Message { // Deprecated: Use Permission.ProtoReflect.Descriptor instead. func (*Permission) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{13} + return file_extention_proto_rawDescGZIP(), []int{15} } func (x *Permission) GetKind() string { @@ -981,7 +1139,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -993,7 +1151,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1006,7 +1164,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{16} } func (x *TupleKey) GetUser() string { @@ -1047,7 +1205,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1059,7 +1217,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1072,7 +1230,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{15} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *Tuple) GetKey() *TupleKey { @@ -1100,7 +1258,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1112,7 +1270,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1125,7 +1283,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{16} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -1159,7 +1317,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1171,7 +1329,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1184,7 +1342,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{17} + return file_extention_proto_rawDescGZIP(), []int{19} } func (x *RelationshipCondition) GetName() string { @@ -1213,7 +1371,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1225,7 +1383,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1238,7 +1396,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{18} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *ReadRequest) GetNamespace() string { @@ -1280,7 +1438,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1292,7 +1450,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1305,7 +1463,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{19} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *ReadRequestTupleKey) GetUser() string { @@ -1339,7 +1497,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1351,7 +1509,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1364,7 +1522,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{20} + return file_extention_proto_rawDescGZIP(), []int{22} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -1390,7 +1548,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1402,7 +1560,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1415,7 +1573,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{21} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -1434,7 +1592,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1446,7 +1604,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1459,7 +1617,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{22} + return file_extention_proto_rawDescGZIP(), []int{24} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -1480,7 +1638,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1492,7 +1650,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1505,7 +1663,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{23} + return file_extention_proto_rawDescGZIP(), []int{25} } func (x *WriteRequest) GetNamespace() string { @@ -1537,7 +1695,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1549,7 +1707,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1562,7 +1720,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{24} + return file_extention_proto_rawDescGZIP(), []int{26} } type BatchCheckRequest struct { @@ -1576,7 +1734,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1588,7 +1746,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1601,7 +1759,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{25} + return file_extention_proto_rawDescGZIP(), []int{27} } func (x *BatchCheckRequest) GetSubject() string { @@ -1639,7 +1797,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1651,7 +1809,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1664,7 +1822,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{26} + return file_extention_proto_rawDescGZIP(), []int{28} } func (x *BatchCheckItem) GetVerb() string { @@ -1718,7 +1876,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1730,7 +1888,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1743,7 +1901,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{27} + return file_extention_proto_rawDescGZIP(), []int{29} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -1762,7 +1920,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1774,7 +1932,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1787,7 +1945,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{28} + return file_extention_proto_rawDescGZIP(), []int{30} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -1807,7 +1965,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1819,7 +1977,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1832,7 +1990,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{29} + return file_extention_proto_rawDescGZIP(), []int{31} } func (x *QueryRequest) GetNamespace() string { @@ -1861,7 +2019,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1873,7 +2031,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1886,7 +2044,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{30} + return file_extention_proto_rawDescGZIP(), []int{32} } func (x *QueryResponse) GetResult() isQueryResponse_Result { @@ -1927,7 +2085,7 @@ type QueryOperation struct { func (x *QueryOperation) Reset() { *x = QueryOperation{} - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1939,7 +2097,7 @@ func (x *QueryOperation) String() string { func (*QueryOperation) ProtoMessage() {} func (x *QueryOperation) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1952,7 +2110,7 @@ func (x *QueryOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. func (*QueryOperation) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{31} + return file_extention_proto_rawDescGZIP(), []int{33} } func (x *QueryOperation) GetOperation() isQueryOperation_Operation { @@ -1991,7 +2149,7 @@ type GetFolderParentsQuery struct { func (x *GetFolderParentsQuery) Reset() { *x = GetFolderParentsQuery{} - mi := &file_extention_proto_msgTypes[32] + mi := &file_extention_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2003,7 +2161,7 @@ func (x *GetFolderParentsQuery) String() string { func (*GetFolderParentsQuery) ProtoMessage() {} func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[32] + mi := &file_extention_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2016,7 +2174,7 @@ func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{32} + return file_extention_proto_rawDescGZIP(), []int{34} } func (x *GetFolderParentsQuery) GetFolder() string { @@ -2036,7 +2194,7 @@ type GetFolderParentsResult struct { func (x *GetFolderParentsResult) Reset() { *x = GetFolderParentsResult{} - mi := &file_extention_proto_msgTypes[33] + mi := &file_extention_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2048,7 +2206,7 @@ func (x *GetFolderParentsResult) String() string { func (*GetFolderParentsResult) ProtoMessage() {} func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[33] + mi := &file_extention_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2061,7 +2219,7 @@ func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{33} + return file_extention_proto_rawDescGZIP(), []int{35} } func (x *GetFolderParentsResult) GetParentUids() []string { @@ -2090,7 +2248,7 @@ var file_extention_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xec, 0x06, 0x0a, 0x0f, 0x4d, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb0, 0x08, 0x0a, 0x0f, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, @@ -2144,74 +2302,102 @@ var file_extention_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x0b, 0x0a, 0x09, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, - 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, - 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, - 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, - 0x22, 0x95, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, 0x0a, 0x13, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x69, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, + 0x0a, 0x13, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, + 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, + 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x22, 0x41, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, + 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, + 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, - 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, - 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, - 0x9c, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, - 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, - 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9c, - 0x01, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, + 0x6f, 0x6c, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, + 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, + 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, + 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, + 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, + 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, + 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, + 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0x7c, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, + 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, + 0x7c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, - 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, - 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, - 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x50, 0x0a, + 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, + 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x50, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, @@ -2417,7 +2603,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -2431,33 +2617,35 @@ var file_extention_proto_goTypes = []any{ (*DeleteUserOrgRoleOperation)(nil), // 9: authz.extention.v1.DeleteUserOrgRoleOperation (*CreateRoleBindingOperation)(nil), // 10: authz.extention.v1.CreateRoleBindingOperation (*DeleteRoleBindingOperation)(nil), // 11: authz.extention.v1.DeleteRoleBindingOperation - (*Resource)(nil), // 12: authz.extention.v1.Resource - (*Permission)(nil), // 13: authz.extention.v1.Permission - (*TupleKey)(nil), // 14: authz.extention.v1.TupleKey - (*Tuple)(nil), // 15: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 16: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 17: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 18: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 19: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 20: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 21: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 22: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 23: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 24: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 25: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 26: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 27: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 28: authz.extention.v1.BatchCheckGroupResource - (*QueryRequest)(nil), // 29: authz.extention.v1.QueryRequest - (*QueryResponse)(nil), // 30: authz.extention.v1.QueryResponse - (*QueryOperation)(nil), // 31: authz.extention.v1.QueryOperation - (*GetFolderParentsQuery)(nil), // 32: authz.extention.v1.GetFolderParentsQuery - (*GetFolderParentsResult)(nil), // 33: authz.extention.v1.GetFolderParentsResult - nil, // 34: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 35: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 37: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 38: google.protobuf.Int32Value + (*CreateTeamBindingOperation)(nil), // 12: authz.extention.v1.CreateTeamBindingOperation + (*DeleteTeamBindingOperation)(nil), // 13: authz.extention.v1.DeleteTeamBindingOperation + (*Resource)(nil), // 14: authz.extention.v1.Resource + (*Permission)(nil), // 15: authz.extention.v1.Permission + (*TupleKey)(nil), // 16: authz.extention.v1.TupleKey + (*Tuple)(nil), // 17: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 18: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 19: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 20: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 21: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 22: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 23: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 24: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 25: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 26: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 27: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 28: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 29: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 30: authz.extention.v1.BatchCheckGroupResource + (*QueryRequest)(nil), // 31: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 32: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 33: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 34: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 35: authz.extention.v1.GetFolderParentsResult + nil, // 36: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 37: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 38: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 39: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 40: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -2470,43 +2658,45 @@ var file_extention_proto_depIdxs = []int32{ 7, // 7: authz.extention.v1.MutateOperation.add_user_org_role:type_name -> authz.extention.v1.AddUserOrgRoleOperation 10, // 8: authz.extention.v1.MutateOperation.create_role_binding:type_name -> authz.extention.v1.CreateRoleBindingOperation 11, // 9: authz.extention.v1.MutateOperation.delete_role_binding:type_name -> authz.extention.v1.DeleteRoleBindingOperation - 12, // 10: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 13, // 11: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 12, // 12: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 13, // 13: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 17, // 14: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 14, // 15: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 36, // 16: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 37, // 17: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 19, // 18: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 38, // 19: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 15, // 20: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 14, // 21: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 16, // 22: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 21, // 23: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 22, // 24: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 26, // 25: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 34, // 26: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 35, // 27: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 31, // 28: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation - 33, // 29: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult - 32, // 30: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery - 28, // 31: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 25, // 32: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 18, // 33: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 23, // 34: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 35: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 29, // 36: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest - 27, // 37: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 20, // 38: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 24, // 39: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 40: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 30, // 41: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse - 37, // [37:42] is the sub-list for method output_type - 32, // [32:37] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 12, // 10: authz.extention.v1.MutateOperation.create_team_binding:type_name -> authz.extention.v1.CreateTeamBindingOperation + 13, // 11: authz.extention.v1.MutateOperation.delete_team_binding:type_name -> authz.extention.v1.DeleteTeamBindingOperation + 14, // 12: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 15, // 13: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 14, // 14: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 15, // 15: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 19, // 16: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 16, // 17: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 38, // 18: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 39, // 19: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 21, // 20: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 40, // 21: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 17, // 22: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 16, // 23: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 18, // 24: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 23, // 25: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 24, // 26: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 28, // 27: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 36, // 28: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 37, // 29: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 33, // 30: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 35, // 31: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 34, // 32: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 30, // 33: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 27, // 34: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 20, // 35: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 25, // 36: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 37: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 31, // 38: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 29, // 39: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 22, // 40: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 26, // 41: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 42: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 32, // 43: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 39, // [39:44] is the sub-list for method output_type + 34, // [34:39] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -2524,11 +2714,13 @@ func file_extention_proto_init() { (*MutateOperation_AddUserOrgRole)(nil), (*MutateOperation_CreateRoleBinding)(nil), (*MutateOperation_DeleteRoleBinding)(nil), + (*MutateOperation_CreateTeamBinding)(nil), + (*MutateOperation_DeleteTeamBinding)(nil), } - file_extention_proto_msgTypes[30].OneofWrappers = []any{ + file_extention_proto_msgTypes[32].OneofWrappers = []any{ (*QueryResponse_FolderParents)(nil), } - file_extention_proto_msgTypes[31].OneofWrappers = []any{ + file_extention_proto_msgTypes[33].OneofWrappers = []any{ (*QueryOperation_GetFolderParents)(nil), } type x struct{} @@ -2537,7 +2729,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 36, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 80886ae13b3..4e33f32db21 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -36,6 +36,8 @@ message MutateOperation { AddUserOrgRoleOperation add_user_org_role = 7; CreateRoleBindingOperation create_role_binding = 8; DeleteRoleBindingOperation delete_role_binding = 9; + CreateTeamBindingOperation create_team_binding = 10; + DeleteTeamBindingOperation delete_team_binding = 11; } } @@ -111,6 +113,24 @@ message DeleteRoleBindingOperation { string role_name = 4; } +message CreateTeamBindingOperation { + // uid of the identity + string subject_name = 1; + // uid of the team + string team_name = 2; + // permission of the identity in the team (admin/member) + string permission = 3; +} + +message DeleteTeamBindingOperation { + // uid of the identity + string subject_name = 1; + // uid of the team + string team_name = 2; + // permission of the identity in the team (admin/member) + string permission = 3; +} + message Resource { // group of the resource (e.g: "dashboard.grafana.app") string group = 1; diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index 45d40df8173..8d3696ceb82 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -16,6 +16,7 @@ const ( OperationGroupPermission OperationGroup = "permission" OperationGroupUserOrgRole OperationGroup = "user_org_role" OperationGroupRoleBinding OperationGroup = "role_binding" + OperationGroupTeamBinding OperationGroup = "team_binding" ) func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { @@ -68,6 +69,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au if err := s.mutateRoleBindings(ctx, storeInf, operations); err != nil { return nil, fmt.Errorf("failed to mutate role bindings: %w", err) } + case OperationGroupTeamBinding: + if err := s.mutateTeamBindings(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate team bindings: %w", err) + } default: s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) } @@ -86,6 +91,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e return OperationGroupUserOrgRole, nil case *authzextv1.MutateOperation_CreateRoleBinding, *authzextv1.MutateOperation_DeleteRoleBinding: return OperationGroupRoleBinding, nil + case *authzextv1.MutateOperation_CreateTeamBinding, *authzextv1.MutateOperation_DeleteTeamBinding: + return OperationGroupTeamBinding, nil } return OperationGroup(""), errors.New("unsupported mutate operation type") } diff --git a/pkg/services/authz/zanzana/server/server_mutate_teambindings.go b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go new file mode 100644 index 00000000000..81e1c9cb437 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go @@ -0,0 +1,98 @@ +package server + +import ( + "context" + "errors" + "fmt" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateTeamBindings(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateTeamBindings") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreateTeamBinding: + tuple, err := s.getTeamBindingTuple(ctx, op.CreateTeamBinding.GetSubjectName(), op.CreateTeamBinding.GetTeamName(), op.CreateTeamBinding.GetPermission()) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeleteTeamBinding: + tuple, err := s.getTeamBindingTuple(ctx, op.DeleteTeamBinding.GetSubjectName(), op.DeleteTeamBinding.GetTeamName(), op.DeleteTeamBinding.GetPermission()) + if err != nil { + return err + } + deleteTuple := &openfgav1.TupleKeyWithoutCondition{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } + deleteTuples = append(deleteTuples, deleteTuple) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource role binding tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getTeamBindingTuple(ctx context.Context, subject string, team string, permission string) (*openfgav1.TupleKey, error) { + if subject == "" { + return nil, errors.New("subject name cannot be empty") + } + + if team == "" { + return nil, errors.New("team name cannot be empty") + } + + relation := "" + switch permission { + case string(iamv0.TeamBindingTeamPermissionAdmin): + relation = zanzana.RelationTeamAdmin + case string(iamv0.TeamBindingTeamPermissionMember): + relation = zanzana.RelationTeamMember + default: + return nil, fmt.Errorf("unknown team permission '%s', expected member or admin", permission) + } + + tuple := &openfgav1.TupleKey{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, subject, ""), + Relation: relation, + Object: zanzana.NewTupleEntry(zanzana.TypeTeam, team, ""), + } + + return tuple, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go new file mode 100644 index 00000000000..5103b142fc5 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_teambindings_test.go @@ -0,0 +1,74 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateTeamBindings(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewTuple("user:1", common.RelationTeamMember, "team:foo"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateTeamBindings(t *testing.T, srv *Server) { + setupMutateTeamBindings(t, srv) + + t.Run("should update user team binding and delete old team binding", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "1", + TeamName: "foo", + Permission: "admin", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "1", + TeamName: "foo", + Permission: "member", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationTeamAdmin, + Object: "team:foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationTeamMember, + Object: "team:foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 7275fd411af..514930bf2ba 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -140,6 +140,10 @@ func TestIntegrationServer(t *testing.T) { t.Run("test mutate role bindings", func(t *testing.T) { testMutateRoleBindings(t, srv) }) + + t.Run("test mutate team bindings", func(t *testing.T) { + testMutateTeamBindings(t, srv) + }) } func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { From 358d0eb266c6248efd7be2d41146a1d947dae558 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 28 Nov 2025 11:44:58 +0100 Subject: [PATCH 162/423] Zanzana: Role write APIs (#114533) * Zanzana: Role write APIs * Add tests * Update pkg/services/authz/zanzana/server/server_mutate_roles.go Co-authored-by: Gabriel MABILLE * fix func usage --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authz/proto/v1/extention.pb.go | 1046 ++++++++++------- pkg/services/authz/proto/v1/extention.proto | 25 + pkg/services/authz/zanzana/common/tuple.go | 8 + .../authz/zanzana/server/server_mutate.go | 7 + .../zanzana/server/server_mutate_roles.go | 108 ++ .../server/server_mutate_roles_test.go | 77 ++ .../authz/zanzana/server/server_test.go | 4 + 7 files changed, 878 insertions(+), 397 deletions(-) create mode 100644 pkg/services/authz/zanzana/server/server_mutate_roles.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_roles_test.go diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 41be2aaf0c0..deff13d5edd 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -127,6 +127,8 @@ type MutateOperation struct { // *MutateOperation_DeleteRoleBinding // *MutateOperation_CreateTeamBinding // *MutateOperation_DeleteTeamBinding + // *MutateOperation_CreateRole + // *MutateOperation_DeleteRole Operation isMutateOperation_Operation `protobuf_oneof:"operation"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -268,6 +270,24 @@ func (x *MutateOperation) GetDeleteTeamBinding() *DeleteTeamBindingOperation { return nil } +func (x *MutateOperation) GetCreateRole() *CreateRoleOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreateRole); ok { + return x.CreateRole + } + } + return nil +} + +func (x *MutateOperation) GetDeleteRole() *DeleteRoleOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteRole); ok { + return x.DeleteRole + } + } + return nil +} + type isMutateOperation_Operation interface { isMutateOperation_Operation() } @@ -316,6 +336,14 @@ type MutateOperation_DeleteTeamBinding struct { DeleteTeamBinding *DeleteTeamBindingOperation `protobuf:"bytes,11,opt,name=delete_team_binding,json=deleteTeamBinding,proto3,oneof"` } +type MutateOperation_CreateRole struct { + CreateRole *CreateRoleOperation `protobuf:"bytes,12,opt,name=create_role,json=createRole,proto3,oneof"` +} + +type MutateOperation_DeleteRole struct { + DeleteRole *DeleteRoleOperation `protobuf:"bytes,13,opt,name=delete_role,json=deleteRole,proto3,oneof"` +} + func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} @@ -338,6 +366,10 @@ func (*MutateOperation_CreateTeamBinding) isMutateOperation_Operation() {} func (*MutateOperation_DeleteTeamBinding) isMutateOperation_Operation() {} +func (*MutateOperation_CreateRole) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteRole) isMutateOperation_Operation() {} + type SetFolderParentOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // UID of the folder @@ -1001,6 +1033,184 @@ func (x *DeleteTeamBindingOperation) GetPermission() string { return "" } +type CreateRoleOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,1,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,2,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + // permissions of the role + Permissions []*RolePermission `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRoleOperation) Reset() { + *x = CreateRoleOperation{} + mi := &file_extention_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRoleOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRoleOperation) ProtoMessage() {} + +func (x *CreateRoleOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[14] + 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 CreateRoleOperation.ProtoReflect.Descriptor instead. +func (*CreateRoleOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{14} +} + +func (x *CreateRoleOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *CreateRoleOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + +func (x *CreateRoleOperation) GetPermissions() []*RolePermission { + if x != nil { + return x.Permissions + } + return nil +} + +type DeleteRoleOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,1,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,2,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + // permissions of the role + Permissions []*RolePermission `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRoleOperation) Reset() { + *x = DeleteRoleOperation{} + mi := &file_extention_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRoleOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRoleOperation) ProtoMessage() {} + +func (x *DeleteRoleOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[15] + 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 DeleteRoleOperation.ProtoReflect.Descriptor instead. +func (*DeleteRoleOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{15} +} + +func (x *DeleteRoleOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *DeleteRoleOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + +func (x *DeleteRoleOperation) GetPermissions() []*RolePermission { + if x != nil { + return x.Permissions + } + return nil +} + +type RolePermission struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` + Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RolePermission) Reset() { + *x = RolePermission{} + mi := &file_extention_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RolePermission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RolePermission) ProtoMessage() {} + +func (x *RolePermission) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[16] + 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 RolePermission.ProtoReflect.Descriptor instead. +func (*RolePermission) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{16} +} + +func (x *RolePermission) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *RolePermission) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + type Resource struct { state protoimpl.MessageState `protogen:"open.v1"` // group of the resource (e.g: "dashboard.grafana.app") @@ -1015,7 +1225,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1027,7 +1237,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1040,7 +1250,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *Resource) GetGroup() string { @@ -1078,7 +1288,7 @@ type Permission struct { func (x *Permission) Reset() { *x = Permission{} - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1090,7 +1300,7 @@ func (x *Permission) String() string { func (*Permission) ProtoMessage() {} func (x *Permission) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1103,7 +1313,7 @@ func (x *Permission) ProtoReflect() protoreflect.Message { // Deprecated: Use Permission.ProtoReflect.Descriptor instead. func (*Permission) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{15} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *Permission) GetKind() string { @@ -1139,7 +1349,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1151,7 +1361,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1164,7 +1374,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{16} + return file_extention_proto_rawDescGZIP(), []int{19} } func (x *TupleKey) GetUser() string { @@ -1205,7 +1415,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1217,7 +1427,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1230,7 +1440,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{17} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *Tuple) GetKey() *TupleKey { @@ -1258,7 +1468,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1480,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1493,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{18} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -1317,7 +1527,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1329,7 +1539,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1342,7 +1552,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{19} + return file_extention_proto_rawDescGZIP(), []int{22} } func (x *RelationshipCondition) GetName() string { @@ -1371,7 +1581,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1383,7 +1593,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1396,7 +1606,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{20} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *ReadRequest) GetNamespace() string { @@ -1438,7 +1648,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1450,7 +1660,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1463,7 +1673,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{21} + return file_extention_proto_rawDescGZIP(), []int{24} } func (x *ReadRequestTupleKey) GetUser() string { @@ -1497,7 +1707,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1509,7 +1719,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1522,7 +1732,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{22} + return file_extention_proto_rawDescGZIP(), []int{25} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -1548,7 +1758,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1560,7 +1770,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1573,7 +1783,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{23} + return file_extention_proto_rawDescGZIP(), []int{26} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -1592,7 +1802,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1604,7 +1814,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1617,7 +1827,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{24} + return file_extention_proto_rawDescGZIP(), []int{27} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -1638,7 +1848,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1650,7 +1860,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1663,7 +1873,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{25} + return file_extention_proto_rawDescGZIP(), []int{28} } func (x *WriteRequest) GetNamespace() string { @@ -1695,7 +1905,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1707,7 +1917,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1720,7 +1930,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{26} + return file_extention_proto_rawDescGZIP(), []int{29} } type BatchCheckRequest struct { @@ -1734,7 +1944,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1746,7 +1956,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1759,7 +1969,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{27} + return file_extention_proto_rawDescGZIP(), []int{30} } func (x *BatchCheckRequest) GetSubject() string { @@ -1797,7 +2007,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1809,7 +2019,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1822,7 +2032,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{28} + return file_extention_proto_rawDescGZIP(), []int{31} } func (x *BatchCheckItem) GetVerb() string { @@ -1876,7 +2086,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1888,7 +2098,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1901,7 +2111,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{29} + return file_extention_proto_rawDescGZIP(), []int{32} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -1920,7 +2130,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1932,7 +2142,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1945,7 +2155,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{30} + return file_extention_proto_rawDescGZIP(), []int{33} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -1965,7 +2175,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1977,7 +2187,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1990,7 +2200,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{31} + return file_extention_proto_rawDescGZIP(), []int{34} } func (x *QueryRequest) GetNamespace() string { @@ -2019,7 +2229,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_extention_proto_msgTypes[32] + mi := &file_extention_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2031,7 +2241,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[32] + mi := &file_extention_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2044,7 +2254,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{32} + return file_extention_proto_rawDescGZIP(), []int{35} } func (x *QueryResponse) GetResult() isQueryResponse_Result { @@ -2085,7 +2295,7 @@ type QueryOperation struct { func (x *QueryOperation) Reset() { *x = QueryOperation{} - mi := &file_extention_proto_msgTypes[33] + mi := &file_extention_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2097,7 +2307,7 @@ func (x *QueryOperation) String() string { func (*QueryOperation) ProtoMessage() {} func (x *QueryOperation) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[33] + mi := &file_extention_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2110,7 +2320,7 @@ func (x *QueryOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. func (*QueryOperation) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{33} + return file_extention_proto_rawDescGZIP(), []int{36} } func (x *QueryOperation) GetOperation() isQueryOperation_Operation { @@ -2149,7 +2359,7 @@ type GetFolderParentsQuery struct { func (x *GetFolderParentsQuery) Reset() { *x = GetFolderParentsQuery{} - mi := &file_extention_proto_msgTypes[34] + mi := &file_extention_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2161,7 +2371,7 @@ func (x *GetFolderParentsQuery) String() string { func (*GetFolderParentsQuery) ProtoMessage() {} func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[34] + mi := &file_extention_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2174,7 +2384,7 @@ func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{34} + return file_extention_proto_rawDescGZIP(), []int{37} } func (x *GetFolderParentsQuery) GetFolder() string { @@ -2194,7 +2404,7 @@ type GetFolderParentsResult struct { func (x *GetFolderParentsResult) Reset() { *x = GetFolderParentsResult{} - mi := &file_extention_proto_msgTypes[35] + mi := &file_extention_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2206,7 +2416,7 @@ func (x *GetFolderParentsResult) String() string { func (*GetFolderParentsResult) ProtoMessage() {} func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[35] + mi := &file_extention_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2219,7 +2429,7 @@ func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{35} + return file_extention_proto_rawDescGZIP(), []int{38} } func (x *GetFolderParentsResult) GetParentUids() []string { @@ -2248,7 +2458,7 @@ var file_extention_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb0, 0x08, 0x0a, 0x0f, 0x4d, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc8, 0x09, 0x0a, 0x0f, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, @@ -2315,32 +2525,32 @@ var file_extention_proto_rawDesc = string([]byte{ 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, - 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, - 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, - 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, - 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x4a, 0x0a, 0x0b, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, + 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, + 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, + 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, @@ -2349,246 +2559,279 @@ var file_extention_proto_rawDesc = string([]byte{ 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, - 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, - 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, - 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, - 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, - 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, - 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, - 0x6f, 0x6c, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, - 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, - 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, - 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, - 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, - 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, - 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x22, 0x7c, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, - 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, - 0x7c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, - 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, + 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, + 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, + 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, + 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, - 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x50, 0x0a, - 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, - 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, - 0x48, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, - 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x12, 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, - 0x0a, 0x15, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, - 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, - 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, - 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, - 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, - 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, - 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, - 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, - 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, - 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, - 0x70, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x75, 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, - 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, - 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, - 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, - 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, - 0x62, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, - 0x65, 0x79, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, - 0x65, 0x73, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, - 0x01, 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, - 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, - 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, - 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, + 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x7c, 0x0a, 0x1a, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, + 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, + 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, + 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, + 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, + 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0xa1, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, - 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, - 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, - 0x10, 0x67, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, - 0x0a, 0x15, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, - 0x39, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, - 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, - 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x95, 0x01, + 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, + 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, + 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x44, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3e, 0x0a, 0x0e, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x22, 0x50, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x48, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, + 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, + 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, + 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, + 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, + 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, + 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, + 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, + 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, - 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, - 0x75, 0x65, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 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, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, + 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, + 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, + 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, + 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, + 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, + 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, + 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, + 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, 0x10, 0x67, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, + 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x39, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, + 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, + 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x38, 0x5a, 0x36, 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, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, }) var ( @@ -2603,7 +2846,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 38) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -2619,33 +2862,36 @@ var file_extention_proto_goTypes = []any{ (*DeleteRoleBindingOperation)(nil), // 11: authz.extention.v1.DeleteRoleBindingOperation (*CreateTeamBindingOperation)(nil), // 12: authz.extention.v1.CreateTeamBindingOperation (*DeleteTeamBindingOperation)(nil), // 13: authz.extention.v1.DeleteTeamBindingOperation - (*Resource)(nil), // 14: authz.extention.v1.Resource - (*Permission)(nil), // 15: authz.extention.v1.Permission - (*TupleKey)(nil), // 16: authz.extention.v1.TupleKey - (*Tuple)(nil), // 17: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 18: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 19: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 20: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 21: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 22: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 23: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 24: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 25: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 26: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 27: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 28: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 29: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 30: authz.extention.v1.BatchCheckGroupResource - (*QueryRequest)(nil), // 31: authz.extention.v1.QueryRequest - (*QueryResponse)(nil), // 32: authz.extention.v1.QueryResponse - (*QueryOperation)(nil), // 33: authz.extention.v1.QueryOperation - (*GetFolderParentsQuery)(nil), // 34: authz.extention.v1.GetFolderParentsQuery - (*GetFolderParentsResult)(nil), // 35: authz.extention.v1.GetFolderParentsResult - nil, // 36: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 37: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 38: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 39: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 40: google.protobuf.Int32Value + (*CreateRoleOperation)(nil), // 14: authz.extention.v1.CreateRoleOperation + (*DeleteRoleOperation)(nil), // 15: authz.extention.v1.DeleteRoleOperation + (*RolePermission)(nil), // 16: authz.extention.v1.RolePermission + (*Resource)(nil), // 17: authz.extention.v1.Resource + (*Permission)(nil), // 18: authz.extention.v1.Permission + (*TupleKey)(nil), // 19: authz.extention.v1.TupleKey + (*Tuple)(nil), // 20: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 21: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 22: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 23: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 24: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 25: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 26: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 27: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 28: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 29: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 30: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 31: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 32: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 33: authz.extention.v1.BatchCheckGroupResource + (*QueryRequest)(nil), // 34: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 35: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 36: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 37: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 38: authz.extention.v1.GetFolderParentsResult + nil, // 39: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 40: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 42: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 43: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -2660,43 +2906,47 @@ var file_extention_proto_depIdxs = []int32{ 11, // 9: authz.extention.v1.MutateOperation.delete_role_binding:type_name -> authz.extention.v1.DeleteRoleBindingOperation 12, // 10: authz.extention.v1.MutateOperation.create_team_binding:type_name -> authz.extention.v1.CreateTeamBindingOperation 13, // 11: authz.extention.v1.MutateOperation.delete_team_binding:type_name -> authz.extention.v1.DeleteTeamBindingOperation - 14, // 12: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 15, // 13: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 14, // 14: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 15, // 15: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 19, // 16: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 16, // 17: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 38, // 18: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 39, // 19: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 21, // 20: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 40, // 21: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 17, // 22: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 16, // 23: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 18, // 24: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 23, // 25: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 24, // 26: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 28, // 27: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 36, // 28: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 37, // 29: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 33, // 30: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation - 35, // 31: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult - 34, // 32: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery - 30, // 33: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 27, // 34: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 20, // 35: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 25, // 36: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 37: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 31, // 38: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest - 29, // 39: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 22, // 40: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 26, // 41: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 42: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 32, // 43: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse - 39, // [39:44] is the sub-list for method output_type - 34, // [34:39] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 14, // 12: authz.extention.v1.MutateOperation.create_role:type_name -> authz.extention.v1.CreateRoleOperation + 15, // 13: authz.extention.v1.MutateOperation.delete_role:type_name -> authz.extention.v1.DeleteRoleOperation + 17, // 14: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 18, // 15: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 17, // 16: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 18, // 17: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 16, // 18: authz.extention.v1.CreateRoleOperation.permissions:type_name -> authz.extention.v1.RolePermission + 16, // 19: authz.extention.v1.DeleteRoleOperation.permissions:type_name -> authz.extention.v1.RolePermission + 22, // 20: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 19, // 21: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 41, // 22: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 42, // 23: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 24, // 24: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 43, // 25: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 20, // 26: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 19, // 27: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 21, // 28: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 26, // 29: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 27, // 30: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 31, // 31: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 39, // 32: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 40, // 33: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 36, // 34: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 38, // 35: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 37, // 36: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 33, // 37: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 30, // 38: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 23, // 39: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 28, // 40: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 41: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 34, // 42: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 32, // 43: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 25, // 44: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 29, // 45: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 46: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 35, // 47: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 43, // [43:48] is the sub-list for method output_type + 38, // [38:43] is the sub-list for method input_type + 38, // [38:38] is the sub-list for extension type_name + 38, // [38:38] is the sub-list for extension extendee + 0, // [0:38] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -2716,11 +2966,13 @@ func file_extention_proto_init() { (*MutateOperation_DeleteRoleBinding)(nil), (*MutateOperation_CreateTeamBinding)(nil), (*MutateOperation_DeleteTeamBinding)(nil), + (*MutateOperation_CreateRole)(nil), + (*MutateOperation_DeleteRole)(nil), } - file_extention_proto_msgTypes[32].OneofWrappers = []any{ + file_extention_proto_msgTypes[35].OneofWrappers = []any{ (*QueryResponse_FolderParents)(nil), } - file_extention_proto_msgTypes[33].OneofWrappers = []any{ + file_extention_proto_msgTypes[36].OneofWrappers = []any{ (*QueryOperation_GetFolderParents)(nil), } type x struct{} @@ -2729,7 +2981,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 38, + NumMessages: 41, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 4e33f32db21..cb59e349f64 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -38,6 +38,8 @@ message MutateOperation { DeleteRoleBindingOperation delete_role_binding = 9; CreateTeamBindingOperation create_team_binding = 10; DeleteTeamBindingOperation delete_team_binding = 11; + CreateRoleOperation create_role = 12; + DeleteRoleOperation delete_role = 13; } } @@ -131,6 +133,29 @@ message DeleteTeamBindingOperation { string permission = 3; } +message CreateRoleOperation { + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 1; + // uid of the role + string role_name = 2; + // permissions of the role + repeated RolePermission permissions = 3; +} + +message DeleteRoleOperation { + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 1; + // uid of the role + string role_name = 2; + // permissions of the role + repeated RolePermission permissions = 3; +} + +message RolePermission { + string action = 1; + string scope = 2; +} + message Resource { // group of the resource (e.g: "dashboard.grafana.app") string group = 1; diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 79efce7373d..b1b6499dcd2 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -447,6 +447,14 @@ func ToOpenFGATuples(tuples []*authzextv1.Tuple) []*openfgav1.Tuple { return result } +func ToOpenFGADeleteTupleKey(tuples *openfgav1.TupleKey) *openfgav1.TupleKeyWithoutCondition { + return &openfgav1.TupleKeyWithoutCondition{ + User: tuples.GetUser(), + Relation: tuples.GetRelation(), + Object: tuples.GetObject(), + } +} + func AddRenderContext(req *openfgav1.CheckRequest) { if req.ContextualTuples == nil { req.ContextualTuples = &openfgav1.ContextualTupleKeys{} diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index 8d3696ceb82..cdd8c5b2f50 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -17,6 +17,7 @@ const ( OperationGroupUserOrgRole OperationGroup = "user_org_role" OperationGroupRoleBinding OperationGroup = "role_binding" OperationGroupTeamBinding OperationGroup = "team_binding" + OperationGroupRole OperationGroup = "role" ) func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { @@ -73,6 +74,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au if err := s.mutateTeamBindings(ctx, storeInf, operations); err != nil { return nil, fmt.Errorf("failed to mutate team bindings: %w", err) } + case OperationGroupRole: + if err := s.mutateRoles(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate roles: %w", err) + } default: s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) } @@ -93,6 +98,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e return OperationGroupRoleBinding, nil case *authzextv1.MutateOperation_CreateTeamBinding, *authzextv1.MutateOperation_DeleteTeamBinding: return OperationGroupTeamBinding, nil + case *authzextv1.MutateOperation_CreateRole, *authzextv1.MutateOperation_DeleteRole: + return OperationGroupRole, nil } return OperationGroup(""), errors.New("unsupported mutate operation type") } diff --git a/pkg/services/authz/zanzana/server/server_mutate_roles.go b/pkg/services/authz/zanzana/server/server_mutate_roles.go new file mode 100644 index 00000000000..4c19b1fd288 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_roles.go @@ -0,0 +1,108 @@ +package server + +import ( + "context" + "strings" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateRoles(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateRoles") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreateRole: + tuples, err := convertRoleToTuples(op.CreateRole.RoleName, op.CreateRole.Permissions) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuples...) + case *authzextv1.MutateOperation_DeleteRole: + tuples, err := convertRoleToTuples(op.DeleteRole.RoleName, op.DeleteRole.Permissions) + if err != nil { + return err + } + deletes := make([]*openfgav1.TupleKeyWithoutCondition, 0, len(tuples)) + for _, tuple := range tuples { + deletes = append(deletes, common.ToOpenFGADeleteTupleKey(tuple)) + } + deleteTuples = append(deleteTuples, deletes...) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource role binding tuples", "error", err) + return err + } + + return nil +} + +// convertRoleToTuples converts role and its permissions (action/scope) to v1 TupleKey format +// using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys +func convertRoleToTuples(roleUID string, permissions []*authzextv1.RolePermission) ([]*openfgav1.TupleKey, error) { + // Convert to zanzana.RolePermission + rolePerms := make([]zanzana.RolePermission, 0, len(permissions)) + for _, perm := range permissions { + // Split the scope to get kind, attribute, identifier + kind, _, identifier := splitScope(perm.Scope) + rolePerms = append(rolePerms, zanzana.RolePermission{ + Action: perm.Action, + Kind: kind, + Identifier: identifier, + }) + } + + // Translate to Zanzana tuples + tuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, rolePerms) + if err != nil { + return nil, err + } + + return tuples, nil +} + +func splitScope(scope string) (string, string, string) { + if scope == "" { + return "", "", "" + } + + fragments := strings.Split(scope, ":") + switch l := len(fragments); l { + case 1: // Splitting a wildcard scope "*" -> kind: "*"; attribute: "*"; identifier: "*" + return fragments[0], fragments[0], fragments[0] + case 2: // Splitting a wildcard scope with specified kind "dashboards:*" -> kind: "dashboards"; attribute: "*"; identifier: "*" + return fragments[0], fragments[1], fragments[1] + default: // Splitting a scope with all fields specified "dashboards:uid:my_dash" -> kind: "dashboards"; attribute: "uid"; identifier: "my_dash" + return fragments[0], fragments[1], strings.Join(fragments[2:], ":") + } +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_roles_test.go b/pkg/services/authz/zanzana/server/server_mutate_roles_test.go new file mode 100644 index 00000000000..1128c0c0f72 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_roles_test.go @@ -0,0 +1,77 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateRoles(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewTuple("role:foo_viewer#assignee", "view", "group_resource:dashboard.grafana.app/dashboards"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateRoles(t *testing.T, srv *Server) { + setupMutateRoles(t, srv) + + t.Run("should update role and delete old role permissions", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateRole{ + CreateRole: &v1.CreateRoleOperation{ + RoleName: "foo_viewer", + RoleKind: "Role", + Permissions: []*v1.RolePermission{ + { + Action: "dashboards:edit", + Scope: "dashboards:*", + }, + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteRole{ + DeleteRole: &v1.DeleteRoleOperation{ + RoleName: "foo_viewer", + RoleKind: "Role", + Permissions: []*v1.RolePermission{ + { + Action: "dashboards:view", + Scope: "dashboards:*", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + User: "role:foo_viewer#assignee", + Relation: "edit", + Object: "group_resource:", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "role:foo_viewer#assignee", res.Tuples[0].Key.User) + require.Equal(t, "group_resource:dashboard.grafana.app/dashboards", res.Tuples[0].Key.Object) + require.Equal(t, "edit", res.Tuples[0].Key.Relation) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 514930bf2ba..14e8f629729 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -144,6 +144,10 @@ func TestIntegrationServer(t *testing.T) { t.Run("test mutate team bindings", func(t *testing.T) { testMutateTeamBindings(t, srv) }) + + t.Run("test mutate roles", func(t *testing.T) { + testMutateRoles(t, srv) + }) } func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { From 725df38dade6bca11c2f6248e57233cf98910db1 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 28 Nov 2025 11:45:14 +0100 Subject: [PATCH 163/423] Zanzana: Use team bindings write APIs on the client side (#114503) * Zanzana: Use team bindings write APIs on the client side * fix linter * remove unused code * Apply suggestions from code review Co-authored-by: Gabriel MABILLE * fix syntax --------- Co-authored-by: Gabriel MABILLE --- .../apis/iam/resource_permission_hooks.go | 3 - pkg/registry/apis/iam/team_binding_hooks.go | 206 ++----- .../apis/iam/team_binding_hooks_test.go | 516 ++++++++---------- 3 files changed, 280 insertions(+), 445 deletions(-) diff --git a/pkg/registry/apis/iam/resource_permission_hooks.go b/pkg/registry/apis/iam/resource_permission_hooks.go index e725726b2ec..010bb40049b 100644 --- a/pkg/registry/apis/iam/resource_permission_hooks.go +++ b/pkg/registry/apis/iam/resource_permission_hooks.go @@ -2,7 +2,6 @@ package iam import ( "context" - "errors" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,8 +13,6 @@ import ( ) var ( - errEmptyName = errors.New("name cannot be empty") - defaultWriteTimeout = 15 * time.Second ) diff --git a/pkg/registry/apis/iam/team_binding_hooks.go b/pkg/registry/apis/iam/team_binding_hooks.go index dcad58cae0a..298237df70a 100644 --- a/pkg/registry/apis/iam/team_binding_hooks.go +++ b/pkg/registry/apis/iam/team_binding_hooks.go @@ -10,42 +10,8 @@ import ( iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana" ) -// convertTeamBindingToTuple converts a TeamBinding to a v1 TupleKey format -// TeamBinding represents a user's membership in a team with a specific permission level -func convertTeamBindingToTuple(tb *iamv0.TeamBinding) (*v1.TupleKey, error) { - if tb.Spec.Subject.Name == "" { - return nil, errEmptyName - } - - if tb.Spec.TeamRef.Name == "" { - return nil, errEmptyName - } - - // Map permission to relation - var relation string - switch tb.Spec.Permission { - case iamv0.TeamBindingTeamPermissionAdmin: - relation = zanzana.RelationTeamAdmin - case iamv0.TeamBindingTeamPermissionMember: - relation = zanzana.RelationTeamMember - default: - // Default to member if unknown permission - relation = zanzana.RelationTeamMember - } - - // Create tuple: user:{subjectUID} has {relation} relation to team:{teamUID} - tuple := &v1.TupleKey{ - User: zanzana.NewTupleEntry(zanzana.TypeUser, tb.Spec.Subject.Name, ""), - Relation: relation, - Object: zanzana.NewTupleEntry(zanzana.TypeTeam, tb.Spec.TeamRef.Name, ""), - } - - return tuple, nil -} - // AfterTeamBindingCreate is a post-create hook that writes the team binding to Zanzana (openFGA) func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime.Object, _ *metav1.CreateOptions) { if b.zClient == nil { @@ -79,20 +45,6 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime. hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() }() - tuple, err := convertTeamBindingToTuple(tb) - if err != nil { - b.logger.Error("failed to convert team binding to tuple", - "namespace", tb.Namespace, - "name", tb.Name, - "subject", tb.Spec.Subject.Name, - "teamRef", tb.Spec.TeamRef.Name, - "permission", tb.Spec.Permission, - "err", err, - ) - status = "failure" - return - } - b.logger.Debug("writing team binding to zanzana", "namespace", tb.Namespace, "name", tb.Name, @@ -104,12 +56,21 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime. ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) defer cancel() - err = b.zClient.Write(ctx, &v1.WriteRequest{ + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ Namespace: tb.Namespace, - Writes: &v1.WriteRequestWrites{ - TupleKeys: []*v1.TupleKey{tuple}, + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: tb.Spec.Subject.Name, + TeamName: tb.Spec.TeamRef.Name, + Permission: string(tb.Spec.Permission), + }, + }, + }, }, }) + if err != nil { status = "failure" b.logger.Error("failed to write team binding to zanzana", @@ -159,34 +120,28 @@ func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context. return nil, nil } - // Convert old team binding to tuple for deletion - var oldTuple *v1.TupleKey - var oldErr error - if oldTB.Spec.Subject.Name != "" && oldTB.Spec.TeamRef.Name != "" { - oldTuple, oldErr = convertTeamBindingToTuple(oldTB) - if oldErr != nil { - b.logger.Error("failed to convert old team binding to tuple", - "namespace", oldTB.Namespace, - "name", oldTB.Name, - "err", oldErr, - ) - return nil, nil - } - } - - // Convert new team binding to tuple for writing - var newTuple *v1.TupleKey - var newErr error - if newTB.Spec.Subject.Name != "" && newTB.Spec.TeamRef.Name != "" { - newTuple, newErr = convertTeamBindingToTuple(newTB) - if newErr != nil { - b.logger.Error("failed to convert new team binding to tuple", - "namespace", newTB.Namespace, - "name", newTB.Name, - "err", newErr, - ) - return nil, nil - } + operations := make([]*v1.MutateOperation, 0, 2) + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: oldTB.Spec.Subject.Name, + TeamName: oldTB.Spec.TeamRef.Name, + Permission: string(oldTB.Spec.Permission), + }, + }, + }) + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: newTB.Spec.Subject.Name, + TeamName: newTB.Spec.TeamRef.Name, + Permission: string(newTB.Spec.Permission), + }, + }, + }) + if len(operations) == 0 { + b.logger.Debug("no updates to team binding in zanzana", "namespace", newTB.Namespace, "name", newTB.Name) + return func(ctx context.Context, success bool) {}, nil } // Return a finish function that performs the zanzana write only on success @@ -224,57 +179,22 @@ func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context. ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) defer cancel() - // Prepare write request - req := &v1.WriteRequest{ - Namespace: newTB.Namespace, - } - - // Add delete for old tuple - if oldTuple != nil && oldErr == nil { - deleteTuple := toTupleKeysWithoutCondition([]*v1.TupleKey{oldTuple}) - req.Deletes = &v1.WriteRequestDeletes{ - TupleKeys: deleteTuple, - } - b.logger.Debug("deleting existing team binding from zanzana", - "namespace", newTB.Namespace, - "subject", oldTB.Spec.Subject.Name, - "teamRef", oldTB.Spec.TeamRef.Name, - ) - } - - // Add write for new tuple - if newTuple != nil && newErr == nil { - req.Writes = &v1.WriteRequestWrites{ - TupleKeys: []*v1.TupleKey{newTuple}, - } - b.logger.Debug("writing new team binding to zanzana", - "namespace", newTB.Namespace, - "subject", newTB.Spec.Subject.Name, - "teamRef", newTB.Spec.TeamRef.Name, - ) - } - // Only make the request if there are deletes or writes - if (req.Deletes != nil && len(req.Deletes.TupleKeys) > 0) || (req.Writes != nil && len(req.Writes.TupleKeys) > 0) { - err := b.zClient.Write(ctx, req) - if err != nil { - status = "failure" - b.logger.Error("failed to update team binding in zanzana", - "err", err, - "namespace", newTB.Namespace, - "name", newTB.Name, - ) - } else { - // Record successful tuple operations - if oldTuple != nil && oldErr == nil { - hooksTuplesCounter.WithLabelValues("teambinding", "update", "delete").Inc() - } - if newTuple != nil && newErr == nil { - hooksTuplesCounter.WithLabelValues("teambinding", "update", "write").Inc() - } - } + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: newTB.Namespace, + Operations: operations, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to update team binding in zanzana", + "err", err, + "namespace", newTB.Namespace, + "name", newTB.Name, + ) } else { - b.logger.Debug("no tuples to update in zanzana", "namespace", newTB.Namespace, "name", newTB.Name) + // Record successful tuple operations + hooksTuplesCounter.WithLabelValues("teambinding", "update", "delete").Inc() + hooksTuplesCounter.WithLabelValues("teambinding", "update", "write").Inc() } }() }, nil @@ -313,22 +233,6 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingDelete(obj runtime. hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() }() - tuple, err := convertTeamBindingToTuple(tb) - if err != nil { - b.logger.Error("failed to convert team binding to tuple for deletion", - "namespace", tb.Namespace, - "name", tb.Name, - "subject", tb.Spec.Subject.Name, - "teamRef", tb.Spec.TeamRef.Name, - "err", err, - ) - status = "failure" - return - } - - // Convert tuple to TupleKeyWithoutCondition for deletion - deleteTuple := toTupleKeysWithoutCondition([]*v1.TupleKey{tuple}) - b.logger.Debug("deleting team binding from zanzana", "namespace", tb.Namespace, "name", tb.Name, @@ -340,10 +244,18 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingDelete(obj runtime. ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) defer cancel() - err = b.zClient.Write(ctx, &v1.WriteRequest{ + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ Namespace: tb.Namespace, - Deletes: &v1.WriteRequestDeletes{ - TupleKeys: deleteTuple, + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: tb.Spec.Subject.Name, + TeamName: tb.Spec.TeamRef.Name, + Permission: string(tb.Spec.Permission), + }, + }, + }, }, }) if err != nil { diff --git a/pkg/registry/apis/iam/team_binding_hooks_test.go b/pkg/registry/apis/iam/team_binding_hooks_test.go index 2f56ba12c87..6b41e8359da 100644 --- a/pkg/registry/apis/iam/team_binding_hooks_test.go +++ b/pkg/registry/apis/iam/team_binding_hooks_test.go @@ -2,16 +2,18 @@ package iam import ( "context" + "slices" "sync" "testing" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/stretchr/testify/require" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/stretchr/testify/require" ) func TestAfterTeamBindingCreate(t *testing.T) { @@ -40,30 +42,29 @@ func TestAfterTeamBindingCreate(t *testing.T) { }, } - testMemberBinding := func(ctx context.Context, req *v1.WriteRequest) error { + testMemberBinding := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) require.Equal(t, "org-1", req.Namespace) - require.Nil(t, req.Deletes) - expectedTuple := &v1.TupleKey{ - User: "user:user-1", - Relation: "member", - Object: "team:team-1", + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "member", + }, + }, } - actualTuple := req.Writes.TupleKeys[0] - require.Equal(t, expectedTuple.User, actualTuple.User) - require.Equal(t, expectedTuple.Relation, actualTuple.Relation) - require.Equal(t, expectedTuple.Object, actualTuple.Object) - require.Nil(t, actualTuple.Condition) + require.True(t, containsTeamBindingOperation(req.Operations, expectedOperation)) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMemberBinding} + b.zClient = &FakeZanzanaClient{mutateCallback: testMemberBinding} b.AfterTeamBindingCreate(&teamBinding, nil) wg.Wait() }) @@ -87,30 +88,29 @@ func TestAfterTeamBindingCreate(t *testing.T) { }, } - testAdminBinding := func(ctx context.Context, req *v1.WriteRequest) error { + testAdminBinding := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) require.Equal(t, "org-2", req.Namespace) - require.Nil(t, req.Deletes) - expectedTuple := &v1.TupleKey{ - User: "user:user-2", - Relation: "admin", - Object: "team:team-2", + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-2", + TeamName: "team-2", + Permission: "admin", + }, + }, } - actualTuple := req.Writes.TupleKeys[0] - require.Equal(t, expectedTuple.User, actualTuple.User) - require.Equal(t, expectedTuple.Relation, actualTuple.Relation) - require.Equal(t, expectedTuple.Object, actualTuple.Object) - require.Nil(t, actualTuple.Condition) + require.True(t, containsTeamBindingOperation(req.Operations, expectedOperation)) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testAdminBinding} + b.zClient = &FakeZanzanaClient{mutateCallback: testAdminBinding} b.AfterTeamBindingCreate(&teamBinding, nil) wg.Wait() }) @@ -141,40 +141,6 @@ func TestAfterTeamBindingCreate(t *testing.T) { // Should not panic or error when zClient is nil builder.AfterTeamBindingCreate(&teamBinding, nil) }) - - t.Run("should handle conversion error gracefully", func(t *testing.T) { - // TeamBinding with empty subject name should fail conversion - teamBinding := iamv0.TeamBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "binding-4", - Namespace: "org-4", - }, - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "", // Empty name should cause error - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "team-4", - }, - Permission: iamv0.TeamBindingTeamPermissionMember, - }, - } - - writeCalled := false - testErrorHandling := func(ctx context.Context, req *v1.WriteRequest) error { - writeCalled = true - // Should not be called due to conversion error - require.Fail(t, "Write should not be called when conversion fails") - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testErrorHandling} - b.AfterTeamBindingCreate(&teamBinding, nil) - // Wait a bit to ensure the goroutine has time to process - // The goroutine will complete but won't call the write callback - time.Sleep(100 * time.Millisecond) - require.False(t, writeCalled, "Write callback should not be called when conversion fails") - }) } func TestBeginTeamBindingUpdate(t *testing.T) { @@ -218,33 +184,37 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - testPermissionUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + testPermissionUpdate := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-1", req.Namespace) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) - // Should delete old member permission - require.NotNil(t, req.Deletes) - require.Len(t, req.Deletes.TupleKeys, 1) - require.Equal( - t, - req.Deletes.TupleKeys[0], - &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, - ) + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "member", + }, + }, + })) - // Should write new admin permission - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) - require.Equal( - t, - req.Writes.TupleKeys[0], - &v1.TupleKey{User: "user:user-1", Relation: "admin", Object: "team:team-1"}, - ) + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "admin", + }, + }, + })) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testPermissionUpdate} + b.zClient = &FakeZanzanaClient{mutateCallback: testPermissionUpdate} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) @@ -288,33 +258,36 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - testUserUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + testUserUpdate := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-2", req.Namespace) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) - // Should delete old user binding - require.NotNil(t, req.Deletes) - require.Len(t, req.Deletes.TupleKeys, 1) - require.Equal( - t, - req.Deletes.TupleKeys[0], - &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, - ) - - // Should write new user binding - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) - require.Equal( - t, - req.Writes.TupleKeys[0], - &v1.TupleKey{User: "user:user-2", Relation: "member", Object: "team:team-1"}, - ) + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "member", + }, + }, + })) + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-2", + TeamName: "team-1", + Permission: "member", + }, + }, + })) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testUserUpdate} + b.zClient = &FakeZanzanaClient{mutateCallback: testUserUpdate} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) @@ -358,33 +331,35 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - testTeamUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + testTeamUpdate := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-3", req.Namespace) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) - // Should delete old team binding - require.NotNil(t, req.Deletes) - require.Len(t, req.Deletes.TupleKeys, 1) - require.Equal( - t, - req.Deletes.TupleKeys[0], - &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "admin", Object: "team:team-1"}, - ) - - // Should write new team binding - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) - require.Equal( - t, - req.Writes.TupleKeys[0], - &v1.TupleKey{User: "user:user-1", Relation: "admin", Object: "team:team-2"}, - ) - + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "admin", + }, + }, + })) + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-2", + Permission: "admin", + }, + }, + })) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testTeamUpdate} + b.zClient = &FakeZanzanaClient{mutateCallback: testTeamUpdate} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) @@ -427,13 +402,13 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - testNoWriteOnFailure := func(ctx context.Context, req *v1.WriteRequest) error { + testNoMutateOnFailure := func(ctx context.Context, req *v1.MutateRequest) error { // Should not be called when success=false - require.Fail(t, "Write should not be called when update fails") + require.Fail(t, "Mutate should not be called when update fails") return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnFailure} + b.zClient = &FakeZanzanaClient{mutateCallback: testNoMutateOnFailure} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) @@ -441,7 +416,7 @@ func TestBeginTeamBindingUpdate(t *testing.T) { // Call finish function with success=false finishFunc(context.Background(), false) - // No wait needed since write should not be called + // No wait needed since mutate should not be called }) t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) { @@ -497,7 +472,7 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, Spec: iamv0.TeamBindingSpec{ Subject: iamv0.TeamBindingspecSubject{ - Name: "", // Empty name - conversion will be skipped + Name: "", // Empty name will cause server-side error on delete }, TeamRef: iamv0.TeamBindingTeamRef{ Name: "team-1", @@ -522,27 +497,42 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - testEmptyOldBinding := func(ctx context.Context, req *v1.WriteRequest) error { + testEmptyOldBinding := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-6", req.Namespace) + require.NotNil(t, req.Operations) - // Should not delete old binding (it was skipped due to empty name) - require.Nil(t, req.Deletes) + // Should have both delete and create operations + // The delete will have empty subject and fail server-side validation + require.Len(t, req.Operations, 2) - // Should write new binding - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) - require.Equal( - t, - req.Writes.TupleKeys[0], - &v1.TupleKey{User: "user:user-2", Relation: "member", Object: "team:team-1"}, - ) + // First operation is delete with empty subject + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "", + TeamName: "team-1", + Permission: "member", + }, + }, + })) + + // Second operation is create with valid data + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateTeamBinding{ + CreateTeamBinding: &v1.CreateTeamBindingOperation{ + SubjectName: "user-2", + TeamName: "team-1", + Permission: "member", + }, + }, + })) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testEmptyOldBinding} + b.zClient = &FakeZanzanaClient{mutateCallback: testEmptyOldBinding} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) @@ -585,22 +575,22 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - writeCalled := false - testNoWriteOnNoChange := func(ctx context.Context, req *v1.WriteRequest) error { - writeCalled = true - require.Fail(t, "Write should not be called when bindings are identical") + mutateCalled := false + testNoMutateOnNoChange := func(ctx context.Context, req *v1.MutateRequest) error { + mutateCalled = true + require.Fail(t, "Mutate should not be called when bindings are identical") return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnNoChange} + b.zClient = &FakeZanzanaClient{mutateCallback: testNoMutateOnNoChange} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) require.Nil(t, finishFunc) // Should return nil when bindings are identical - // Verify write was never called + // Verify mutate was never called time.Sleep(100 * time.Millisecond) - require.False(t, writeCalled, "Write callback should not be called when bindings are identical") + require.False(t, mutateCalled, "Mutate callback should not be called when bindings are identical") }) t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) { @@ -636,22 +626,22 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - writeCalled := false - testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error { - writeCalled = true - require.Fail(t, "Write should not be called when new binding has empty subject name") + mutateCalled := false + testNoMutateOnInvalidBinding := func(ctx context.Context, req *v1.MutateRequest) error { + mutateCalled = true + require.Fail(t, "Mutate should not be called when new binding has empty subject name") return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding} + b.zClient = &FakeZanzanaClient{mutateCallback: testNoMutateOnInvalidBinding} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name - // Verify write was never called + // Verify mutate was never called time.Sleep(100 * time.Millisecond) - require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name") + require.False(t, mutateCalled, "Mutate callback should not be called when new binding has empty subject name") }) t.Run("should return nil finish func when new binding has empty team ref name", func(t *testing.T) { @@ -687,22 +677,22 @@ func TestBeginTeamBindingUpdate(t *testing.T) { }, } - writeCalled := false - testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error { - writeCalled = true - require.Fail(t, "Write should not be called when new binding has empty team ref name") + mutateCalled := false + testNoMutateOnInvalidBinding := func(ctx context.Context, req *v1.MutateRequest) error { + mutateCalled = true + require.Fail(t, "Mutate should not be called when new binding has empty team ref name") return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding} + b.zClient = &FakeZanzanaClient{mutateCallback: testNoMutateOnInvalidBinding} finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) require.NoError(t, err) require.Nil(t, finishFunc) // Should return nil when new binding has empty team ref name - // Verify write was never called + // Verify mutate was never called time.Sleep(100 * time.Millisecond) - require.False(t, writeCalled, "Write callback should not be called when new binding has empty team ref name") + require.False(t, mutateCalled, "Mutate callback should not be called when new binding has empty team ref name") }) } @@ -732,26 +722,28 @@ func TestAfterTeamBindingDelete(t *testing.T) { }, } - testMemberDelete := func(ctx context.Context, req *v1.WriteRequest) error { + testMemberDelete := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-1", req.Namespace) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) - // Should have deletes but no writes - require.NotNil(t, req.Deletes) - require.Len(t, req.Deletes.TupleKeys, 1) - require.Nil(t, req.Writes) - - require.Equal( - t, - req.Deletes.TupleKeys[0], - &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, - ) + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-1", + TeamName: "team-1", + Permission: "member", + }, + }, + } + require.True(t, containsTeamBindingOperation(req.Operations, expectedOperation)) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testMemberDelete} + b.zClient = &FakeZanzanaClient{mutateCallback: testMemberDelete} b.AfterTeamBindingDelete(&teamBinding, nil) wg.Wait() }) @@ -775,26 +767,28 @@ func TestAfterTeamBindingDelete(t *testing.T) { }, } - testAdminDelete := func(ctx context.Context, req *v1.WriteRequest) error { + testAdminDelete := func(ctx context.Context, req *v1.MutateRequest) error { defer wg.Done() require.NotNil(t, req) require.Equal(t, "org-2", req.Namespace) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) - // Should have deletes but no writes - require.NotNil(t, req.Deletes) - require.Len(t, req.Deletes.TupleKeys, 1) - require.Nil(t, req.Writes) - - require.Equal( - t, - req.Deletes.TupleKeys[0], - &v1.TupleKeyWithoutCondition{User: "user:user-2", Relation: "admin", Object: "team:team-2"}, - ) + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-2", + TeamName: "team-2", + Permission: "admin", + }, + }, + } + require.True(t, containsTeamBindingOperation(req.Operations, expectedOperation)) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testAdminDelete} + b.zClient = &FakeZanzanaClient{mutateCallback: testAdminDelete} b.AfterTeamBindingDelete(&teamBinding, nil) wg.Wait() }) @@ -826,8 +820,9 @@ func TestAfterTeamBindingDelete(t *testing.T) { builder.AfterTeamBindingDelete(&teamBinding, nil) }) - t.Run("should handle conversion error gracefully", func(t *testing.T) { - // TeamBinding with empty team ref name should fail conversion + t.Run("should handle empty team name gracefully", func(t *testing.T) { + wg.Add(1) + // TeamBinding with empty team ref name will be sent to server which will return error teamBinding := iamv0.TeamBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "binding-4", @@ -838,129 +833,60 @@ func TestAfterTeamBindingDelete(t *testing.T) { Name: "user-4", }, TeamRef: iamv0.TeamBindingTeamRef{ - Name: "", // Empty name should cause error + Name: "", // Empty name will cause server-side error }, Permission: iamv0.TeamBindingTeamPermissionMember, }, } - writeCalled := false - testErrorHandling := func(ctx context.Context, req *v1.WriteRequest) error { - writeCalled = true - // Should not be called due to conversion error - require.Fail(t, "Write should not be called when conversion fails") + testErrorHandling := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) + require.Equal(t, "org-4", req.Namespace) + + // Operation will have empty team name, which would fail server-side validation + require.True(t, containsTeamBindingOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteTeamBinding{ + DeleteTeamBinding: &v1.DeleteTeamBindingOperation{ + SubjectName: "user-4", + TeamName: "", + Permission: "member", + }, + }, + })) return nil } - b.zClient = &FakeZanzanaClient{writeCallback: testErrorHandling} + b.zClient = &FakeZanzanaClient{mutateCallback: testErrorHandling} b.AfterTeamBindingDelete(&teamBinding, nil) - // Wait a bit to ensure the goroutine has time to process - // The goroutine will complete but won't call the write callback - time.Sleep(100 * time.Millisecond) - require.False(t, writeCalled, "Write callback should not be called when conversion fails") + wg.Wait() }) } -func TestConvertTeamBindingToTuple(t *testing.T) { - t.Run("should convert member permission correctly", func(t *testing.T) { - tb := &iamv0.TeamBinding{ - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "user-1", - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "team-1", - }, - Permission: iamv0.TeamBindingTeamPermissionMember, - }, +func containsTeamBindingOperation(operations []*v1.MutateOperation, operation *v1.MutateOperation) bool { + return slices.ContainsFunc(operations, func(o *v1.MutateOperation) bool { + switch operation.Operation.(type) { + case *v1.MutateOperation_DeleteTeamBinding: + deleteOperation := operation.Operation.(*v1.MutateOperation_DeleteTeamBinding) + deleteO, ok := o.Operation.(*v1.MutateOperation_DeleteTeamBinding) + if !ok { + return false + } + return deleteO.DeleteTeamBinding.SubjectName == deleteOperation.DeleteTeamBinding.SubjectName && + deleteO.DeleteTeamBinding.TeamName == deleteOperation.DeleteTeamBinding.TeamName && + deleteO.DeleteTeamBinding.Permission == deleteOperation.DeleteTeamBinding.Permission + case *v1.MutateOperation_CreateTeamBinding: + createOperation := operation.Operation.(*v1.MutateOperation_CreateTeamBinding) + createO, ok := o.Operation.(*v1.MutateOperation_CreateTeamBinding) + if !ok { + return false + } + return createO.CreateTeamBinding.SubjectName == createOperation.CreateTeamBinding.SubjectName && + createO.CreateTeamBinding.TeamName == createOperation.CreateTeamBinding.TeamName && + createO.CreateTeamBinding.Permission == createOperation.CreateTeamBinding.Permission } - - tuple, err := convertTeamBindingToTuple(tb) - require.NoError(t, err) - require.NotNil(t, tuple) - require.Equal(t, "user:user-1", tuple.User) - require.Equal(t, "member", tuple.Relation) - require.Equal(t, "team:team-1", tuple.Object) - require.Nil(t, tuple.Condition) - }) - - t.Run("should convert admin permission correctly", func(t *testing.T) { - tb := &iamv0.TeamBinding{ - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "user-2", - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "team-2", - }, - Permission: iamv0.TeamBindingTeamPermissionAdmin, - }, - } - - tuple, err := convertTeamBindingToTuple(tb) - require.NoError(t, err) - require.NotNil(t, tuple) - require.Equal(t, "user:user-2", tuple.User) - require.Equal(t, "admin", tuple.Relation) - require.Equal(t, "team:team-2", tuple.Object) - require.Nil(t, tuple.Condition) - }) - - t.Run("should return error for empty subject name", func(t *testing.T) { - tb := &iamv0.TeamBinding{ - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "", - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "team-1", - }, - Permission: iamv0.TeamBindingTeamPermissionMember, - }, - } - - tuple, err := convertTeamBindingToTuple(tb) - require.Error(t, err) - require.Nil(t, tuple) - require.Equal(t, errEmptyName, err) - }) - - t.Run("should return error for empty team ref name", func(t *testing.T) { - tb := &iamv0.TeamBinding{ - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "user-1", - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "", - }, - Permission: iamv0.TeamBindingTeamPermissionMember, - }, - } - - tuple, err := convertTeamBindingToTuple(tb) - require.Error(t, err) - require.Nil(t, tuple) - require.Equal(t, errEmptyName, err) - }) - - t.Run("should default to member for unknown permission", func(t *testing.T) { - tb := &iamv0.TeamBinding{ - Spec: iamv0.TeamBindingSpec{ - Subject: iamv0.TeamBindingspecSubject{ - Name: "user-1", - }, - TeamRef: iamv0.TeamBindingTeamRef{ - Name: "team-1", - }, - Permission: "unknown", // Invalid permission - }, - } - - tuple, err := convertTeamBindingToTuple(tb) - require.NoError(t, err) - require.NotNil(t, tuple) - // Should default to member relation - require.Equal(t, "member", tuple.Relation) + return false }) } From eafc8ab1cd60dcb588dea1e264cfda06a1d2f77e Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Fri, 28 Nov 2025 11:51:56 +0100 Subject: [PATCH 164/423] Alerting: Foundations of historian app. (#114463) We have two historians in alerting - alert state and notification. The intention of this app is to provide query capabilities for both. In this initial commit, the existing /history API is simply cloned to the new app. It is identical except that it will send Kubernetes-style error responses instead of Grafana-style. This approach was taken to implement the new app more iteratively - ideally we would define a new API, but this requires quite a significant overhaul of the backend code. --- apps/alerting/historian/Makefile | 9 + apps/alerting/historian/go.mod | 86 +++++ apps/alerting/historian/go.sum | 238 +++++++++++++ .../historian/kinds/cue.mod/module.cue | 2 + apps/alerting/historian/kinds/manifest.cue | 40 +++ .../alertinghistorian/v0alpha1/constants.go | 18 + .../v0alpha1/dummy_client_gen.go | 99 ++++++ .../v0alpha1/dummy_codec_gen.go | 28 ++ .../v0alpha1/dummy_metadata_gen.go | 31 ++ .../v0alpha1/dummy_object_gen.go | 319 ++++++++++++++++++ .../v0alpha1/dummy_schema_gen.go | 34 ++ .../v0alpha1/dummy_spec_gen.go | 14 + .../v0alpha1/dummy_status_gen.go | 44 +++ ...getalertstatehistory_response_types_gen.go | 15 + .../getalertstatequery_response_types_gen.go | 3 + .../pkg/apis/alertinghistorian_manifest.go | 175 ++++++++++ apps/alerting/historian/pkg/app/app.go | 45 +++ .../historian/pkg/app/config/config.go | 9 + .../dummy/v0alpha1/dummy_object_gen.ts | 49 +++ .../dummy/v0alpha1/types.metadata.gen.ts | 30 ++ .../dummy/v0alpha1/types.spec.gen.ts | 11 + .../dummy/v0alpha1/types.status.gen.ts | 30 ++ go.mod | 2 + .../apps/alerting/historian/handlers.go | 60 ++++ .../apps/alerting/historian/handlers_test.go | 309 +++++++++++++++++ .../apps/alerting/historian/register.go | 58 ++++ pkg/registry/apps/apps.go | 6 + pkg/registry/apps/apps_test.go | 5 +- pkg/registry/apps/wireset.go | 2 + pkg/server/wire_gen.go | 13 +- pkg/services/featuremgmt/toggles_gen.json | 2 +- 31 files changed, 1782 insertions(+), 4 deletions(-) create mode 100644 apps/alerting/historian/Makefile create mode 100644 apps/alerting/historian/go.mod create mode 100644 apps/alerting/historian/go.sum create mode 100644 apps/alerting/historian/kinds/cue.mod/module.cue create mode 100644 apps/alerting/historian/kinds/manifest.cue create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/constants.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_client_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_codec_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_metadata_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_object_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_schema_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_spec_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_status_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatehistory_response_types_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatequery_response_types_gen.go create mode 100644 apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go create mode 100644 apps/alerting/historian/pkg/app/app.go create mode 100644 apps/alerting/historian/pkg/app/config/config.go create mode 100644 apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/dummy_object_gen.ts create mode 100644 apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.metadata.gen.ts create mode 100644 apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.spec.gen.ts create mode 100644 apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.status.gen.ts create mode 100644 pkg/registry/apps/alerting/historian/handlers.go create mode 100644 pkg/registry/apps/alerting/historian/handlers_test.go create mode 100644 pkg/registry/apps/alerting/historian/register.go diff --git a/apps/alerting/historian/Makefile b/apps/alerting/historian/Makefile new file mode 100644 index 00000000000..378b08281e6 --- /dev/null +++ b/apps/alerting/historian/Makefile @@ -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 diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod new file mode 100644 index 00000000000..eec687eb79f --- /dev/null +++ b/apps/alerting/historian/go.mod @@ -0,0 +1,86 @@ +module github.com/grafana/grafana/apps/alerting/historian + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.2 + k8s.io/apimachinery v0.34.2 + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b +) + +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/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.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // 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/uuid v1.6.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // 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.7.7 // 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/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.66.1 // 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.1.0 // 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.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.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.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.8 // 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.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum new file mode 100644 index 00000000000..a0f2396b250 --- /dev/null +++ b/apps/alerting/historian/go.sum @@ -0,0 +1,238 @@ +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 v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +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.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +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-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.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/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/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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +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/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.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +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.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +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/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +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/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/historian/kinds/cue.mod/module.cue b/apps/alerting/historian/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..2f45dd38aec --- /dev/null +++ b/apps/alerting/historian/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/alerting/historian/kinds" +language: version: "v0.8.2" diff --git a/apps/alerting/historian/kinds/manifest.cue b/apps/alerting/historian/kinds/manifest.cue new file mode 100644 index 00000000000..6f2bfc21cb9 --- /dev/null +++ b/apps/alerting/historian/kinds/manifest.cue @@ -0,0 +1,40 @@ +package kinds + +manifest: { + appName: "alerting-historian" + groupOverride: "historian.alerting.grafana.app" + versions: { + "v0alpha1": v0alpha1 + } +} + +v0alpha1: { + kinds: [dummyv0alpha1] + + routes: { + namespaced: { + // This endpoint is an exact copy of the existing /history endpoint, + // with the exception that error responses will be Kubernetes-style, + // not Grafana-style. It will be replaced in the future with a better + // more schema-friendly API. + "/alertstate/history": { + "GET": { + response: { + body: [string]: _ + } + responseMetadata: typeMeta: false + } + } + } + } +} + +dummyv0alpha1: { + kind: "Dummy" + schema: { + // Spec is the schema of our resource. The spec should include all the user-editable information for the kind. + spec: { + dummyField: int + } + } +} \ No newline at end of file diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/constants.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/constants.go new file mode 100644 index 00000000000..85867e8de0e --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/constants.go @@ -0,0 +1,18 @@ +package v0alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "historian.alerting.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_client_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_client_gen.go new file mode 100644 index 00000000000..fba40cf2385 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type DummyClient struct { + client *resource.TypedClient[*Dummy, *DummyList] +} + +func NewDummyClient(client resource.Client) *DummyClient { + return &DummyClient{ + client: resource.NewTypedClient[*Dummy, *DummyList](client, DummyKind()), + } +} + +func NewDummyClientFromGenerator(generator resource.ClientGenerator) (*DummyClient, error) { + c, err := generator.ClientFor(DummyKind()) + if err != nil { + return nil, err + } + return NewDummyClient(c), nil +} + +func (c *DummyClient) Get(ctx context.Context, identifier resource.Identifier) (*Dummy, error) { + return c.client.Get(ctx, identifier) +} + +func (c *DummyClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*DummyList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *DummyClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*DummyList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *DummyClient) Create(ctx context.Context, obj *Dummy, opts resource.CreateOptions) (*Dummy, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = DummyKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *DummyClient) Update(ctx context.Context, obj *Dummy, opts resource.UpdateOptions) (*Dummy, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *DummyClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Dummy, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *DummyClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus DummyStatus, opts resource.UpdateOptions) (*Dummy, error) { + return c.client.Update(ctx, &Dummy{ + TypeMeta: metav1.TypeMeta{ + Kind: DummyKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *DummyClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_codec_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_codec_gen.go new file mode 100644 index 00000000000..6512ec5d36d --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// DummyJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type DummyJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*DummyJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*DummyJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &DummyJSONCodec{} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_metadata_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_metadata_gen.go new file mode 100644 index 00000000000..f56576b2b72 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// 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. +type DummyMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewDummyMetadata creates a new DummyMetadata object. +func NewDummyMetadata() *DummyMetadata { + return &DummyMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_object_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_object_gen.go new file mode 100644 index 00000000000..827abbaa7a2 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Dummy struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Dummy + Spec DummySpec `json:"spec" yaml:"spec"` + + Status DummyStatus `json:"status" yaml:"status"` +} + +func (o *Dummy) GetSpec() any { + return o.Spec +} + +func (o *Dummy) SetSpec(spec any) error { + cast, ok := spec.(DummySpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Dummy) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Dummy) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Dummy) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(DummyStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type DummyStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Dummy) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Dummy) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Dummy) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Dummy) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Dummy) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Dummy) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Dummy) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Dummy) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Dummy) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Dummy) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Dummy) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Dummy) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Dummy) DeepCopy() *Dummy { + cpy := &Dummy{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Dummy) DeepCopyInto(dst *Dummy) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Dummy{} + +// +k8s:openapi-gen=true +type DummyList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Dummy `json:"items" yaml:"items"` +} + +func (o *DummyList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *DummyList) Copy() resource.ListObject { + cpy := &DummyList{ + TypeMeta: o.TypeMeta, + Items: make([]Dummy, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Dummy); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *DummyList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *DummyList) SetItems(items []resource.Object) { + o.Items = make([]Dummy, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Dummy) + } +} + +func (o *DummyList) DeepCopy() *DummyList { + cpy := &DummyList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *DummyList) DeepCopyInto(dst *DummyList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &DummyList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *DummySpec) DeepCopy() *DummySpec { + cpy := &DummySpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *DummySpec) DeepCopyInto(dst *DummySpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of DummyStatus +func (s *DummyStatus) DeepCopy() *DummyStatus { + cpy := &DummyStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies DummyStatus into another DummyStatus object +func (s *DummyStatus) DeepCopyInto(dst *DummyStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_schema_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_schema_gen.go new file mode 100644 index 00000000000..a7d3de4ed1c --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaDummy = resource.NewSimpleSchema("historian.alerting.grafana.app", "v0alpha1", &Dummy{}, &DummyList{}, resource.WithKind("Dummy"), + resource.WithPlural("dummys"), resource.WithScope(resource.NamespacedScope)) + kindDummy = resource.Kind{ + Schema: schemaDummy, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &DummyJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func DummyKind() resource.Kind { + return kindDummy +} + +// Schema returns a resource.SimpleSchema representation of Dummy +func DummySchema() *resource.SimpleSchema { + return schemaDummy +} + +// Interface compliance checks +var _ resource.Schema = kindDummy diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_spec_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_spec_gen.go new file mode 100644 index 00000000000..16d4eab9409 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_spec_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// +k8s:openapi-gen=true +type DummySpec struct { + DummyField int64 `json:"dummyField"` +} + +// NewDummySpec creates a new DummySpec object. +func NewDummySpec() *DummySpec { + return &DummySpec{} +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_status_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_status_gen.go new file mode 100644 index 00000000000..36d064053f5 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/dummy_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type DummystatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State DummyStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewDummystatusOperatorState creates a new DummystatusOperatorState object. +func NewDummystatusOperatorState() *DummystatusOperatorState { + return &DummystatusOperatorState{} +} + +// +k8s:openapi-gen=true +type DummyStatus struct { + // 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 map[string]DummystatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewDummyStatus creates a new DummyStatus object. +func NewDummyStatus() *DummyStatus { + return &DummyStatus{} +} + +// +k8s:openapi-gen=true +type DummyStatusOperatorStateState string + +const ( + DummyStatusOperatorStateStateSuccess DummyStatusOperatorStateState = "success" + DummyStatusOperatorStateStateInProgress DummyStatusOperatorStateState = "in_progress" + DummyStatusOperatorStateStateFailed DummyStatusOperatorStateState = "failed" +) diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatehistory_response_types_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatehistory_response_types_gen.go new file mode 100644 index 00000000000..b9f54ed4718 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatehistory_response_types_gen.go @@ -0,0 +1,15 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type GetAlertstatehistory struct { + Body map[string]interface{} `json:"body"` +} + +// NewGetAlertstatehistory creates a new GetAlertstatehistory object. +func NewGetAlertstatehistory() *GetAlertstatehistory { + return &GetAlertstatehistory{ + Body: map[string]interface{}{}, + } +} diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatequery_response_types_gen.go b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatequery_response_types_gen.go new file mode 100644 index 00000000000..90130b85cf3 --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1/getalertstatequery_response_types_gen.go @@ -0,0 +1,3 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 diff --git a/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go b/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go new file mode 100644 index 00000000000..4e6401e5b1d --- /dev/null +++ b/apps/alerting/historian/pkg/apis/alertinghistorian_manifest.go @@ -0,0 +1,175 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "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/alerting/historian/pkg/apis/alertinghistorian/v0alpha1" +) + +var ( + rawSchemaDummyv0alpha1 = []byte(`{"Dummy":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"spec":{"additionalProperties":false,"description":"Spec is the schema of our resource. The spec should include all the user-editable information for the kind.","properties":{"dummyField":{"type":"integer"}},"required":["dummyField"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaDummyv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDummyv0alpha1, &versionSchemaDummyv0alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "alerting-historian", + Group: "historian.alerting.grafana.app", + PreferredVersion: "v0alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Dummy", + Plural: "Dummys", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaDummyv0alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{ + "/alertstate/history": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getAlertstatehistory", + + 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{ + "body": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{}, + }, + }, + }, + }, + }, + }, + }, + }, + Required: []string{ + "body", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + 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("alerting-historian") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Dummy/v0alpha1": v0alpha1.DummyKind(), +} + +// 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||/alertstate/history|GET": v0alpha1.GetAlertstatehistory{}, +} + +// 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 "/" 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) +} diff --git a/apps/alerting/historian/pkg/app/app.go b/apps/alerting/historian/pkg/app/app.go new file mode 100644 index 00000000000..8d5feb39ec7 --- /dev/null +++ b/apps/alerting/historian/pkg/app/app.go @@ -0,0 +1,45 @@ +package app + +import ( + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" + + "github.com/grafana/grafana/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1" + "github.com/grafana/grafana/apps/alerting/historian/pkg/app/config" +) + +func New(cfg app.Config) (app.App, error) { + runtimeConfig := cfg.SpecificConfig.(config.RuntimeConfig) + + simpleConfig := simple.AppConfig{ + Name: "alerting.historian", + KubeConfig: cfg.KubeConfig, + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v0alpha1": { + { + Namespaced: true, + Path: "/alertstate/history", + Method: "GET", + }: runtimeConfig.GetAlertStateHistoryHandler, + }, + }, + // TODO: Remove when SDK is fixed. + ManagedKinds: []simple.AppManagedKind{ + { + Kind: v0alpha1.DummyKind(), + }, + }, + } + + 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 +} diff --git a/apps/alerting/historian/pkg/app/config/config.go b/apps/alerting/historian/pkg/app/config/config.go new file mode 100644 index 00000000000..5a40503dea8 --- /dev/null +++ b/apps/alerting/historian/pkg/app/config/config.go @@ -0,0 +1,9 @@ +package config + +import ( + "github.com/grafana/grafana-app-sdk/simple" +) + +type RuntimeConfig struct { + GetAlertStateHistoryHandler simple.AppCustomRouteHandler +} diff --git a/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/dummy_object_gen.ts b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/dummy_object_gen.ts new file mode 100644 index 00000000000..8189d09b6fb --- /dev/null +++ b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/dummy_object_gen.ts @@ -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; + annotations?: Record; + 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 Dummy { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.metadata.gen.ts b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.metadata.gen.ts @@ -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; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.spec.gen.ts b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..00cf31e8dda --- /dev/null +++ b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.spec.gen.ts @@ -0,0 +1,11 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +export interface Spec { + dummyField: number; +} + +export const defaultSpec = (): Spec => ({ + dummyField: 0, +}); + diff --git a/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.status.gen.ts b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/alerting/historian/plugin/src/generated/dummy/v0alpha1/types.status.gen.ts @@ -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; +} + +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; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/go.mod b/go.mod index e1ed6dddd56..3d277125a1f 100644 --- a/go.mod +++ b/go.mod @@ -234,6 +234,7 @@ require ( require ( github.com/grafana/grafana/apps/advisor v0.0.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/alertenrichment v0.0.0 // @grafana/alerting-backend + github.com/grafana/grafana/apps/alerting/historian v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/alerting/rules v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/annotation v0.0.0 // @grafana/grafana-backend-services-squad @@ -267,6 +268,7 @@ require ( replace ( github.com/grafana/grafana/apps/advisor => ./apps/advisor github.com/grafana/grafana/apps/alerting/alertenrichment => ./apps/alerting/alertenrichment + github.com/grafana/grafana/apps/alerting/historian => ./apps/alerting/historian github.com/grafana/grafana/apps/alerting/notifications => ./apps/alerting/notifications github.com/grafana/grafana/apps/alerting/rules => ./apps/alerting/rules github.com/grafana/grafana/apps/annotation => ./apps/annotation diff --git a/pkg/registry/apps/alerting/historian/handlers.go b/pkg/registry/apps/alerting/historian/handlers.go new file mode 100644 index 00000000000..f24323684f9 --- /dev/null +++ b/pkg/registry/apps/alerting/historian/handlers.go @@ -0,0 +1,60 @@ +package historian + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-plugin-sdk-go/data" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/ngalert/api" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type Historian interface { + Query(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) +} + +type handlers struct { + historian Historian +} + +func (h handlers) GetAlertStateHistoryHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + user, err := identity.GetRequester(ctx) + if err != nil { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusUnauthorized, + Message: "authentication required", + }} + } + + query, err := api.ParseHistoryQuery(user.GetOrgID(), user, request.URL.Query()) + if err != nil { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusBadRequest, + Message: err.Error(), + }} + } + + frame, err := h.historian.Query(ctx, query) + if err != nil { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusInternalServerError, + Message: err.Error(), + }} + } + + writer.Header().Add("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + return json.NewEncoder(writer).Encode(frame) +} diff --git a/pkg/registry/apps/alerting/historian/handlers_test.go b/pkg/registry/apps/alerting/historian/handlers_test.go new file mode 100644 index 00000000000..30a7f2ba764 --- /dev/null +++ b/pkg/registry/apps/alerting/historian/handlers_test.go @@ -0,0 +1,309 @@ +package historian + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type mockHistorian struct { + queryFunc func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) +} + +func (m *mockHistorian) Query(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + if m.queryFunc != nil { + return m.queryFunc(ctx, query) + } + return nil, errors.New("not implemented") +} + +type mockResponseWriter struct { + *httptest.ResponseRecorder + headers http.Header +} + +func newMockResponseWriter() *mockResponseWriter { + return &mockResponseWriter{ + ResponseRecorder: httptest.NewRecorder(), + headers: make(http.Header), + } +} + +func (m *mockResponseWriter) Header() http.Header { + return m.headers +} + +func TestGetAlertStateHistoryHandler(t *testing.T) { + t.Run("returns data frame when query succeeds", func(t *testing.T) { + now := time.Now() + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{now, now.Add(time.Second)}), + data.NewField("Line", nil, []string{"alert fired", "alert resolved"}), + ) + + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + assert.Equal(t, int64(123), query.OrgID) + assert.NotNil(t, query.SignedInUser) + return testFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ + OrgID: 123, + }) + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: ""}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, writer.Code) + assert.Equal(t, "application/json", writer.headers.Get("Content-Type")) + + var result *data.Frame + err = json.Unmarshal(writer.Body.Bytes(), &result) + require.NoError(t, err) + assert.Equal(t, "test", result.Name) + assert.Equal(t, 2, result.Rows()) + }) + + t.Run("passes query parameters to historian", func(t *testing.T) { + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{time.Now()}), + data.NewField("Line", nil, []string{"test"}), + ) + + var capturedQuery models.HistoryQuery + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + capturedQuery = query + return testFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 99}) + + params := url.Values{} + params.Set("ruleUID", "rule-123") + params.Set("dashboardUID", "dash-456") + params.Set("panelID", "7") + params.Set("from", "1000") + params.Set("to", "2000") + params.Set("limit", "50") + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: params.Encode()}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + assert.Equal(t, "rule-123", capturedQuery.RuleUID) + assert.Equal(t, "dash-456", capturedQuery.DashboardUID) + assert.Equal(t, int64(7), capturedQuery.PanelID) + assert.Equal(t, time.Unix(1000, 0), capturedQuery.From) + assert.Equal(t, time.Unix(2000, 0), capturedQuery.To) + assert.Equal(t, 50, capturedQuery.Limit) + }) + + t.Run("handles label matchers in query", func(t *testing.T) { + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{time.Now()}), + data.NewField("Line", nil, []string{"test"}), + ) + + var capturedQuery models.HistoryQuery + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + capturedQuery = query + return testFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 1}) + + params := url.Values{} + params.Add("labels", `env=prod`) + params.Add("labels", `region=us-west`) + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: params.Encode()}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + if len(capturedQuery.Labels) > 0 { + assert.NotEmpty(t, capturedQuery.Labels) + } + }) + + t.Run("returns unauthorized when no user in context", func(t *testing.T) { + h := handlers{historian: &mockHistorian{}} + ctx := context.Background() + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: ""}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.Error(t, err) + assert.Contains(t, err.Error(), "authentication required") + }) + + t.Run("returns internal error when historian query fails", func(t *testing.T) { + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + return nil, errors.New("database connection failed") + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 1}) + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: ""}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.Error(t, err) + assert.Contains(t, err.Error(), "database connection failed") + }) + + t.Run("returns empty frame when no results", func(t *testing.T) { + emptyFrame := data.NewFrame("empty") + + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + return emptyFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 1}) + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: ""}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, writer.Code) + + var result *data.Frame + err = json.Unmarshal(writer.Body.Bytes(), &result) + require.NoError(t, err) + assert.Equal(t, 0, result.Rows()) + }) + + t.Run("encodes complex data frame with multiple fields", func(t *testing.T) { + now := time.Now() + complexFrame := data.NewFrame("complex", + data.NewField("Time", nil, []time.Time{now}), + data.NewField("Line", nil, []string{"alert fired"}), + data.NewField("Value", nil, []float64{42.5}), + data.NewField("Labels", nil, []string{`{"env":"prod"}`}), + ) + + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + return complexFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 1}) + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: ""}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, writer.Code) + + var result *data.Frame + err = json.Unmarshal(writer.Body.Bytes(), &result) + require.NoError(t, err) + assert.Equal(t, 4, len(result.Fields)) + assert.Equal(t, 1, result.Rows()) + }) +} + +func TestParseHistoryQueryIntegration(t *testing.T) { + t.Run("parses all supported query parameters", func(t *testing.T) { + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{time.Now()}), + data.NewField("Line", nil, []string{"test"}), + ) + + var capturedQuery models.HistoryQuery + mock := &mockHistorian{ + queryFunc: func(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + capturedQuery = query + return testFrame, nil + }, + } + + h := handlers{historian: mock} + ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{OrgID: 5}) + + params := url.Values{} + params.Set("ruleUID", "test-rule") + params.Set("dashboardUID", "test-dash") + params.Set("panelID", "3") + params.Set("from", "1609459200") + params.Set("to", "1609545600") + params.Set("limit", "100") + params.Set("current", "alerting") + params.Set("previous", "normal") + + writer := newMockResponseWriter() + req := &app.CustomRouteRequest{ + URL: &url.URL{RawQuery: params.Encode()}, + } + + err := h.GetAlertStateHistoryHandler(ctx, writer, req) + + require.NoError(t, err) + assert.Equal(t, int64(5), capturedQuery.OrgID) + assert.Equal(t, "test-rule", capturedQuery.RuleUID) + assert.Equal(t, "test-dash", capturedQuery.DashboardUID) + assert.Equal(t, int64(3), capturedQuery.PanelID) + assert.Equal(t, time.Unix(1609459200, 0), capturedQuery.From) + assert.Equal(t, time.Unix(1609545600, 0), capturedQuery.To) + assert.Equal(t, 100, capturedQuery.Limit) + assert.Equal(t, "alerting", capturedQuery.Current) + assert.Equal(t, "normal", capturedQuery.Previous) + }) +} diff --git a/pkg/registry/apps/alerting/historian/register.go b/pkg/registry/apps/alerting/historian/register.go new file mode 100644 index 00000000000..fb0e90e7062 --- /dev/null +++ b/pkg/registry/apps/alerting/historian/register.go @@ -0,0 +1,58 @@ +package historian + +import ( + "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/simple" + restclient "k8s.io/client-go/rest" + + "github.com/grafana/grafana/apps/alerting/historian/pkg/apis" + historianApp "github.com/grafana/grafana/apps/alerting/historian/pkg/app" + historianAppConfig "github.com/grafana/grafana/apps/alerting/historian/pkg/app/config" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*AlertingHistorianAppInstaller)(nil) +) + +type AlertingHistorianAppInstaller struct { + appsdkapiserver.AppInstaller +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + ng *ngalert.AlertNG, +) (*AlertingHistorianAppInstaller, error) { + if ng.IsDisabled() { + log.New("app-registry").Info("Skipping Kubernetes Alerting Historian apiserver (historian.alerting.grafana.app): Unified Alerting is disabled") + return nil, nil + } + + installer := &AlertingHistorianAppInstaller{} + + handlers := &handlers{ + historian: ng.Api.Historian, + } + + appSpecificConfig := historianAppConfig.RuntimeConfig{ + GetAlertStateHistoryHandler: handlers.GetAlertStateHistoryHandler, + } + + provider := simple.NewAppProvider(apis.LocalManifest(), appSpecificConfig, historianApp.New) + + appConfig := app.Config{ + KubeConfig: restclient.Config{}, + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: appSpecificConfig, + } + + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{}) + if err != nil { + return nil, err + } + installer.AppInstaller = i + return installer, nil +} diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index 983605dedbc..9ea31109495 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry/apps/advisor" + "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/annotation" @@ -42,6 +43,7 @@ func ProvideAppInstallers( annotationAppInstaller *annotation.AnnotationAppInstaller, exampleAppInstaller *example.ExampleAppInstaller, advisorAppInstaller *advisor.AdvisorAppInstaller, + alertingHistorianAppInstaller *historian.AlertingHistorianAppInstaller, ) []appsdkapiserver.AppInstaller { installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, @@ -75,6 +77,10 @@ func ProvideAppInstallers( if features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) { installers = append(installers, advisorAppInstaller) } + //nolint:staticcheck // not yet migrated to OpenFeature + if features.IsEnabledGlobally(featuremgmt.FlagKubernetesAlertingHistorian) && alertingHistorianAppInstaller != nil { + installers = append(installers, alertingHistorianAppInstaller) + } return installers } diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index b7f6c2565e6..6a6f0c9a2aa 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/registry/apps/advisor" + "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/annotation" @@ -25,6 +26,8 @@ func TestProvideAppInstallers_Table(t *testing.T) { annotationAppInstaller := &annotation.AnnotationAppInstaller{} exampleAppInstaller := &example.ExampleAppInstaller{} advisorAppInstaller := &advisor.AdvisorAppInstaller{} + historianAppInstaller := &historian.AlertingHistorianAppInstaller{} + tests := []struct { name string flags []any @@ -40,7 +43,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) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index fb403ae4df7..cc6953e5dce 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -3,6 +3,7 @@ package appregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/annotation" @@ -25,6 +26,7 @@ var WireSet = wire.NewSet( correlations.RegisterAppInstaller, rules.RegisterAppInstaller, notifications.RegisterAppInstaller, + historian.RegisterAppInstaller, logsdrilldown.RegisterAppInstaller, annotation.RegisterAppInstaller, example.RegisterAppInstaller, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 75341e77f78..3304a905fc0 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -80,6 +80,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/registry/apps" advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" + "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/annotation" @@ -825,7 +826,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, err := historian.RegisterAppInstaller(cfg, alertNG) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) 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 { @@ -1475,7 +1480,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, err := historian.RegisterAppInstaller(cfg, alertNG) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) 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 { diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 59ad50e79e2..ea80af0831e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3633,4 +3633,4 @@ } } ] -} \ No newline at end of file +} From 49175bb2cb486977b78462cb80e04edbd9e7461c Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 28 Nov 2025 11:27:48 +0000 Subject: [PATCH 165/423] Chore: Run some React 19 codemods (#114575) running react 19 codemods --- .../grafana-data/src/utils/OptionsUIBuilders.ts | 1 + .../src/configuration/AlertingSettingsOverhaul.tsx | 1 + .../src/configuration/shared/utils.tsx | 1 + .../src/services/pluginExtensions/utils.test.tsx | 2 +- .../src/components/BarGauge/BarGauge.tsx | 2 +- .../src/components/BigValue/BigValueLayout.tsx | 2 +- .../src/components/Button/Button.test.tsx | 1 + .../CallToActionCard/CallToActionCard.story.tsx | 1 + .../CallToActionCard/CallToActionCard.tsx | 1 + .../ClipboardButton/ClipboardButton.test.tsx | 1 + .../src/components/Collapse/Collapse.test.tsx | 1 + .../components/ColorPicker/NamedColorsPalette.tsx | 1 + .../src/components/ConfirmModal/ConfirmModal.tsx | 1 + .../src/components/ContextMenu/WithContextMenu.tsx | 2 +- .../components/CustomScrollbar/CustomScrollbar.tsx | 2 +- .../components/DataLinks/DataLinksContextMenu.tsx | 2 +- .../DataSourceSettings/AlertingSettings.tsx | 2 ++ .../SecureSocksProxySettings.tsx | 2 ++ .../components/DateTimePickers/TimeRangePicker.tsx | 2 +- .../TimeZonePicker/TimeZoneOption.tsx | 2 +- .../DateTimePickers/utils/useTimeSync.tsx | 2 +- .../EmptySearchResult/EmptySearchResult.tsx | 1 + .../src/components/FilterPill/FilterPill.test.tsx | 1 + .../grafana-ui/src/components/Forms/FieldArray.tsx | 2 +- .../src/components/Forms/Legacy/Input/Input.tsx | 2 +- packages/grafana-ui/src/components/Gauge/Gauge.tsx | 4 +++- .../grafana-ui/src/components/InfoBox/InfoBox.tsx | 1 + .../src/components/List/AbstractList.tsx | 2 +- packages/grafana-ui/src/components/Modal/Modal.tsx | 2 +- .../src/components/Pagination/Pagination.tsx | 2 +- .../components/PanelChrome/PanelDescription.tsx | 1 + .../src/components/QueryField/QueryField.tsx | 4 +++- .../RenderUserContentAsHTML.tsx | 2 +- .../src/components/Segment/SegmentInput.story.tsx | 2 +- .../src/components/Select/InputControl.tsx | 2 +- .../src/components/Select/MultiValue.tsx | 1 + .../src/components/Select/SelectMenu.tsx | 2 +- packages/grafana-ui/src/components/Select/types.ts | 1 + .../TabbedContainer/TabbedContainer.test.tsx | 1 + .../src/components/Table/Cells/GeoCell.tsx | 1 + .../src/components/Table/Cells/JSONViewCell.tsx | 2 +- .../src/components/Table/TableNG/TableNG.tsx | 2 +- .../grafana-ui/src/components/Tabs/Tabs.test.tsx | 1 + .../grafana-ui/src/components/Tags/Tag.test.tsx | 1 + .../src/components/Tags/TagList.test.tsx | 1 + .../src/components/Toggletip/Toggletip.tsx | 2 +- .../grafana-ui/src/components/Toggletip/types.ts | 1 + .../src/components/Tooltip/PopoverController.tsx | 2 +- .../grafana-ui/src/components/Tooltip/Tooltip.tsx | 2 +- .../grafana-ui/src/components/Tooltip/types.ts | 1 + .../components/ValuePicker/ValuePicker.test.tsx | 1 + .../src/components/VizLegend/VizLegendTable.tsx | 1 + .../grafana-ui/src/components/VizLegend/types.ts | 1 + .../src/components/VizRepeater/VizRepeater.tsx | 2 +- packages/grafana-ui/src/graveyard/Graph/Graph.tsx | 6 ++++-- .../src/graveyard/Graph/GraphSeriesToggler.tsx | 2 +- public/app/core/components/Animations/FadeIn.tsx | 2 +- .../AppChrome/AppChromeExtensionPoint.tsx | 2 ++ .../AppChrome/MegaMenu/FeatureHighlight.tsx | 1 + public/app/core/components/Branding/Branding.tsx | 2 +- .../core/components/FolderFilter/FolderFilter.tsx | 2 +- .../core/components/Layers/LayerDragDropList.tsx | 1 + public/app/core/components/Login/LoginCtrl.tsx | 2 +- .../components/PanelTypeFilter/PanelTypeFilter.tsx | 2 +- .../app/core/components/RolePicker/RolePicker.tsx | 2 +- .../core/components/RolePicker/RolePickerInput.tsx | 2 +- .../core/components/RolePicker/RolePickerMenu.tsx | 2 +- .../components/RolePicker/RolePickerSubMenu.tsx | 1 + .../app/core/components/Select/OrgPicker.test.tsx | 1 + public/app/core/components/help/HelpModal.tsx | 2 +- public/app/features/admin/ServerStatsCard.tsx | 1 + .../alerting/unified/RedirectToRuleViewer.tsx | 2 +- .../unified/components/ConditionalWrap.tsx | 2 +- .../components/GrafanaAlertmanagerWarning.test.tsx | 1 + .../unified/components/WithReturnButton.tsx | 2 +- .../unified/components/common/DetailText.tsx | 2 ++ .../contact-points/ContactPointHeader.tsx | 2 +- .../contact-points/EditContactPoint.test.tsx | 1 + .../contact-points/useExportContactPoint.tsx | 2 +- .../extensions/AlertingRuleExtensionPointMenu.tsx | 2 +- .../mute-timings/useExportMuteTimingsDrawer.tsx | 2 +- .../components/notification-policies/Modals.tsx | 2 +- .../notification-policies/Policy.test.tsx | 1 + .../components/notification-policies/Policy.tsx | 2 +- .../components/permissions/ManagePermissions.tsx | 2 +- .../components/receivers/TemplateDataDocs.tsx | 1 + .../components/receivers/TemplatePreview.test.tsx | 2 +- .../components/receivers/TemplatePreview.tsx | 1 + .../components/receivers/form/ChannelOptions.tsx | 1 + .../components/receivers/form/ChannelSubForm.tsx | 2 +- .../receivers/form/fields/DeletedSubform.tsx | 2 +- .../rule-editor/AnnotationsStep.test.tsx | 1 + .../rule-editor/CloudRulesSourcePicker.tsx | 2 +- .../components/rule-editor/NeedHelpInfo.tsx | 1 + .../unified/components/rule-viewer/DeleteModal.tsx | 2 +- .../components/rule-viewer/RuleViewerLayout.tsx | 1 + .../rule-viewer/RuleViewerVisualization.tsx | 2 ++ .../components/rules/RuleActionsButtons.tsx | 2 +- .../components/rules/RuleDetailsAnnotations.tsx | 1 + .../components/rules/RuleDetailsButtons.tsx | 2 +- .../components/rules/RuleDetailsDataSources.tsx | 2 +- .../components/rules/RuleDetailsExpression.tsx | 1 + .../unified/components/rules/RuleListErrors.tsx | 2 +- .../components/settings/AlertmanagerConfig.tsx | 2 +- .../rule-list/components/RuleActionsButtons.V2.tsx | 2 +- public/app/features/auth-config/ErrorContainer.tsx | 1 + .../AutoGridLayoutManagerEditor.tsx | 8 ++++++-- .../scene/layout-tabs/TabItemRenderer.tsx | 4 +++- .../editors/DataSourceVariableEditor.test.tsx | 1 + .../editors/IntervalVariableEditor.test.tsx | 1 + .../DashboardSettings/GeneralSettings.tsx | 2 +- .../components/PanelEditor/PanelHeaderCorner.tsx | 2 +- .../components/PanelEditor/PanelNotSupported.tsx | 2 +- .../components/ShareModal/ViewJsonModal.tsx | 2 +- .../components/SubMenu/AnnotationPicker.tsx | 2 +- .../DashboardEmptyExtensionPoint.tsx | 2 ++ .../app/features/dashboard/dashgrid/PanelLinks.tsx | 1 + .../datasources/components/DataSourceAddButton.tsx | 2 +- public/app/features/explore/Explore.tsx | 4 +++- .../app/features/explore/Logs/LogsSamplePanel.tsx | 2 +- public/app/features/explore/MetaInfoText.tsx | 2 +- .../TracePageHeader/SpanGraph/Scrubber.test.tsx | 1 + .../components/TraceTimelineViewer/index.tsx | 4 +++- .../extensions/ToolbarExtensionPointMenu.tsx | 2 +- .../expressions/components/QueryToolbox.tsx | 2 +- .../ChangeLibraryPanelModal.tsx | 2 ++ .../LibraryPanelCard/LibraryPanelCard.tsx | 2 +- .../LibraryPanelsSearch/LibraryPanelsSearch.tsx | 2 +- .../OpenLibraryPanelModal.tsx | 2 +- public/app/features/logs/components/LogLabels.tsx | 2 +- .../components/fieldSelector/AvailableFields.tsx | 2 +- .../components/panel/LogListControlsOption.tsx | 2 +- .../DeletePublicDashboardButton.tsx | 1 + .../admin/components/PluginDetailsBody.test.tsx | 1 + .../plugins/admin/components/PluginDetailsBody.tsx | 2 +- .../plugins/admin/components/PluginSubtitle.tsx | 2 +- .../admin/components/VersionInstallButton.test.tsx | 1 + .../plugins/admin/components/VersionList.test.tsx | 1 + .../features/plugins/admin/pages/PluginDetails.tsx | 1 + .../extensions/registry/useRegistrySlice.test.tsx | 2 +- .../plugins/extensions/usePluginComponent.test.tsx | 1 + .../extensions/usePluginComponents.test.tsx | 2 +- .../plugins/extensions/usePluginComponents.tsx | 2 +- .../plugins/extensions/usePluginFunctions.test.tsx | 1 + .../plugins/extensions/usePluginLinks.test.tsx | 1 + .../Wizard/ProvisioningWizard.test.tsx | 1 + .../query/components/QueryActionComponent.ts | 2 ++ .../features/query/components/QueryEditorRow.tsx | 2 +- .../serviceaccounts/ServiceAccountCreatePage.tsx | 2 +- .../serviceaccounts/ServiceAccountPage.tsx | 2 +- .../ServiceAccountsListPage.test.tsx | 1 + .../serviceaccounts/ServiceAccountsListPage.tsx | 2 +- .../components/ServiceAccountProfile.tsx | 2 +- .../components/ServiceAccountProfileRow.tsx | 2 +- .../components/ServiceAccountRoleRow.tsx | 2 ++ .../components/ServiceAccountTokensTable.tsx | 1 + .../support-bundles/SupportBundlesCreate.tsx | 2 +- public/app/features/users/UsersActionBar.tsx | 1 + .../datasource/DataSourceVariableEditor.test.tsx | 1 + .../variables/inspect/NetworkGraphModal.tsx | 2 +- .../ConfigEditor/AzureCredentialsForm.tsx | 2 +- .../CurrentUserFallbackCredentials.tsx | 2 +- .../components/TracesQueryEditor/Filter.tsx | 2 +- .../SecureSocksProxySettingsNewStyling.tsx | 2 ++ .../LogsQueryEditor/LogsQueryEditor.tsx | 2 +- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 3 +-- .../components/QueryEditor/QueryEditor.tsx | 2 +- .../components/QueryEditor/QueryHeader.tsx | 2 ++ .../configuration/MappingsConfiguration.tsx | 2 +- .../graphite/configuration/MappingsHelp.tsx | 2 ++ .../query/influxql/QueryEditorModeSwitcher.tsx | 2 +- .../query/influxql/code/RawInfluxQLEditor.tsx | 2 +- .../editor/query/influxql/visual/AddButton.tsx | 2 ++ .../query/influxql/visual/FormatAsSection.tsx | 1 + .../editor/query/influxql/visual/FromSection.tsx | 2 ++ .../editor/query/influxql/visual/InputSection.tsx | 1 + .../query/influxql/visual/OrderByTimeSection.tsx | 1 + .../query/influxql/visual/PartListSection.tsx | 2 +- .../editor/query/influxql/visual/Seg.tsx | 2 +- .../editor/query/influxql/visual/TagsSection.tsx | 2 ++ .../influxql/visual/VisualInfluxQLEditor.test.tsx | 1 + .../query/influxql/visual/VisualInfluxQLEditor.tsx | 2 +- public/app/plugins/datasource/mssql/types.ts | 2 ++ .../configuration/ConfigEditorPackage.tsx | 1 + .../app/plugins/panel/annolist/AnnoListPanel.tsx | 2 +- .../app/plugins/panel/bargauge/BarGaugePanel.tsx | 2 +- public/app/plugins/panel/gauge/GaugePanel.tsx | 2 +- public/app/plugins/panel/logs/LogsPanel.tsx | 14 +++++++++++--- public/app/plugins/panel/nodeGraph/EdgeLabel.tsx | 2 +- .../app/plugins/panel/nodeGraph/useContextMenu.tsx | 2 +- .../app/plugins/panel/radialbar/RadialBarPanel.tsx | 2 ++ public/app/plugins/panel/stat/StatPanel.tsx | 2 +- public/app/routes/RoutesWrapper.tsx | 2 +- 193 files changed, 238 insertions(+), 116 deletions(-) diff --git a/packages/grafana-data/src/utils/OptionsUIBuilders.ts b/packages/grafana-data/src/utils/OptionsUIBuilders.ts index b88f633ca5e..78a80efb6e1 100644 --- a/packages/grafana-data/src/utils/OptionsUIBuilders.ts +++ b/packages/grafana-data/src/utils/OptionsUIBuilders.ts @@ -1,4 +1,5 @@ import { set, cloneDeep } from 'lodash'; +import type { JSX } from 'react'; import { FieldNamePickerConfigSettings, diff --git a/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx b/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx index 94ea6281492..3551cdcb336 100644 --- a/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx +++ b/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx @@ -1,5 +1,6 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/AlertingSettingsOverhaul.tsx import { cx } from '@emotion/css'; +import type { JSX } from 'react'; import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; diff --git a/packages/grafana-prometheus/src/configuration/shared/utils.tsx b/packages/grafana-prometheus/src/configuration/shared/utils.tsx index 699d0e7f706..b0a1638cab3 100644 --- a/packages/grafana-prometheus/src/configuration/shared/utils.tsx +++ b/packages/grafana-prometheus/src/configuration/shared/utils.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx b/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx index b57a795c3b5..b62ae46f27c 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx +++ b/packages/grafana-runtime/src/services/pluginExtensions/utils.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import React from 'react'; +import React, { type JSX } from 'react'; import { ComponentTypeWithExtensionMeta, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 639bf41ca13..8478081182c 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -1,6 +1,6 @@ // Library import { cx } from '@emotion/css'; -import { CSSProperties, PureComponent, ReactNode } from 'react'; +import { CSSProperties, PureComponent, ReactNode, type JSX } from 'react'; import * as React from 'react'; import tinycolor from 'tinycolor2'; diff --git a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx index d13408d3271..8eee4b3ad12 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx @@ -1,4 +1,4 @@ -import { CSSProperties } from 'react'; +import { CSSProperties, type JSX } from 'react'; import * as React from 'react'; import tinycolor from 'tinycolor2'; diff --git a/packages/grafana-ui/src/components/Button/Button.test.tsx b/packages/grafana-ui/src/components/Button/Button.test.tsx index b78b9aafe5d..d82291f3263 100644 --- a/packages/grafana-ui/src/components/Button/Button.test.tsx +++ b/packages/grafana-ui/src/components/Button/Button.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { Button, LinkButton } from './Button'; diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx index 8c107a20129..e46107b6b02 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx @@ -1,5 +1,6 @@ import { action } from '@storybook/addon-actions'; import { StoryFn, Meta } from '@storybook/react'; +import type { JSX } from 'react'; import { Button } from '../Button/Button'; diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx index cd859f77256..7b63d5cf296 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.test.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.test.tsx index 0cf2a5ee010..224bc6bb2b9 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.test.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.test.tsx @@ -1,5 +1,6 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { ClipboardButton } from './ClipboardButton'; diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx index 817df10153d..f68093ce9f5 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { Collapse } from './Collapse'; diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx index 5fd64e37772..f3cdb9e07b1 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx index b57f9d8557b..e4125db7204 100644 --- a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; +import type { JSX } from 'react'; import { IconName } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx index 2f145e26baf..d439938d26e 100644 --- a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, type JSX } from 'react'; import * as React from 'react'; import { ContextMenu } from '../ContextMenu/ContextMenu'; diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 77cd0b63579..82215940405 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { RefCallback, useCallback, useEffect, useRef } from 'react'; +import { RefCallback, useCallback, useEffect, useRef, type JSX } from 'react'; import * as React from 'react'; import Scrollbars, { positionValues } from 'react-custom-scrollbars-2'; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index dc85dca5464..5e3b71cbe73 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { CSSProperties } from 'react'; +import { CSSProperties, type JSX } from 'react'; import * as React from 'react'; import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx index 98570eb2e15..4ec3bc939c2 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx index d8708ddb470..0e6b9796afa 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 0719693e625..459a7d56e1c 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; -import { memo, createRef, useState, useEffect } from 'react'; +import { memo, createRef, useState, useEffect, type JSX } from 'react'; import { rangeUtil, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOption.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOption.tsx index cf3a2dec556..7957587508d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOption.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOption.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { isString } from 'lodash'; -import { PropsWithChildren, RefCallback } from 'react'; +import { PropsWithChildren, RefCallback, type JSX } from 'react'; import * as React from 'react'; import { GrafanaTheme2, SelectableValue, getTimeZoneInfo } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/DateTimePickers/utils/useTimeSync.tsx b/packages/grafana-ui/src/components/DateTimePickers/utils/useTimeSync.tsx index 3aa15a7fb98..89d151b689f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/utils/useTimeSync.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/utils/useTimeSync.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, type JSX } from 'react'; import { usePrevious } from 'react-use'; import { TimeRange } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx index 72f353b4ed7..4373ea2f94b 100644 --- a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx +++ b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/FilterPill/FilterPill.test.tsx b/packages/grafana-ui/src/components/FilterPill/FilterPill.test.tsx index 1fff175840f..eae37775224 100644 --- a/packages/grafana-ui/src/components/FilterPill/FilterPill.test.tsx +++ b/packages/grafana-ui/src/components/FilterPill/FilterPill.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { FilterPill } from './FilterPill'; diff --git a/packages/grafana-ui/src/components/Forms/FieldArray.tsx b/packages/grafana-ui/src/components/Forms/FieldArray.tsx index 5c21767ab84..ad8f18b16c2 100644 --- a/packages/grafana-ui/src/components/Forms/FieldArray.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldArray.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, type JSX } from 'react'; import { useFieldArray, UseFieldArrayProps } from 'react-hook-form'; import { FieldArrayApi } from '../../types/forms'; diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx index a45a854a045..5bcd70c2585 100644 --- a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.tsx @@ -14,7 +14,7 @@ export enum LegacyInputStatus { export interface Props extends React.HTMLProps { validationEvents?: ValidationEvents; hideErrorMessage?: boolean; - inputRef?: React.LegacyRef; + inputRef?: React.Ref; // Override event props and append status as argument onBlur?: (event: React.FocusEvent, status?: LegacyInputStatus) => void; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 8c050c109c0..133826864ec 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -168,7 +168,9 @@ export class Gauge extends PureComponent { const gaugeElement = (
(this.canvasElement = element)} + ref={(element) => { + this.canvasElement = element; + }} /> ); diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx index bf3e0e7f69c..52e8ed8e567 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/List/AbstractList.tsx b/packages/grafana-ui/src/components/List/AbstractList.tsx index 920b6cf0b5b..de1d360800f 100644 --- a/packages/grafana-ui/src/components/List/AbstractList.tsx +++ b/packages/grafana-ui/src/components/List/AbstractList.tsx @@ -1,5 +1,5 @@ import { cx, css } from '@emotion/css'; -import { PureComponent } from 'react'; +import { PureComponent, type JSX } from 'react'; import { stylesFactory } from '../../themes/stylesFactory'; diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index fc00bc9e562..aaeb2c3e426 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -2,7 +2,7 @@ import { cx } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { OverlayContainer, useOverlay } from '@react-aria/overlays'; -import { PropsWithChildren, useRef } from 'react'; +import { PropsWithChildren, useRef, type JSX } from 'react'; import * as React from 'react'; import { t } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index c0c25d4186b..57afc826b93 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useMemo } from 'react'; +import { useMemo, type JSX } from 'react'; import { t } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx index f66ca51bb86..608795b219b 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx index 053bddb3ad3..1ce8a68dc87 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -211,7 +211,9 @@ export class UnThemedQueryField extends PureComponent
(this.editor = editor!)} + ref={(editor) => { + this.editor = editor!; + }} schema={SCHEMA} autoCorrect={false} readOnly={this.props.disabled} diff --git a/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx b/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx index 3fb806bc162..456d5044dd3 100644 --- a/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx +++ b/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, PropsWithChildren } from 'react'; +import { HTMLAttributes, PropsWithChildren, type JSX } from 'react'; import * as React from 'react'; import { textUtil } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx index d8278697334..c0a829dc760 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx @@ -1,6 +1,6 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; -import { useState } from 'react'; +import { useState, type JSX } from 'react'; import * as React from 'react'; import { Icon } from '../Icon/Icon'; diff --git a/packages/grafana-ui/src/components/Select/InputControl.tsx b/packages/grafana-ui/src/components/Select/InputControl.tsx index 4635b6a27dc..f45ba1c74b3 100644 --- a/packages/grafana-ui/src/components/Select/InputControl.tsx +++ b/packages/grafana-ui/src/components/Select/InputControl.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { forwardRef } from 'react'; +import { forwardRef, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Select/MultiValue.tsx b/packages/grafana-ui/src/components/Select/MultiValue.tsx index 987d34a1290..31bd3d23d91 100644 --- a/packages/grafana-ui/src/components/Select/MultiValue.tsx +++ b/packages/grafana-ui/src/components/Select/MultiValue.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { JSX } from 'react'; import { t } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index bfb8b17a1c1..37bd1452e91 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { max } from 'lodash'; -import { RefCallback, useLayoutEffect, useMemo, useRef } from 'react'; +import { RefCallback, useLayoutEffect, useMemo, useRef, type JSX } from 'react'; import * as React from 'react'; import { FixedSizeList as List } from 'react-window'; diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 3512ab9fbee..6ed93a5c0e5 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { JSX } from 'react'; import { ActionMeta as SelectActionMeta, CommonProps as ReactSelectCommonProps, diff --git a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx index c28e2b37a39..5d6a6e4984a 100644 --- a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx +++ b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { IconName } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx b/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx index 16d37fc3f4e..6328b157b29 100644 --- a/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx @@ -1,5 +1,6 @@ import WKT from 'ol/format/WKT'; import { Geometry } from 'ol/geom'; +import type { JSX } from 'react'; import { TableCellProps } from '../types'; diff --git a/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx index 6e8b678d528..c9476244bcf 100644 --- a/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { isString } from 'lodash'; -import { useState } from 'react'; +import { useState, type JSX } from 'react'; import { getCellLinks } from '../../../utils/table'; import { CellActions } from '../CellActions'; diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index a3772785d63..798ca4e1ae0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -2,7 +2,7 @@ import 'react-data-grid/lib/styles.css'; import { clsx } from 'clsx'; import memoize from 'micro-memoize'; -import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; +import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState, type JSX } from 'react'; import { Cell, CellRendererProps, diff --git a/packages/grafana-ui/src/components/Tabs/Tabs.test.tsx b/packages/grafana-ui/src/components/Tabs/Tabs.test.tsx index 91b0aabce60..1caafa6ef44 100644 --- a/packages/grafana-ui/src/components/Tabs/Tabs.test.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tabs.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { Tab } from './Tab'; import { TabsBar } from './TabsBar'; diff --git a/packages/grafana-ui/src/components/Tags/Tag.test.tsx b/packages/grafana-ui/src/components/Tags/Tag.test.tsx index 48fce39b162..b5535284105 100644 --- a/packages/grafana-ui/src/components/Tags/Tag.test.tsx +++ b/packages/grafana-ui/src/components/Tags/Tag.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { Tag } from './Tag'; diff --git a/packages/grafana-ui/src/components/Tags/TagList.test.tsx b/packages/grafana-ui/src/components/Tags/TagList.test.tsx index 7c0d8a0314b..f8a4a393c88 100644 --- a/packages/grafana-ui/src/components/Tags/TagList.test.tsx +++ b/packages/grafana-ui/src/components/Tags/TagList.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { TagList } from './TagList'; diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx index 41cc4e55ab4..002f15ec04b 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -11,7 +11,7 @@ import { useInteractions, } from '@floating-ui/react'; import { Placement } from '@popperjs/core'; -import { memo, cloneElement, isValidElement, useRef, useState } from 'react'; +import { memo, cloneElement, isValidElement, useRef, useState, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; diff --git a/packages/grafana-ui/src/components/Toggletip/types.ts b/packages/grafana-ui/src/components/Toggletip/types.ts index 62d0335a868..ff730df05b2 100644 --- a/packages/grafana-ui/src/components/Toggletip/types.ts +++ b/packages/grafana-ui/src/components/Toggletip/types.ts @@ -1,3 +1,4 @@ +import type { JSX } from 'react'; export interface ToggletipContentProps { /** * @deprecated diff --git a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx index 7128a67528a..1e792aa95d8 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx @@ -1,5 +1,5 @@ import { Placement } from '@popperjs/core'; -import { Component } from 'react'; +import { Component, type JSX } from 'react'; import { PopoverContent } from './types'; diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 665e2d7bf6c..59902c5c953 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -10,7 +10,7 @@ import { useInteractions, safePolygon, } from '@floating-ui/react'; -import { forwardRef, cloneElement, isValidElement, useCallback, useId, useRef, useState } from 'react'; +import { forwardRef, cloneElement, isValidElement, useCallback, useId, useRef, useState, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; diff --git a/packages/grafana-ui/src/components/Tooltip/types.ts b/packages/grafana-ui/src/components/Tooltip/types.ts index ac01b7f5409..dcdede5e19c 100644 --- a/packages/grafana-ui/src/components/Tooltip/types.ts +++ b/packages/grafana-ui/src/components/Tooltip/types.ts @@ -1,4 +1,5 @@ import { Placement } from '@floating-ui/react'; +import type { JSX } from 'react'; export interface PopoverContentProps { /** diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx index e5cf2ccce1d..42f6d57c79e 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { ValuePicker } from './ValuePicker'; diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx index 0383aa1eab1..b0f578fae79 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/VizLegend/types.ts b/packages/grafana-ui/src/components/VizLegend/types.ts index 4032276bf6a..3c0de880792 100644 --- a/packages/grafana-ui/src/components/VizLegend/types.ts +++ b/packages/grafana-ui/src/components/VizLegend/types.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { JSX } from 'react'; import { DataFrameFieldIndex, DisplayValue } from '@grafana/data'; import { LegendDisplayMode, LegendPlacement, LineStyle } from '@grafana/schema'; diff --git a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx index 8a42baf89d7..a1fb6c6a1c3 100644 --- a/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx +++ b/packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx @@ -1,5 +1,5 @@ import { clamp } from 'lodash'; -import { PureComponent, CSSProperties } from 'react'; +import { PureComponent, CSSProperties, type JSX } from 'react'; import * as React from 'react'; import { VizOrientation } from '@grafana/data'; diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx index 759e041decd..d926214c3cb 100644 --- a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx +++ b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx @@ -1,7 +1,7 @@ // Libraries import $ from 'jquery'; import { uniqBy } from 'lodash'; -import { PureComponent } from 'react'; +import { PureComponent, type JSX } from 'react'; import * as React from 'react'; // Types @@ -384,7 +384,9 @@ export class Graph extends PureComponent {
(this.element = e)} + ref={(e) => { + this.element = e; + }} style={{ height, width }} onMouseLeave={() => { this.setState({ isTooltipVisible: false }); diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx index 3361b077b03..8c38dc62708 100644 --- a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx +++ b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx @@ -1,5 +1,5 @@ import { difference, isEqual } from 'lodash'; -import { Component } from 'react'; +import { Component, type JSX } from 'react'; import * as React from 'react'; import { GraphSeriesXY } from '@grafana/data'; diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx index 80a12637f18..9570f7255e9 100644 --- a/public/app/core/components/Animations/FadeIn.tsx +++ b/public/app/core/components/Animations/FadeIn.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, useRef } from 'react'; +import { CSSProperties, useRef, type JSX } from 'react'; import Transition, { ExitHandler } from 'react-transition-group/Transition'; interface Props { diff --git a/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx b/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx index 96d7d68d8ad..9f3e37555d6 100644 --- a/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx +++ b/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { PluginExtensionPoints } from '@grafana/data'; import { config, renderLimitedComponents, usePluginComponents } from '@grafana/runtime'; import { useGrafana } from 'app/core/context/GrafanaContext'; diff --git a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx b/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx index 6678e57a2ec..ca660e8750c 100644 --- a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/core/components/Branding/Branding.tsx b/public/app/core/components/Branding/Branding.tsx index b32199ed4c7..e44eb62658b 100644 --- a/public/app/core/components/Branding/Branding.tsx +++ b/public/app/core/components/Branding/Branding.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { FC } from 'react'; +import { FC, type JSX } from 'react'; import { colorManipulator } from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index 1f8f3e5557d..6f2ba1c6c91 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, type JSX } from 'react'; import { t } from '@grafana/i18n'; import { ComboboxOption, MultiCombobox } from '@grafana/ui'; diff --git a/public/app/core/components/Layers/LayerDragDropList.tsx b/public/app/core/components/Layers/LayerDragDropList.tsx index d90dfdf04af..b2eca9ca2f1 100644 --- a/public/app/core/components/Layers/LayerDragDropList.tsx +++ b/public/app/core/components/Layers/LayerDragDropList.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index 9ea5cba50cf..1a5f3d572e4 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -1,4 +1,4 @@ -import { memo, useState, useCallback } from 'react'; +import { memo, useState, useCallback, type JSX } from 'react'; import { t } from '@grafana/i18n'; import { FetchError, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; diff --git a/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx b/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx index 61e3a78d800..4ab642ac551 100644 --- a/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx +++ b/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState, type JSX } from 'react'; import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index 25cff0889a1..948b8af9506 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; +import { FormEvent, useCallback, useEffect, useState, useRef, type JSX } from 'react'; import { OrgRole } from '@grafana/data'; import { ClickOutsideWrapper, Portal, useTheme2 } from '@grafana/ui'; diff --git a/public/app/core/components/RolePicker/RolePickerInput.tsx b/public/app/core/components/RolePicker/RolePickerInput.tsx index 3f8158020c3..9acdbed24d9 100644 --- a/public/app/core/components/RolePicker/RolePickerInput.tsx +++ b/public/app/core/components/RolePicker/RolePickerInput.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { FormEvent, HTMLProps, useEffect, useRef } from 'react'; +import { FormEvent, HTMLProps, useEffect, useRef, type JSX } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 81c38519ae6..4287d17c979 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type JSX } from 'react'; import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; diff --git a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx index 3dff3fc88be..1254dac06a4 100644 --- a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx @@ -1,4 +1,5 @@ import { cx } from '@emotion/css'; +import type { JSX } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, useStyles2, useTheme2 } from '@grafana/ui'; diff --git a/public/app/core/components/Select/OrgPicker.test.tsx b/public/app/core/components/Select/OrgPicker.test.tsx index 38344d1fd8a..4bab94faefa 100644 --- a/public/app/core/components/Select/OrgPicker.test.tsx +++ b/public/app/core/components/Select/OrgPicker.test.tsx @@ -1,5 +1,6 @@ import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { OrgPicker } from './OrgPicker'; diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 82f39fac28f..e334d23982e 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; +import { useMemo, type JSX } from 'react'; import { useAssistant } from '@grafana/assistant'; import { FeatureState, GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/features/admin/ServerStatsCard.tsx b/public/app/features/admin/ServerStatsCard.tsx index 070005e7e3a..872d3844e0b 100644 --- a/public/app/features/admin/ServerStatsCard.tsx +++ b/public/app/features/admin/ServerStatsCard.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import type { JSX } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx index 96410c28320..bbcd025d1db 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; +import { type JSX, useMemo } from 'react'; import { Navigate } from 'react-router-dom-v5-compat'; import { useLocation } from 'react-use'; diff --git a/public/app/features/alerting/unified/components/ConditionalWrap.tsx b/public/app/features/alerting/unified/components/ConditionalWrap.tsx index 55b80d9ce45..559abb737d3 100644 --- a/public/app/features/alerting/unified/components/ConditionalWrap.tsx +++ b/public/app/features/alerting/unified/components/ConditionalWrap.tsx @@ -1,4 +1,4 @@ -import { Ref, cloneElement, forwardRef } from 'react'; +import { type JSX, Ref, cloneElement, forwardRef } from 'react'; interface ConditionalWrapProps { shouldWrap: boolean; diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerWarning.test.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerWarning.test.tsx index 52c87f5f41c..dbfa2dbfeba 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerWarning.test.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerWarning.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; +import type { JSX } from 'react'; import { Provider } from 'react-redux'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; diff --git a/public/app/features/alerting/unified/components/WithReturnButton.tsx b/public/app/features/alerting/unified/components/WithReturnButton.tsx index 6b266949e1b..2308df8f063 100644 --- a/public/app/features/alerting/unified/components/WithReturnButton.tsx +++ b/public/app/features/alerting/unified/components/WithReturnButton.tsx @@ -1,4 +1,4 @@ -import { cloneElement, useCallback } from 'react'; +import { type JSX, cloneElement, useCallback } from 'react'; import { useReturnToPrevious } from '@grafana/runtime'; diff --git a/public/app/features/alerting/unified/components/common/DetailText.tsx b/public/app/features/alerting/unified/components/common/DetailText.tsx index f35c0765a60..296c5c79818 100644 --- a/public/app/features/alerting/unified/components/common/DetailText.tsx +++ b/public/app/features/alerting/unified/components/common/DetailText.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { t } from '@grafana/i18n'; import { Box, ClipboardButton, Stack, Text, Tooltip } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx index 1825b5e40a0..0fb1403cb35 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { Fragment, useState } from 'react'; +import { Fragment, type JSX, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index 75413e20e33..9bc321a40a6 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -1,4 +1,5 @@ import 'core-js/stable/structured-clone'; +import type { JSX } from 'react'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { render, screen, within } from 'test/test-utils'; diff --git a/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx index 3dcb8753ccc..3638497e2a7 100644 --- a/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { type JSX, useCallback, useMemo, useState } from 'react'; import { useToggle } from 'react-use'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; diff --git a/public/app/features/alerting/unified/components/extensions/AlertingRuleExtensionPointMenu.tsx b/public/app/features/alerting/unified/components/extensions/AlertingRuleExtensionPointMenu.tsx index 20ef95fdb22..ef7f9411a45 100644 --- a/public/app/features/alerting/unified/components/extensions/AlertingRuleExtensionPointMenu.tsx +++ b/public/app/features/alerting/unified/components/extensions/AlertingRuleExtensionPointMenu.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useMemo } from 'react'; +import { type JSX, ReactElement, useMemo } from 'react'; import { PluginExtensionLink } from '@grafana/data'; import { Menu } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer.tsx b/public/app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer.tsx index 3d65d91aa9a..b9cc9d1af55 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { type JSX, useCallback, useMemo, useState } from 'react'; import { useToggle } from 'react-use'; import { GrafanaMuteTimingsExporter } from '../export/GrafanaMuteTimingsExporter'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Modals.tsx b/public/app/features/alerting/unified/components/notification-policies/Modals.tsx index d3187c41a7e..20c222749fc 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Modals.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Modals.tsx @@ -1,5 +1,5 @@ import { groupBy } from 'lodash'; -import { FC, useCallback, useMemo, useState } from 'react'; +import { FC, type JSX, useCallback, useMemo, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, Icon, Modal, ModalProps, Spinner, Stack } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx index f27d5503d12..a10bca42100 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx @@ -1,6 +1,7 @@ import { renderHook, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { first, noop } from 'lodash'; +import type { JSX } from 'react'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { render } from 'test/test-utils'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 3f80f6cd902..c4b6a0c55b7 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { isArray, sumBy, uniqueId } from 'lodash'; import pluralize from 'pluralize'; import * as React from 'react'; -import { FC, Fragment, ReactNode, useState } from 'react'; +import { FC, Fragment, type JSX, ReactNode, useState } from 'react'; import { useToggle } from 'react-use'; import { AlertLabel, getInheritedProperties } from '@grafana/alerting'; diff --git a/public/app/features/alerting/unified/components/permissions/ManagePermissions.tsx b/public/app/features/alerting/unified/components/permissions/ManagePermissions.tsx index 33ebf71d59a..8ba91403352 100644 --- a/public/app/features/alerting/unified/components/permissions/ManagePermissions.tsx +++ b/public/app/features/alerting/unified/components/permissions/ManagePermissions.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, useState } from 'react'; +import { ComponentProps, type JSX, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, Drawer } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx b/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx index 4252cfde654..239b8e9abf4 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateDataDocs.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import * as React from 'react'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx index fd38b545945..e4e1f441860 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.test.tsx @@ -1,4 +1,4 @@ -import { default as React } from 'react'; +import { type JSX, default as React } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { Provider } from 'react-redux'; import { render, screen, waitFor, within } from 'test/test-utils'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 5e5ecc1e08b..c4b154809b8 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -1,6 +1,7 @@ import { css, cx } from '@emotion/css'; import { compact, uniqueId } from 'lodash'; import * as React from 'react'; +import type { JSX } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index fbdfdbbf493..16042c0e654 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { JSX } from 'react'; import { DeepMap, FieldError, FieldErrors, useFormContext } from 'react-hook-form'; import { Field, SecretInput } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 0202e7c2e52..645c6565a01 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { sortBy } from 'lodash'; import * as React from 'react'; -import { useEffect, useMemo } from 'react'; +import { type JSX, useEffect, useMemo } from 'react'; import { Controller, FieldErrors, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx index bb5537cbbee..092e64f1da4 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { type JSX, useEffect } from 'react'; import { useFormContext } from 'react-hook-form'; interface Props { diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx index a8f73230c21..5fb3267432a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.test.tsx @@ -1,3 +1,4 @@ +import type { JSX } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { render, screen, within } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; diff --git a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx index 72ec6200ec5..8479b335181 100644 --- a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { type JSX, useCallback } from 'react'; import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourcePicker, DataSourcePickerProps } from 'app/features/datasources/components/picker/DataSourcePicker'; diff --git a/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx b/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx index 04b28bd3bf4..0be96f0bdc2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx index a26a9709d38..b29257a971d 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { type JSX, useCallback, useMemo, useState } from 'react'; import { t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx index 1f61f10e640..d9c61ca8fed 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import * as React from 'react'; +import type { JSX } from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx index c55b01c6d16..93f6b73feab 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { PanelData } from '@grafana/data'; import { VizWrapper } from '../rule-editor/VizWrapper'; diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 206d939811f..68aed9c2dfe 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -1,5 +1,5 @@ import { isString } from 'lodash'; -import { useState } from 'react'; +import { type JSX, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { LinkButton, Stack } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx index b2ff856a498..367ca5f5d6a 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsAnnotations.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx index 28cd663bdeb..8f733cd6df0 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsButtons.tsx @@ -1,4 +1,4 @@ -import { Fragment } from 'react'; +import { Fragment, type JSX } from 'react'; import { textUtil } from '@grafana/data'; import { Trans } from '@grafana/i18n'; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx index 3ba34bf86fa..fc03406cffd 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; +import { type JSX, useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsExpression.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsExpression.tsx index d6d472886d4..3e60213a82f 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsExpression.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsExpression.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import type { JSX } from 'react'; import { t } from '@grafana/i18n'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; diff --git a/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx b/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx index 58cc844d92c..6a35dbe5d0b 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListErrors.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { SerializedError } from '@reduxjs/toolkit'; -import { FC, ReactElement, useMemo, useState } from 'react'; +import { FC, type JSX, ReactElement, useMemo, useState } from 'react'; import { useLocalStorage } from 'react-use'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx index 8282d16ed97..f853c19357b 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useState } from 'react'; +import { type JSX, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import AutoSizer from 'react-virtualized-auto-sizer'; diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx index 393355ade61..a73d27f7165 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx @@ -1,5 +1,5 @@ import { isString } from 'lodash'; -import { useState } from 'react'; +import { type JSX, useState } from 'react'; import { RequireAtLeastOne } from 'type-fest'; import { Trans, t } from '@grafana/i18n'; diff --git a/public/app/features/auth-config/ErrorContainer.tsx b/public/app/features/auth-config/ErrorContainer.tsx index 1a51263a364..148ee9d33da 100644 --- a/public/app/features/auth-config/ErrorContainer.tsx +++ b/public/app/features/auth-config/ErrorContainer.tsx @@ -1,3 +1,4 @@ +import type { JSX } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Alert } from '@grafana/ui'; diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx index fb2416f437c..2ab55869429 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx @@ -117,7 +117,9 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan id="min-column-width" defaultValue={columnWidth} onBlur={onCustomMinWidthChanged} - ref={(ref) => setInputRef(ref)} + ref={(ref) => { + setInputRef(ref); + }} type="number" min={50} max={2000} @@ -233,7 +235,9 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage id="min-height" defaultValue={rowHeight} onBlur={onCustomHeightChanged} - ref={(ref) => setInputRef(ref)} + ref={(ref) => { + setInputRef(ref); + }} type="number" min={50} max={2000} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index c9af2bc439c..0fa22e9d305 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -56,7 +56,9 @@ export function TabItemRenderer({ model }: SceneComponentProps) { {(dragProvided, dragSnapshot) => (
dragProvided.innerRef(ref)} + ref={(ref) => { + dragProvided.innerRef(ref); + }} className={cx(dragSnapshot.isDragging && styles.dragging)} {...dragProvided.draggableProps} {...dragProvided.dragHandleProps} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.test.tsx index 44e0cb9a5e2..b376bc2b0e5 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.test.tsx @@ -2,6 +2,7 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { DataSourceVariable } from '@grafana/scenes'; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx index ef51fde16f6..392f31ec92f 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx @@ -2,6 +2,7 @@ import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { JSX } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { IntervalVariable } from '@grafana/scenes'; diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index d7b32cfabb2..461052cbe2a 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -1,4 +1,4 @@ -import { useCallback, ChangeEvent, useState } from 'react'; +import { useCallback, ChangeEvent, useState, type JSX } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { TimeZone } from '@grafana/data'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx index 2ba5aec62bb..57fe4325b14 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useCallback } from 'react'; +import { useCallback, type JSX } from 'react'; import { GrafanaTheme2, renderMarkdown, LinkModelSupplier, ScopedVars, IconName } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx b/public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx index 0e52eb1c74c..1d1b739f87a 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelNotSupported.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, type JSX } from 'react'; import { Trans } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; diff --git a/public/app/features/dashboard/components/ShareModal/ViewJsonModal.tsx b/public/app/features/dashboard/components/ShareModal/ViewJsonModal.tsx index 926cc15cd45..9e2b0c6f578 100644 --- a/public/app/features/dashboard/components/ShareModal/ViewJsonModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ViewJsonModal.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, type JSX } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { Trans, t } from '@grafana/i18n'; diff --git a/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx b/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx index 78b04d0bef4..d4e11bdb3ae 100644 --- a/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx +++ b/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, type JSX } from 'react'; import { AnnotationQuery, EventBus, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmptyExtensionPoint.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmptyExtensionPoint.tsx index 5e7a551514f..f604ca3e778 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmptyExtensionPoint.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmptyExtensionPoint.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { PluginExtensionPoints } from '@grafana/data'; import { config, renderLimitedComponents, usePluginComponents } from '@grafana/runtime'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; diff --git a/public/app/features/dashboard/dashgrid/PanelLinks.tsx b/public/app/features/dashboard/dashgrid/PanelLinks.tsx index 3ad8fca5d9f..6e1a5cb2fb3 100644 --- a/public/app/features/dashboard/dashgrid/PanelLinks.tsx +++ b/public/app/features/dashboard/dashgrid/PanelLinks.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { DataLink, GrafanaTheme2, LinkModel } from '@grafana/data'; import { t } from '@grafana/i18n'; diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index f765188b50d..a3af80ccf6c 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, type JSX } from 'react'; import { Pages } from '@grafana/e2e-selectors'; import { Trans } from '@grafana/i18n'; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 79678f4e15a..39c5b09dc6c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -640,7 +640,9 @@ export class Explore extends PureComponent { )} (this.scrollElement = scrollElement || undefined)} + ref={(scrollElement) => { + this.scrollElement = scrollElement || undefined; + }} >
{datasourceInstance ? ( diff --git a/public/app/features/explore/Logs/LogsSamplePanel.tsx b/public/app/features/explore/Logs/LogsSamplePanel.tsx index 5dfba3eee39..0f99fde8063 100644 --- a/public/app/features/explore/Logs/LogsSamplePanel.tsx +++ b/public/app/features/explore/Logs/LogsSamplePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useRef } from 'react'; +import { useRef, type JSX } from 'react'; import { CoreApp, diff --git a/public/app/features/explore/MetaInfoText.tsx b/public/app/features/explore/MetaInfoText.tsx index d2106cb155d..8e3dfb48a3d 100644 --- a/public/app/features/explore/MetaInfoText.tsx +++ b/public/app/features/explore/MetaInfoText.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { memo } from 'react'; +import { memo, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx index b57f4274519..6cab9c42f80 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.test.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { render, screen, fireEvent, within } from '@testing-library/react'; +import type { JSX } from 'react'; import Scrubber, { ScrubberProps } from './Scrubber'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx index 5843cdec64e..ce86d210b63 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx @@ -197,7 +197,9 @@ export class UnthemedTraceTimelineViewer extends PureComponent { return (
ref && this.setState({ height: ref.getBoundingClientRect().height })} + ref={(ref: HTMLDivElement | null) => { + ref && this.setState({ height: ref.getBoundingClientRect().height }); + }} > { return ( <> - {query.metricQueryType === MetricQueryType.Search && ( <> {query.metricEditorMode === MetricEditorMode.Builder && ( diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx index 04bf910f1d4..2999ff7f79b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, type JSX } from 'react'; import { QueryEditorProps } from '@grafana/data'; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx index 57568a0666e..78da93ba3a1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { CoreApp, LoadingState, QueryEditorProps, SelectableValue } from '@grafana/data'; import { EditorHeader, InlineSelect, FlexItem } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; diff --git a/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx index 0df6363a7ce..39f90596fb1 100644 --- a/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx +++ b/public/app/plugins/datasource/graphite/configuration/MappingsConfiguration.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from 'react'; +import { ChangeEvent, useState, type JSX } from 'react'; import { Box, Button, Icon, InlineField, InlineFieldRow, Input } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx b/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx index 37a93580ca1..12a0a581e8e 100644 --- a/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx +++ b/public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { Alert } from '@grafana/ui'; type Props = { diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/QueryEditorModeSwitcher.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/QueryEditorModeSwitcher.tsx index 7bc5dcf6a6e..246f21c9b1c 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/QueryEditorModeSwitcher.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/QueryEditorModeSwitcher.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, type JSX } from 'react'; import { Button, ConfirmModal } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/code/RawInfluxQLEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/code/RawInfluxQLEditor.tsx index 0098e8eb0af..5871e0ebf4c 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/code/RawInfluxQLEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/code/RawInfluxQLEditor.tsx @@ -1,4 +1,4 @@ -import { useId } from 'react'; +import { useId, type JSX } from 'react'; import { Stack, InlineField, Input, Select, TextArea } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/AddButton.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/AddButton.tsx index 7123f816f22..7df491257d2 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/AddButton.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/AddButton.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { SelectableValue } from '@grafana/data'; import { unwrap } from '../utils/unwrap'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FormatAsSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FormatAsSection.tsx index 6bf005185f4..b88cb6d1bc6 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FormatAsSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FormatAsSection.tsx @@ -1,4 +1,5 @@ import { cx } from '@emotion/css'; +import type { JSX } from 'react'; import { Select } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx index 153ec2aa039..b0dd96e711c 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/FromSection.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { AccessoryButton } from '@grafana/plugin-ui'; import { DEFAULT_POLICY } from '../../../../../types'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/InputSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/InputSection.tsx index 98c4ebf30ac..11deca0c8fe 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/InputSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/InputSection.tsx @@ -1,4 +1,5 @@ import { cx } from '@emotion/css'; +import type { JSX } from 'react'; import { Input } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/OrderByTimeSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/OrderByTimeSection.tsx index eab1943a4ea..d2a18d280f7 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/OrderByTimeSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/OrderByTimeSection.tsx @@ -1,4 +1,5 @@ import { cx } from '@emotion/css'; +import type { JSX } from 'react'; import { SelectableValue } from '@grafana/data'; import { Select } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx index 14f188bd683..b02fdeeafa2 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/PartListSection.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { Fragment, useMemo } from 'react'; +import { Fragment, useMemo, type JSX } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { AccessoryButton } from '@grafana/plugin-ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/Seg.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/Seg.tsx index 47b24fcecf6..2adae762b4e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/Seg.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/Seg.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import debouncePromise from 'debounce-promise'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, type JSX } from 'react'; import { useAsyncFn } from 'react-use'; import { SelectableValue } from '@grafana/data'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx index 3edc8320144..ec7a138b590 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { SelectableValue } from '@grafana/data'; import { AccessoryButton } from '@grafana/plugin-ui'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx index 837a56b85e4..ce6e52fde97 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx @@ -1,4 +1,5 @@ import { render, waitFor } from '@testing-library/react'; +import type { JSX } from 'react'; import InfluxDatasource from '../../../../../datasource'; import { getMockInfluxDS, getMockDSInstanceSettings } from '../../../../../mocks/datasource'; diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx index ddc3f82cbb9..ac1a4a8dea6 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useId, useMemo } from 'react'; +import { useId, useMemo, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { InlineLabel, SegmentSection, useStyles2 } from '@grafana/ui'; diff --git a/public/app/plugins/datasource/mssql/types.ts b/public/app/plugins/datasource/mssql/types.ts index 22066dcdd03..c4ed9763cec 100644 --- a/public/app/plugins/datasource/mssql/types.ts +++ b/public/app/plugins/datasource/mssql/types.ts @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { AzureCredentials } from '@grafana/azure-sdk'; import { SQLOptions } from '@grafana/sql'; import { HttpSettingsBaseProps } from '@grafana/ui/internal'; diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditorPackage.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditorPackage.tsx index 68140a80e68..a5bfccd9d23 100644 --- a/public/app/plugins/datasource/prometheus/configuration/ConfigEditorPackage.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditorPackage.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import type { JSX } from 'react'; import { SIGV4ConnectionConfig } from '@grafana/aws-sdk'; import { hasCredentials } from '@grafana/azure-sdk'; diff --git a/public/app/plugins/panel/annolist/AnnoListPanel.tsx b/public/app/plugins/panel/annolist/AnnoListPanel.tsx index de0baf6f375..77dbc76feb3 100644 --- a/public/app/plugins/panel/annolist/AnnoListPanel.tsx +++ b/public/app/plugins/panel/annolist/AnnoListPanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { createRef, PureComponent } from 'react'; +import { createRef, PureComponent, type JSX } from 'react'; import { Subscription } from 'rxjs'; import { diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 0c59c7905c1..d8714fd8f4a 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -1,5 +1,5 @@ import { isNumber } from 'lodash'; -import { PureComponent } from 'react'; +import { PureComponent, type JSX } from 'react'; import { DisplayProcessor, diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 2d8eb4c9610..3ae1988e30a 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { PureComponent, type JSX } from 'react'; import { FieldDisplay, getDisplayProcessor, getFieldDisplayValues, PanelProps } from '@grafana/data'; import { BarGaugeSizing, VizOrientation } from '@grafana/schema'; diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 191e503fd90..3cf47744762 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -576,7 +576,9 @@ export const LogsPanel = ({ onMouseLeave={onLogContainerMouseLeave} className={style.logListContainer} style={height ? { minHeight: height } : undefined} - ref={(element: HTMLDivElement) => setScrollElement(element)} + ref={(element: HTMLDivElement) => { + setScrollElement(element); + }} > {deduplicatedRows.length > 0 && scrollElement && ( )} {!config.featureToggles.newLogsPanel && !showControls && ( - setScrollElement(scrollElement)}> + { + setScrollElement(scrollElement); + }} + >
{showCommonLabels && !isAscending && renderCommonLabels()} {showCommonLabels && !isAscending && renderCommonLabels()} setScrollElement(scrollElement)} + ref={(scrollElement: HTMLDivElement | null) => { + setScrollElement(scrollElement); + }} visualisationType="logs" loading={infiniteScrolling} loadMoreLogs={enableInfiniteScrolling ? loadMoreLogs : undefined} diff --git a/public/app/plugins/panel/nodeGraph/EdgeLabel.tsx b/public/app/plugins/panel/nodeGraph/EdgeLabel.tsx index ee727b4f79b..48f871a3a10 100644 --- a/public/app/plugins/panel/nodeGraph/EdgeLabel.tsx +++ b/public/app/plugins/panel/nodeGraph/EdgeLabel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { memo } from 'react'; +import { memo, type JSX } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; diff --git a/public/app/plugins/panel/nodeGraph/useContextMenu.tsx b/public/app/plugins/panel/nodeGraph/useContextMenu.tsx index 65daef5aeda..8ab331cfb3d 100644 --- a/public/app/plugins/panel/nodeGraph/useContextMenu.tsx +++ b/public/app/plugins/panel/nodeGraph/useContextMenu.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { MouseEvent, useCallback, useState } from 'react'; +import { MouseEvent, useCallback, useState, type JSX } from 'react'; import * as React from 'react'; import { DataFrame, Field, GrafanaTheme2, LinkModel, LinkTarget } from '@grafana/data'; diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index c176b650c24..decd084f375 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + import { DisplayValueAlignmentFactors, FieldDisplay, diff --git a/public/app/plugins/panel/stat/StatPanel.tsx b/public/app/plugins/panel/stat/StatPanel.tsx index 055fc4adc93..40df4acb9eb 100644 --- a/public/app/plugins/panel/stat/StatPanel.tsx +++ b/public/app/plugins/panel/stat/StatPanel.tsx @@ -1,5 +1,5 @@ import { isNumber } from 'lodash'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, type JSX } from 'react'; import { DisplayValueAlignmentFactors, diff --git a/public/app/routes/RoutesWrapper.tsx b/public/app/routes/RoutesWrapper.tsx index dafa5b000c5..2c36d7571f9 100644 --- a/public/app/routes/RoutesWrapper.tsx +++ b/public/app/routes/RoutesWrapper.tsx @@ -1,4 +1,4 @@ -import { ComponentType, ReactNode } from 'react'; +import { ComponentType, ReactNode, type JSX } from 'react'; import { Router } from 'react-router-dom'; import { CompatRouter } from 'react-router-dom-v5-compat'; From 026a0003041192499c3533ee5901d9faa513b00a Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 28 Nov 2025 13:32:25 +0200 Subject: [PATCH 166/423] Provisioning: Prevent duplicate source links (#114577) --- public/app/features/dashboard/api/v1.ts | 5 +++-- public/app/features/dashboard/api/v2.ts | 7 ++++--- .../app/features/provisioning/utils/sourceLink.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index 9414d3531a6..c69093258cb 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -21,7 +21,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { buildSourceLink } from 'app/features/provisioning/utils/sourceLink'; +import { buildSourceLink, removeExistingSourceLinks } from 'app/features/provisioning/utils/sourceLink'; import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -164,7 +164,8 @@ export class K8sDashboardAPI implements DashboardAPI { // Inject source link for repo-managed dashboards const sourceLink = await buildSourceLink(annotations); if (sourceLink) { - result.dashboard.links = [sourceLink, ...(result.dashboard.links || [])]; + const linksWithoutSource = removeExistingSourceLinks(result.dashboard.links); + result.dashboard.links = [sourceLink, ...linksWithoutSource]; } if (dash.metadata.labels?.[DeprecatedInternalId]) { diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index ddf85da7338..35c5e1a7a0d 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -9,8 +9,8 @@ import { AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl, - AnnoKeyMessage, AnnoKeyGrantPermissions, + AnnoKeyMessage, DeprecatedInternalId, Resource, ResourceClient, @@ -18,7 +18,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { buildSourceLink } from 'app/features/provisioning/utils/sourceLink'; +import { buildSourceLink, removeExistingSourceLinks } from 'app/features/provisioning/utils/sourceLink'; import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -79,7 +79,8 @@ export class K8sDashboardV2API // Inject source link for repo-managed dashboards const sourceLink = await buildSourceLink(dashboard.metadata.annotations); if (sourceLink) { - dashboard.spec.links = [sourceLink, ...(dashboard.spec.links || [])]; + const linksWithoutSource = removeExistingSourceLinks(dashboard.spec.links); + dashboard.spec.links = [sourceLink, ...linksWithoutSource]; } return dashboard; diff --git a/public/app/features/provisioning/utils/sourceLink.ts b/public/app/features/provisioning/utils/sourceLink.ts index 6018f463eab..cb8269fe715 100644 --- a/public/app/features/provisioning/utils/sourceLink.ts +++ b/public/app/features/provisioning/utils/sourceLink.ts @@ -16,6 +16,20 @@ import { isValidRepoType } from '../guards'; import { getHasTokenInstructions, getRepoFileUrl } from './git'; +/** + * Find and remove existing source links from the links array. + * A source link is identified by its tooltip matching the source link tooltip translation. + * Returns the links array with source links removed. + */ +export function removeExistingSourceLinks(links: DashboardLink[] | undefined): DashboardLink[] { + if (!links) { + return []; + } + // TODO This is a pretty hacky way to match the source links, needs a better alternative + const sourceLinkTooltip = t('dashboard.source-link.tooltip', 'View source file in repository'); + return links.filter((link) => link.tooltip !== sourceLinkTooltip); +} + /** * Build a source link for a repo-managed dashboard. * Returns undefined if the dashboard is not repo-managed or if the repository is not a git provider. From 11a27ab8707f79171aef10adf0933054eb6ca312 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 28 Nov 2025 12:00:31 +0000 Subject: [PATCH 167/423] Chore: Convert more class components to functional (#114311) * refactor ColorPicker to functional components * don't need memo for these components * convert CustomHeadersSettings to a functional component * ignore Legacy form components * ignore legacy forms in some lint rules * convert JSONFormatter to a functional component * convert WrapperWithState to a functional component * convert StatsPicker to a functional component * convert PopoverController to a functional component * convert UnitPicker to a functional component * fix linting * fix flaky dashboardcontrolsmenu test --- eslint-suppressions.json | 62 ----- eslint.config.js | 14 +- .../components/ColorPicker/ColorPicker.tsx | 128 ++++++----- .../ColorPicker/ColorPickerInput.tsx | 1 - .../ColorPicker/ColorPickerPopover.tsx | 138 +++++------- .../ColorPicker/SeriesColorPickerPopover.tsx | 3 +- .../ColorPicker/SpectrumPalette.tsx | 2 +- .../CustomHeadersSettings.tsx | 213 ++++++++---------- .../JSONFormatter/JSONFormatter.tsx | 65 +++--- .../StatsPicker/StatsPicker.story.tsx | 54 ++--- .../components/StatsPicker/StatsPicker.tsx | 91 ++++---- .../components/Tooltip/PopoverController.tsx | 49 ++-- .../src/components/UnitPicker/UnitPicker.tsx | 92 ++++---- .../scene/DashboardControlsMenu.test.tsx | 10 +- 14 files changed, 390 insertions(+), 532 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 3f5773bf84f..71b855ae3b9 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -567,14 +567,6 @@ "packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "packages/grafana-ui/src/components/Combobox/Combobox.story.tsx": { @@ -605,9 +597,6 @@ "packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "packages/grafana-ui/src/components/DataSourceSettings/types.ts": { @@ -644,14 +633,8 @@ "@typescript-eslint/consistent-type-assertions": { "count": 2 }, - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, "no-restricted-syntax": { "count": 1 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "packages/grafana-ui/src/components/Forms/Legacy/Select/NoOptionsMessage.tsx": { @@ -660,38 +643,18 @@ } }, "packages/grafana-ui/src/components/Forms/Legacy/Select/Select.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, "no-restricted-syntax": { "count": 6 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 3 } }, "packages/grafana-ui/src/components/Forms/Legacy/Select/SelectOption.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - }, "no-restricted-syntax": { "count": 4 } }, - "packages/grafana-ui/src/components/Forms/Legacy/Select/SelectOptionGroup.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 3 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.tsx": { "no-restricted-syntax": { "count": 3 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "packages/grafana-ui/src/components/Gauge/Gauge.tsx": { @@ -709,11 +672,6 @@ "count": 3 } }, - "packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -838,16 +796,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 @@ -933,21 +881,11 @@ "count": 1 } }, - "packages/grafana-ui/src/components/Tooltip/PopoverController.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Typeahead/Typeahead.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 2 } }, - "packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx": { "@grafana/no-aria-label-selectors": { "count": 1 diff --git a/eslint.config.js b/eslint.config.js index 2af812e1fe3..bd1be26465a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -132,6 +132,7 @@ module.exports = [ reportUnusedDisableDirectives: false, }, files: ['**/*.{ts,tsx,js}'], + ignores: ['packages/grafana-ui/src/components/Forms/Legacy/**'], plugins: { '@emotion': emotionPlugin, lodash: lodashPlugin, @@ -254,6 +255,9 @@ module.exports = [ name: 'grafana/jsx-a11y-overrides', files: ['**/*.tsx'], ignores: ['**/*.{spec,test}.tsx'], + plugins: { + 'jsx-a11y': jsxA11yPlugin, + }, rules: { ...jsxA11yPlugin.configs.recommended.rules, 'jsx-a11y/no-autofocus': [ @@ -278,6 +282,9 @@ module.exports = [ name: 'grafana/packages', files: ['packages/**/*.{ts,tsx}'], ignores: [], + plugins: { + import: importPlugin, + }, rules: { 'import/no-extraneous-dependencies': ['error', { includeInternal: true }], 'no-restricted-imports': [ @@ -378,6 +385,7 @@ module.exports = [ plugins: { 'testing-library': testingLibraryPlugin, 'jest-dom': jestDomPlugin, + jest: jestPlugin, }, files: [ 'public/app/features/alerting/**/__tests__/**/*.[jt]s?(x)', @@ -508,10 +516,12 @@ module.exports = [ // Old betterer rules config: { files: ['**/*.{js,jsx,ts,tsx}'], - ignores: + ignores: [ // FIXME: Remove once all enterprise issues are fixed - // we don't have a suppressions file/approach for enterprise code yet - enterpriseIgnores, + ...enterpriseIgnores, + 'packages/grafana-ui/src/components/Forms/Legacy/**', + ], rules: { '@typescript-eslint/no-explicit-any': 'error', '@grafana/no-aria-label-selectors': 'error', diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index f44b3f00fa4..5611dc95b35 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -1,11 +1,16 @@ import { css } from '@emotion/css'; -import { Component, createRef } from 'react'; -import * as React from 'react'; +import { + type ComponentType, + createElement, + type PropsWithChildren, + type ReactNode, + type RefObject, + useRef, +} from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { withTheme2 } from '../../themes/ThemeContext'; -import { stylesFactory } from '../../themes/stylesFactory'; +import { useTheme2 } from '../../themes/ThemeContext'; import { closePopover } from '../../utils/closePopover'; import { Popover } from '../Tooltip/Popover'; import { PopoverController } from '../Tooltip/PopoverController'; @@ -21,76 +26,81 @@ import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; * component as a custom trigger you will need to forward the reference to first HTMLElement child. */ type ColorPickerTriggerRenderer = (props: { - // This should be a React.RefObject but due to how object refs are defined you cannot downcast from that - // to a specific type like React.RefObject even though it would be fine in runtime. - ref: React.RefObject; + // This should be a RefObject but due to how object refs are defined you cannot downcast from that + // to a specific type like RefObject even though it would be fine in runtime. + ref: RefObject; showColorPicker: () => void; hideColorPicker: () => void; -}) => React.ReactNode; +}) => ReactNode; export const colorPickerFactory = ( - popover: React.ComponentType>, + popover: ComponentType>, displayName = 'ColorPicker' ) => { - return class ColorPicker extends Component { - static displayName = displayName; - pickerTriggerRef = createRef(); + const ColorPickerComponent = (props: T & { children?: ColorPickerTriggerRenderer }) => { + const { children, onChange, color, id } = props; + const theme = useTheme2(); + const pickerTriggerRef = useRef(null); + const styles = getStyles(theme); - render() { - const { theme, children, onChange, color, id } = this.props; - const styles = getStyles(theme); - const popoverElement = React.createElement(popover, { - ...{ ...this.props, children: null }, + const popoverElement = createElement( + popover, + { + ...props, onChange, - }); - return ( - - {(showPopper, hidePopper, popperProps) => { - return ( - <> - {this.pickerTriggerRef.current && ( - closePopover(event, hidePopper)} - /> - )} + }, + null + ); - {children ? ( - children({ - ref: this.pickerTriggerRef, - showColorPicker: showPopper, - hideColorPicker: hidePopper, - }) - ) : ( - - )} - - ); - }} - - ); - } + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {pickerTriggerRef.current && ( + closePopover(event, hidePopper)} + /> + )} + + {children ? ( + children({ + ref: pickerTriggerRef, + showColorPicker: showPopper, + hideColorPicker: hidePopper, + }) + ) : ( + + )} + + ); + }} + + ); }; + + return ColorPickerComponent; }; /** * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-colorpicker--docs */ -export const ColorPicker = withTheme2(colorPickerFactory(ColorPickerPopover, 'ColorPicker')); -export const SeriesColorPicker = withTheme2(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker')); +export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker'); +export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'); -const getStyles = stylesFactory((theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2) => { return { colorPicker: css({ position: 'absolute', @@ -102,4 +112,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { overflow: 'auto', }), }; -}); +}; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx index 5dd4e2acb25..15a4c6f5c13 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx @@ -72,7 +72,6 @@ export const ColorPickerInput = forwardRef void; -export interface ColorPickerProps extends Themeable2 { +export interface ColorPickerProps { color: string; onChange: ColorPickerChangeHandler; enableNamedColors?: boolean; @@ -29,69 +26,56 @@ export interface Props extends ColorPickerProps, PopoverContentProps { customPickers?: T; } -type PickerType = 'palette' | 'spectrum'; - export interface CustomPickersDescriptor { [key: string]: { - tabComponent: React.ComponentType; + tabComponent: ComponentType; name: string; }; } -interface State { - activePicker: PickerType | keyof T; -} +type PickerType = 'palette' | 'spectrum'; -class UnThemedColorPickerPopover extends Component, State> { - constructor(props: Props) { - super(props); - this.state = { - activePicker: 'palette', - }; - } +export const ColorPickerPopover = (props: Props) => { + const { color, onChange, enableNamedColors, customPickers } = props; + const theme = useTheme2(); + const [activePicker, setActivePicker] = useState('palette'); - handleChange = (color: string) => { - const { onChange, enableNamedColors, theme } = this.props; + const styles = getStyles(theme); + + const handleChange = (color: string) => { if (enableNamedColors) { return onChange(color); } onChange(colorManipulator.asHexString(theme.visualization.getColorByName(color))); }; - onTabChange = (tab: PickerType | keyof T) => { - return () => this.setState({ activePicker: tab }); + const onTabChange = (tab: PickerType | keyof T) => { + return () => setActivePicker(tab); }; - renderPicker = () => { - const { activePicker } = this.state; - const { color } = this.props; - - switch (activePicker) { - case 'spectrum': - return ; - case 'palette': - return ; - default: - return this.renderCustomPicker(activePicker); - } - }; - - renderCustomPicker = (tabKey: keyof T) => { - const { customPickers, color, theme } = this.props; + const renderCustomPicker = (tabKey: keyof T) => { if (!customPickers) { return null; } - return React.createElement(customPickers[tabKey].tabComponent, { + return createElement(customPickers[tabKey].tabComponent, { color, - theme, - onChange: this.handleChange, + onChange: handleChange, }); }; - renderCustomPickerTabs = () => { - const { customPickers } = this.props; + const renderPicker = () => { + switch (activePicker) { + case 'spectrum': + return ; + case 'palette': + return ; + default: + return renderCustomPicker(activePicker); + } + }; + const renderCustomPickerTabs = () => { if (!customPickers) { return null; } @@ -99,49 +83,39 @@ class UnThemedColorPickerPopover extends Comp return ( <> {Object.keys(customPickers).map((key) => { - return ; + return ; })} ); }; - render() { - const { theme } = this.props; - const { activePicker } = this.state; + return ( + + {/* + tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope + see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 + */} +
+ + + + {renderCustomPickerTabs()} + +
{renderPicker()}
+
+
+ ); +}; - const styles = getStyles(theme); - - return ( - - {/* - tabIndex=-1 is needed here to support highlighting text within the picker when using FocusScope - see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 - */} -
- - - - {this.renderCustomPickerTabs()} - -
{this.renderPicker()}
-
-
- ); - } -} - -export const ColorPickerPopover = withTheme2(UnThemedColorPickerPopover); -ColorPickerPopover.displayName = 'ColorPickerPopover'; - -const getStyles = stylesFactory((theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2) => { return { colorPickerPopover: css({ borderRadius: theme.shape.radius.default, @@ -165,4 +139,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { borderRadius: `${theme.shape.radius.default} ${theme.shape.radius.default} 0 0`, }), }; -}); +}; diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 6e38fa3148e..83053a9bd11 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,6 +1,5 @@ import { t } from '@grafana/i18n'; -import { withTheme2 } from '../../themes/ThemeContext'; import { InlineField } from '../Forms/InlineField'; import { InlineSwitch } from '../Switch/Switch'; import { PopoverContentProps } from '../Tooltip/types'; @@ -36,4 +35,4 @@ export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) = }; // This component is to enable SeriesColorPickerPopover usage via series-color-picker-popover directive -export const SeriesColorPickerPopoverWithTheme = withTheme2(SeriesColorPickerPopover); +export const SeriesColorPickerPopoverWithTheme = SeriesColorPickerPopover; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx index d8c06371ab9..d03621aa248 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx @@ -38,7 +38,7 @@ const SpectrumPalette = ({ color, onChange }: SpectrumPaletteProps) => { return (
- +
); }; diff --git a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx index d7d94b186c2..20a66a01564 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { uniqueId } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useState } from 'react'; import { DataSourceSettings } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; @@ -27,10 +27,6 @@ export interface Props { onChange: (config: DataSourceSettings) => void; } -export interface State { - headers: CustomHeaders; -} - interface CustomHeaderRowProps { header: CustomHeader; onReset: (id: string) => void; @@ -98,150 +94,129 @@ const CustomHeaderRow = ({ header, onBlur, onChange, onRemove, onReset }: Custom CustomHeaderRow.displayName = 'CustomHeaderRow'; -export class CustomHeadersSettings extends PureComponent { - state: State = { - headers: [], - }; - - constructor(props: Props) { - super(props); - const { jsonData, secureJsonData, secureJsonFields } = this.props.dataSourceConfig; - this.state = { - headers: Object.keys(jsonData) - .sort() - .filter((key) => key.startsWith('httpHeaderName')) - .map((key, index) => { - return { - id: uniqueId(), - name: jsonData[key], - value: secureJsonData !== undefined ? secureJsonData[key] : '', - configured: (secureJsonFields && secureJsonFields[`httpHeaderValue${index + 1}`]) || false, - }; - }), - }; - } - - updateSettings = () => { - const { headers } = this.state; +export const CustomHeadersSettings = memo(({ dataSourceConfig, onChange }) => { + const [headers, setHeaders] = useState(() => { + const { jsonData, secureJsonData, secureJsonFields } = dataSourceConfig; + return Object.keys(jsonData) + .sort() + .filter((key) => key.startsWith('httpHeaderName')) + .map((key, index) => { + return { + id: uniqueId(), + name: jsonData[key], + value: secureJsonData !== undefined ? secureJsonData[key] : '', + configured: (secureJsonFields && secureJsonFields[`httpHeaderValue${index + 1}`]) || false, + }; + }); + }); + const updateSettings = (newHeaders: CustomHeaders) => { // we remove every httpHeaderName* field const newJsonData = Object.fromEntries( - Object.entries(this.props.dataSourceConfig.jsonData).filter(([key, val]) => !key.startsWith('httpHeaderName')) + Object.entries(dataSourceConfig.jsonData).filter(([key, val]) => !key.startsWith('httpHeaderName')) ); // we remove every httpHeaderValue* field const newSecureJsonData = Object.fromEntries( - Object.entries(this.props.dataSourceConfig.secureJsonData || {}).filter( - ([key, val]) => !key.startsWith('httpHeaderValue') - ) + Object.entries(dataSourceConfig.secureJsonData || {}).filter(([key, val]) => !key.startsWith('httpHeaderValue')) ); // then we add the current httpHeader-fields - for (const [index, header] of headers.entries()) { + for (const [index, header] of newHeaders.entries()) { newJsonData[`httpHeaderName${index + 1}`] = header.name; if (!header.configured) { newSecureJsonData[`httpHeaderValue${index + 1}`] = header.value; } } - this.props.onChange({ - ...this.props.dataSourceConfig, + onChange({ + ...dataSourceConfig, jsonData: newJsonData, secureJsonData: newSecureJsonData, }); }; - onHeaderAdd = () => { - this.setState((prevState) => { - return { headers: [...prevState.headers, { id: uniqueId(), name: '', value: '', configured: false }] }; - }); + const onHeaderAdd = () => { + setHeaders((prevHeaders) => [...prevHeaders, { id: uniqueId(), name: '', value: '', configured: false }]); }; - onHeaderChange = (headerIndex: number, value: CustomHeader) => { - this.setState(({ headers }) => { - return { - headers: headers.map((item, index) => { - if (headerIndex !== index) { - return item; - } - return { ...value }; - }), - }; - }); - }; - - onHeaderReset = (headerId: string) => { - this.setState(({ headers }) => { - return { - headers: headers.map((h, i) => { - if (h.id !== headerId) { - return h; - } - return { - ...h, - value: '', - configured: false, - }; - }), - }; - }); - }; - - onHeaderRemove = (headerId: string) => { - this.setState( - ({ headers }) => ({ - headers: headers.filter((h) => h.id !== headerId), - }), - this.updateSettings + const onHeaderChange = (headerIndex: number, value: CustomHeader) => { + setHeaders((prevHeaders) => + prevHeaders.map((item, index) => { + if (headerIndex !== index) { + return item; + } + return { ...value }; + }) ); }; - render() { - const { headers } = this.state; - const { dataSourceConfig } = this.props; + const onHeaderReset = (headerId: string) => { + setHeaders((prevHeaders) => + prevHeaders.map((h) => { + if (h.id !== headerId) { + return h; + } + return { + ...h, + value: '', + configured: false, + }; + }) + ); + }; - return ( - + const onHeaderRemove = (headerId: string) => { + setHeaders((prevHeaders) => { + const newHeaders = prevHeaders.filter((h) => h.id !== headerId); + updateSettings(newHeaders); + return newHeaders; + }); + }; + + return ( + + + +
+ Custom HTTP Headers +
+
+
+
+ {headers.map((header, i) => ( + { + onHeaderChange(i, h); + }} + onBlur={() => updateSettings(headers)} + onRemove={onHeaderRemove} + onReset={onHeaderReset} + /> + ))} +
+ {!dataSourceConfig.readOnly && ( -
- Custom HTTP Headers -
+
-
- {headers.map((header, i) => ( - { - this.onHeaderChange(i, h); - }} - onBlur={this.updateSettings} - onRemove={this.onHeaderRemove} - onReset={this.onHeaderReset} - /> - ))} -
- {!dataSourceConfig.readOnly && ( - - - - - - )} -
- ); - } -} + )} +
+ ); +}); + +CustomHeadersSettings.displayName = 'CustomHeadersSettings'; export default CustomHeadersSettings; diff --git a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx index 089fd514418..c63634be362 100644 --- a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx +++ b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx @@ -1,4 +1,4 @@ -import { PureComponent, createRef } from 'react'; +import { memo, useRef, useEffect } from 'react'; import { JsonExplorer, JsonExplorerConfig } from './json_explorer/json_explorer'; // We have made some monkey-patching of json-formatter-js so we can't switch right now @@ -10,45 +10,32 @@ interface Props { onDidRender?: (formattedJson: {}) => void; } -export class JSONFormatter extends PureComponent { - private wrapperRef = createRef(); +export const JSONFormatter = memo( + ({ className, json, config = { animateOpen: true }, open = 3, onDidRender }) => { + const wrapperRef = useRef(null); - static defaultProps = { - open: 3, - config: { - animateOpen: true, - }, - }; + useEffect(() => { + const wrapperEl = wrapperRef.current; + if (!wrapperEl) { + return; + } - componentDidMount() { - this.renderJson(); + const formatter = new JsonExplorer(json, open, config); + const hasChildren = wrapperEl.hasChildNodes(); + + if (hasChildren && wrapperEl.lastChild) { + wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); + } else { + wrapperEl.appendChild(formatter.render()); + } + + if (onDidRender) { + onDidRender(formatter.json); + } + }, [json, config, open, onDidRender]); + + return
; } +); - componentDidUpdate() { - this.renderJson(); - } - - renderJson = () => { - const { json, config, open, onDidRender } = this.props; - const wrapperEl = this.wrapperRef.current; - const formatter = new JsonExplorer(json, open, config); - // @ts-ignore - const hasChildren: boolean = wrapperEl.hasChildNodes(); - if (hasChildren) { - // @ts-ignore - wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); - } else { - // @ts-ignore - wrapperEl.appendChild(formatter.render()); - } - - if (onDidRender) { - onDidRender(formatter.json); - } - }; - - render() { - const { className } = this.props; - return
; - } -} +JSONFormatter.displayName = 'JSONFormatter'; diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index 49c3f4b8966..db0f53084d8 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -1,45 +1,33 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; -import { PureComponent } from 'react'; +import { memo, useState } from 'react'; import { Field } from '../Forms/Field'; import { Props, StatsPicker } from './StatsPicker'; -interface State { - stats: string[]; -} +const WrapperWithState = memo(({ placeholder, allowMultiple, menuPlacement, width }) => { + const [stats, setStats] = useState([]); -class WrapperWithState extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { - stats: [], - }; - } + return ( + + { + action('Picked:')(newStats); + setStats(newStats); + }} + menuPlacement={menuPlacement} + width={width} + /> + + ); +}); - render() { - const { placeholder, allowMultiple, menuPlacement, width } = this.props; - const { stats } = this.state; - - return ( - - { - action('Picked:')(stats); - this.setState({ stats }); - }} - menuPlacement={menuPlacement} - width={width} - /> - - ); - } -} +WrapperWithState.displayName = 'WrapperWithState'; const meta: Meta = { title: 'Pickers/StatsPicker', diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index 009ae565dfe..0bf185e1354 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -1,5 +1,5 @@ import { difference } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useEffect } from 'react'; import { fieldReducers, SelectableValue, FieldReducerInfo } from '@grafana/data'; @@ -18,54 +18,47 @@ export interface Props { filterOptions?: (ext: FieldReducerInfo) => boolean; } -export class StatsPicker extends PureComponent { - static defaultProps: Partial = { - allowMultiple: false, - }; +export const StatsPicker = memo( + ({ + placeholder, + onChange, + stats, + allowMultiple = false, + defaultStat, + className, + width, + menuPlacement, + inputId, + filterOptions, + }) => { + useEffect(() => { + const current = fieldReducers.list(stats); + if (current.length !== stats.length) { + const found = current.map((v) => v.id); + const notFound = difference(stats, found); + console.warn('Unknown stats', notFound, stats); + onChange(current.map((stat) => stat.id)); + } - componentDidMount() { - this.checkInput(); - } + // Make sure there is only one + if (!allowMultiple && stats.length > 1) { + console.warn('Removing extra stat', stats); + onChange([stats[0]]); + } - componentDidUpdate(prevProps: Props) { - this.checkInput(); - } + // Set the reducer from callback + if (defaultStat && stats.length < 1) { + onChange([defaultStat]); + } + }, [stats, allowMultiple, defaultStat, onChange]); - checkInput = () => { - const { stats, allowMultiple, defaultStat, onChange } = this.props; - - const current = fieldReducers.list(stats); - if (current.length !== stats.length) { - const found = current.map((v) => v.id); - const notFound = difference(stats, found); - console.warn('Unknown stats', notFound, stats); - onChange(current.map((stat) => stat.id)); - } - - // Make sure there is only one - if (!allowMultiple && stats.length > 1) { - console.warn('Removing extra stat', stats); - onChange([stats[0]]); - } - - // Set the reducer from callback - if (defaultStat && stats.length < 1) { - onChange([defaultStat]); - } - }; - - onSelectionChange = (item: SelectableValue) => { - const { onChange } = this.props; - if (Array.isArray(item)) { - onChange(item.map((v) => v.value)); - } else { - onChange(item && item.value ? [item.value] : []); - } - }; - - render() { - const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId, filterOptions } = - this.props; + const onSelectionChange = (item: SelectableValue) => { + if (Array.isArray(item)) { + onChange(item.map((v) => v.value)); + } else { + onChange(item && item.value ? [item.value] : []); + } + }; const select = fieldReducers.selectOptions(stats, filterOptions); return ( @@ -78,10 +71,12 @@ export class StatsPicker extends PureComponent { isSearchable={true} options={select.options} placeholder={placeholder} - onChange={this.onSelectionChange} + onChange={onSelectionChange} menuPlacement={menuPlacement} inputId={inputId} /> ); } -} +); + +StatsPicker.displayName = 'StatsPicker'; diff --git a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx index 1e792aa95d8..37e3ae49473 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx @@ -1,5 +1,5 @@ import { Placement } from '@popperjs/core'; -import { Component, type JSX } from 'react'; +import { useState, useRef, useCallback, type JSX } from 'react'; import { PopoverContent } from './types'; @@ -21,37 +21,28 @@ interface Props { hideAfter?: number; } -interface State { - show: boolean; -} +const PopoverController = ({ placement = 'auto', content, children, hideAfter }: Props) => { + const [show, setShow] = useState(false); + const hideTimeoutRef = useRef | null>(null); -class PopoverController extends Component { - private hideTimeout: ReturnType | null = null; - state = { show: false }; - - showPopper = () => { - if (this.hideTimeout) { - clearTimeout(this.hideTimeout); + const showPopper = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); } - this.setState({ show: true }); - }; + setShow(true); + }, []); - hidePopper = () => { - this.hideTimeout = setTimeout(() => { - this.setState({ show: false }); - }, this.props.hideAfter); - }; + const hidePopper = useCallback(() => { + hideTimeoutRef.current = setTimeout(() => { + setShow(false); + }, hideAfter); + }, [hideAfter]); - render() { - const { children, content, placement = 'auto' } = this.props; - const { show } = this.state; - - return children(this.showPopper, this.hidePopper, { - show, - placement, - content, - }); - } -} + return children(showPopper, hidePopper, { + show, + placement, + content, + }); +}; export { PopoverController }; diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index ac53b424bdb..90b9263f55d 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { getValueFormats, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -19,58 +19,52 @@ function formatCreateLabel(input: string) { /** * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-unitpicker--docs */ -export class UnitPicker extends PureComponent { - onChange = (value: SelectableValue) => { - this.props.onChange(value.value); - }; +export const UnitPicker = memo(({ onChange, value, width, id }) => { + // Set the current selection + let current: SelectableValue | undefined = undefined; - render() { - const { value, width, id } = this.props; + // All units + const unitGroups = getValueFormats(); - // Set the current selection - let current: SelectableValue | undefined = undefined; - - // All units - const unitGroups = getValueFormats(); - - // Need to transform the data structure to work well with Select - const groupOptions: CascaderOption[] = unitGroups.map((group) => { - const options = group.submenu.map((unit) => { - const sel = { - label: unit.text, - value: unit.value, - }; - if (unit.value === value) { - current = sel; - } - return sel; - }); - - return { - label: group.text, - value: group.text, - items: options, + // Need to transform the data structure to work well with Select + const groupOptions: CascaderOption[] = unitGroups.map((group) => { + const options = group.submenu.map((unit) => { + const sel = { + label: unit.text, + value: unit.value, }; + if (unit.value === value) { + current = sel; + } + return sel; }); - // Show the custom unit - if (value && !current) { - current = { value, label: value }; - } + return { + label: group.text, + value: group.text, + items: options, + }; + }); - return ( - - ); + // Show the custom unit + if (value && !current) { + current = { value, label: value }; } -} + + return ( + + ); +}); + +UnitPicker.displayName = 'UnitPicker'; diff --git a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx index fc6ab425ef9..101bd69ec97 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { VariableHide } from '@grafana/data'; @@ -65,15 +65,13 @@ describe('DashboardControlsMenu', () => { }), ]; - act(() => { - render(); - }); + render(); // Should have rendered a dropdown expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('textVar2')).toBeInTheDocument(); expect(await screen.findByText('queryVar')).toBeInTheDocument(); @@ -104,7 +102,7 @@ describe('DashboardControlsMenu', () => { expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('customVar')).toBeInTheDocument(); expect(screen.queryByText('textVar2')).not.toBeInTheDocument(); From 12c6d7e83fb8ba89d74edc1d5e0a5fdd59fdfde4 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Fri, 28 Nov 2025 13:13:35 +0100 Subject: [PATCH 168/423] fix(unified): in-proc SQLite data migration (#114537) * feat: unified storage migrations integration tests * chore: add comment and adjust db path name * chore: refactor test cases into interface * fix: unified SQLite migration with SQLStore migrator * revert changes to newResourceDBProvider --- .../unified/migrations/resource_migration.go | 16 ++ pkg/storage/unified/migrations/service.go | 12 +- pkg/storage/unified/migrations/validator.go | 99 +++++-- pkg/storage/unified/resource/transaction.go | 25 ++ pkg/storage/unified/sql/bulk.go | 250 ++++++++++-------- pkg/storage/unified/sql/db/dbimpl/db.go | 5 + pkg/util/xorm/session.go | 11 +- 7 files changed, 286 insertions(+), 132 deletions(-) create mode 100644 pkg/storage/unified/resource/transaction.go diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index 281ee1c85e3..5bbed9c5722 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -72,6 +73,17 @@ func (m *ResourceMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) erro m.log.Info("Starting migration for all organizations", "org_count", len(orgs), "resources", m.resources) + if mg.Dialect.DriverName() == migrator.SQLite { + // reuse transaction in SQLite to avoid "database is locked" errors + tx, err := sess.Tx() + if err != nil { + m.log.Error("Failed to get transaction from session", "error", err) + return fmt.Errorf("failed to get transaction: %w", err) + } + ctx = resource.ContextWithTransaction(ctx, tx.Tx) + m.log.Info("Stored migrator transaction in context for bulk operations (SQLite compatibility)") + } + for _, org := range orgs { if err := m.migrateOrg(ctx, sess, org); err != nil { return err @@ -107,6 +119,10 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, m.log.Error("Migration failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) } + if response.Error != nil { + m.log.Error("Migration reported error", "org_id", org.ID, "error", response.Error.String(), "duration", time.Since(startTime)) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, fmt.Errorf("migration error: %s", response.Error.Message)) + } // Validate the migration results if err := m.validateMigration(migrationCtx, sess, response); err != nil { diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 1a78f837066..23b8c2706ae 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -85,8 +85,13 @@ func RegisterMigrations( // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") + migrationLocking := sec.Key("migration_locking").MustBool(true) + if mg.Dialect.DriverName() == sqlstoremigrator.SQLite { + // disable migration locking for SQLite to avoid "database is locked" errors in the bulk operations + migrationLocking = false + } if err := mg.RunMigrations(ctx, - sec.Key("migration_locking").MustBool(true), + migrationLocking, sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { return fmt.Errorf("unified storage data migration failed: %w", err) } @@ -98,12 +103,14 @@ func RegisterMigrations( func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { folders := schema.GroupResource{Group: "folder.grafana.app", Resource: "folders"} dashboards := schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"} + driverName := mg.Dialect.DriverName() folderCountValidator := NewCountValidator( client, folders, "dashboard", "org_id = ? and is_folder = true", + driverName, ) dashboardCountValidator := NewCountValidator( @@ -111,9 +118,10 @@ func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator dashboards, "dashboard", "org_id = ? and is_folder = false", + driverName, ) - folderTreeValidator := NewFolderTreeValidator(client, folders) + folderTreeValidator := NewFolderTreeValidator(client, folders, driverName) dashboardsAndFolders := NewResourceMigration( migrator, diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go index 7a973976402..0bdc67cc205 100644 --- a/pkg/storage/unified/migrations/validator.go +++ b/pkg/storage/unified/migrations/validator.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -57,6 +58,7 @@ type CountValidator struct { resource schema.GroupResource table string whereClause string + driverName string } func NewCountValidator( @@ -64,6 +66,7 @@ func NewCountValidator( resource schema.GroupResource, table string, whereClause string, + driverName string, ) Validator { return &CountValidator{ name: "CountValidator", @@ -71,6 +74,7 @@ func NewCountValidator( resource: resource, table: table, whereClause: whereClause, + driverName: driverName, } } @@ -120,22 +124,32 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo return fmt.Errorf("failed to count %s: %w", v.table, err) } - // Get unified storage count using GetStats API - statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ - Namespace: summary.Namespace, - Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, - }) - if err != nil { - return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", - summary.Group, summary.Resource, summary.Namespace, err) - } - - // Find the count for this specific resource type var unifiedCount int64 - for _, stat := range statsResp.Stats { - if stat.Group == summary.Group && stat.Resource == summary.Resource { - unifiedCount = stat.Count - break + if v.driverName == migrator.SQLite { + unifiedCount, err = sess.Table("resource"). + Where("namespace = ? AND `group` = ? AND resource = ?", + summary.Namespace, summary.Group, summary.Resource). + Count() + if err != nil { + return fmt.Errorf("failed to count resource table for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + } else { + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + // Find the count for this specific resource type + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break + } } } @@ -162,19 +176,22 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo } type FolderTreeValidator struct { - name string - client resourcepb.ResourceIndexClient - resource schema.GroupResource + name string + client resourcepb.ResourceIndexClient + resource schema.GroupResource + driverName string } func NewFolderTreeValidator( client resourcepb.ResourceIndexClient, resource schema.GroupResource, + driverName string, ) Validator { return &FolderTreeValidator{ - name: "FolderTreeValidator", - client: client, - resource: resource, + name: "FolderTreeValidator", + client: client, + resource: resource, + driverName: driverName, } } @@ -185,6 +202,12 @@ type legacyFolder struct { Title string `xorm:"title"` } +type unifiedFolder struct { + GUID string `xorm:"guid"` + Name string `xorm:"name"` + Folder string `xorm:"folder"` +} + func (v *FolderTreeValidator) Name() string { return v.name } @@ -218,7 +241,12 @@ func (v *FolderTreeValidator) Validate(ctx context.Context, sess *xorm.Session, } // Build unified storage folder parent map - unifiedParentMap, err := v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + var unifiedParentMap map[string]string + if v.driverName == migrator.SQLite { + unifiedParentMap, err = v.buildUnifiedFolderParentMapSQLite(sess, summary.Namespace, log) + } else { + unifiedParentMap, err = v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + } if err != nil { return fmt.Errorf("failed to build unified folder parent map: %w", err) } @@ -348,3 +376,30 @@ func (v *FolderTreeValidator) buildUnifiedFolderParentMap(ctx context.Context, n return parentMap, nil } + +func (v *FolderTreeValidator) buildUnifiedFolderParentMapSQLite(sess *xorm.Session, namespace string, log log.Logger) (map[string]string, error) { + var folders []unifiedFolder + err := sess.Table("resource"). + Cols("guid", "name", "folder"). + Where("namespace = ? AND resource = ?", namespace, "folder"). + Find(&folders) + if err != nil { + return nil, fmt.Errorf("failed to query unified folders: %w", err) + } + + parentMap := make(map[string]string) + for _, folder := range folders { + parentMap[folder.Name] = folder.Folder + } + + if len(parentMap) == 0 { + log.Debug("No unified folders found for namespace", "namespace", namespace) + return make(map[string]string), nil + } + + log.Debug("Built unified folder parent map", + "folder_count", len(parentMap), + "namespace", namespace) + + return parentMap, nil +} diff --git a/pkg/storage/unified/resource/transaction.go b/pkg/storage/unified/resource/transaction.go new file mode 100644 index 00000000000..7d47f02af71 --- /dev/null +++ b/pkg/storage/unified/resource/transaction.go @@ -0,0 +1,25 @@ +package resource + +import ( + "context" + "database/sql" +) + +type transactionContextKey struct{} + +// ContextWithTransaction returns a new context with the transaction stored directly. +// This is used for SQLite migrations where the transaction needs to be shared +// between the migration code and unified storage operations within the same process. +func ContextWithTransaction(ctx context.Context, tx *sql.Tx) context.Context { + return context.WithValue(ctx, transactionContextKey{}, tx) +} + +// TransactionFromContext retrieves the transaction from context +func TransactionFromContext(ctx context.Context) *sql.Tx { + if v := ctx.Value(transactionContextKey{}); v != nil { + if tx, ok := v.(*sql.Tx); ok { + return tx + } + } + return nil +} diff --git a/pkg/storage/unified/sql/bulk.go b/pkg/storage/unified/sql/bulk.go index 7d4d7165d50..6580975a764 100644 --- a/pkg/storage/unified/sql/bulk.go +++ b/pkg/storage/unified/sql/bulk.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/google/uuid" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -111,6 +113,19 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings } defer b.bulkLock.Finish(setting.Collection) + // If provided, reuse the inproc transaction for SQLite + if clientCtx := inprocgrpc.ClientContext(ctx); clientCtx != nil && b.dialect.DialectName() == "sqlite" { + if externalTx := resource.TransactionFromContext(clientCtx); externalTx != nil { + b.log.Info("Using SQLite transaction from client context") + rsp := &resourcepb.BulkResponse{} + err := b.processBulkWithTx(ctx, dbimpl.NewTx(externalTx), setting, iter, rsp) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp + } + } + // We may want to first write parquet, then read parquet if b.dialect.DialectName() == "sqlite" { file, err := os.CreateTemp("", "grafana-bulk-export-*.parquet") @@ -151,109 +166,134 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings, iter resource.BulkRequestIterator) *resourcepb.BulkResponse { rsp := &resourcepb.BulkResponse{} err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { - rollbackWithError := func(err error) error { - txerr := tx.Rollback() - if txerr != nil { - b.log.Warn("rollback", "error", txerr) - } else { - b.log.Info("rollback") + return b.processBulkWithTx(ctx, tx, setting, iter, rsp) + }) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp +} + +// processBulkWithTx performs the bulk operation using the provided transaction. +// This is used both when creating our own transaction and when reusing an external one. +func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resource.BulkSettings, iter resource.BulkRequestIterator, rsp *resourcepb.BulkResponse) error { + rollbackWithError := func(err error) error { + txerr := tx.Rollback() + if txerr != nil { + b.log.Warn("rollback", "error", txerr) + } else { + b.log.Info("rollback") + } + return err + } + bulk := &bulkWroker{ + ctx: ctx, + tx: tx, + dialect: b.dialect, + logger: logging.FromContext(ctx), + } + + // Calculate the RV based on incoming request timestamps + rv := newBulkRV() + + summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) + + // First clear everything in the transaction + if setting.RebuildCollection { + for _, key := range setting.Collection { + summary, err := bulk.deleteCollection(key) + if err != nil { + return rollbackWithError(err) } + summaries[resource.NSGR(key)] = summary + rsp.Summary = append(rsp.Summary, summary) + } + } else { + for _, key := range setting.Collection { + summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + } + } + } + + obj := &unstructured.Unstructured{} + + // Write each event into the history + for iter.Next() { + if iter.RollbackRequested() { + return rollbackWithError(nil) + } + req := iter.Request() + if req == nil { + return rollbackWithError(fmt.Errorf("missing request")) + } + rsp.Processed++ + + if req.Action == resourcepb.BulkRequest_UNKNOWN { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unknown action", + }) + continue + } + + err := obj.UnmarshalJSON(req.Value) + if err != nil { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unable to unmarshal json", + }) + continue + } + + // Write the event to history + if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ + SQLTemplate: sqltemplate.New(b.dialect), + WriteEvent: resource.WriteEvent{ + Key: req.Key, + Type: resourcepb.WatchEvent_Type(req.Action), + Value: req.Value, + PreviousRV: -1, // Used for WATCH, but we want to skip watch events + }, + Folder: req.Folder, + GUID: uuid.New().String(), + ResourceVersion: rv.next(obj), + }); err != nil { + return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) + } + } + + // Now update the resource table from history + for _, key := range setting.Collection { + k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) + summary := summaries[k] + if summary == nil { + return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) + } + + err := bulk.syncCollection(key, summary) + if err != nil { return err } - bulk := &bulkWroker{ - ctx: ctx, - tx: tx, - dialect: b.dialect, - logger: logging.FromContext(ctx), - } - // Calculate the RV based on incoming request timestamps - rv := newBulkRV() - - summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) - - // First clear everything in the transaction - if setting.RebuildCollection { - for _, key := range setting.Collection { - summary, err := bulk.deleteCollection(key) - if err != nil { - return rollbackWithError(err) + if b.dialect.DialectName() == "sqlite" { + nextRV, err := b.rvManager.lock(ctx, tx, key.Group, key.Resource) + if err != nil { + b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key)) + // Save the incremented RV + if err := b.rvManager.saveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { + b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key)) } - summaries[resource.NSGR(key)] = summary - rsp.Summary = append(rsp.Summary, summary) } } else { - for _, key := range setting.Collection { - summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - } - } - } - - obj := &unstructured.Unstructured{} - - // Write each event into the history - for iter.Next() { - if iter.RollbackRequested() { - return rollbackWithError(nil) - } - req := iter.Request() - if req == nil { - return rollbackWithError(fmt.Errorf("missing request")) - } - rsp.Processed++ - - if req.Action == resourcepb.BulkRequest_UNKNOWN { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unknown action", - }) - continue - } - - err := obj.UnmarshalJSON(req.Value) - if err != nil { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unable to unmarshal json", - }) - continue - } - - // Write the event to history - if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: resource.WriteEvent{ - Key: req.Key, - Type: resourcepb.WatchEvent_Type(req.Action), - Value: req.Value, - PreviousRV: -1, // Used for WATCH, but we want to skip watch events - }, - Folder: req.Folder, - GUID: uuid.New().String(), - ResourceVersion: rv.next(obj), - }); err != nil { - return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) - } - } - - // Now update the resource table from history - for _, key := range setting.Collection { - k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) - summary := summaries[k] - if summary == nil { - return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) - } - - err := bulk.syncCollection(key, summary) - if err != nil { - return err - } - // Make sure the collection RV is above our last written event _, err = b.rvManager.ExecWithRV(ctx, key, func(tx db.Tx) (string, error) { return "", nil @@ -261,19 +301,15 @@ func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings if err != nil { b.log.Warn("error increasing RV", "error", err) } - - // Update the last import time. This is important to trigger reindexing - // of the resource for a given namespace. - if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { - return rollbackWithError(err) - } } - return nil - }) - if err != nil { - rsp.Error = resource.AsErrorResult(err) + + // Update the last import time. This is important to trigger reindexing + // of the resource for a given namespace. + if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { + return rollbackWithError(err) + } } - return rsp + return nil } func (b *backend) updateLastImportTime(ctx context.Context, tx db.Tx, key *resourcepb.ResourceKey, now time.Time) error { diff --git a/pkg/storage/unified/sql/db/dbimpl/db.go b/pkg/storage/unified/sql/db/dbimpl/db.go index 262b03334aa..00099733bcf 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db.go +++ b/pkg/storage/unified/sql/db/dbimpl/db.go @@ -48,6 +48,11 @@ type sqlTx struct { *sql.Tx } +// NewTx wraps an existing *sql.Tx with sqlTx +func NewTx(tx *sql.Tx) db.Tx { + return sqlTx{tx} +} + func (tx sqlTx) QueryContext(ctx context.Context, query string, args ...any) (db.Rows, error) { // // codeql-suppress go/sql-query-built-from-user-controlled-sources "The query comes from a safe template source // and the parameters are passed as arguments." diff --git a/pkg/util/xorm/session.go b/pkg/util/xorm/session.go index b174b8e4ed9..03edf4b2983 100644 --- a/pkg/util/xorm/session.go +++ b/pkg/util/xorm/session.go @@ -7,6 +7,7 @@ package xorm import ( "context" "database/sql" + "errors" "fmt" "hash/crc32" "reflect" @@ -43,7 +44,7 @@ type Session struct { afterProcessors []executedProcessor prepareStmt bool - stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) + stmtCache map[uint32]*core.Stmt // key: hash.Hash32 of (queryStr, len(queryStr)) // !evalphobia! stored the last executed query on this session lastSQL string @@ -236,6 +237,14 @@ func (session *Session) DB() *core.DB { return session.db } +// Tx returns the underlying transaction +func (session *Session) Tx() (*core.Tx, error) { + if session.tx == nil { + return nil, errors.New("no open transaction") + } + return session.tx, nil +} + func cleanupProcessorsClosures(slices *[]func(any)) { if len(*slices) > 0 { *slices = make([]func(any), 0) From 43c3322cafe8e1e7ba8600df8394293b374bae52 Mon Sep 17 00:00:00 2001 From: Ana Ivanov <38096095+anaivanov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:28:19 +0100 Subject: [PATCH 169/423] Increase limitPerPlugin from 40 to 80 (#114573) * Increase limitPerPlugin from 40 to 60 * Increase limitPerPlugin from 60 to 80 --- .../app/features/commandPalette/actions/useExtensionActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/commandPalette/actions/useExtensionActions.ts b/public/app/features/commandPalette/actions/useExtensionActions.ts index 112bdc5ab0d..643a4647e2d 100644 --- a/public/app/features/commandPalette/actions/useExtensionActions.ts +++ b/public/app/features/commandPalette/actions/useExtensionActions.ts @@ -13,7 +13,7 @@ export default function useExtensionActions(): CommandPaletteAction[] { const { links } = usePluginLinks({ extensionPointId: PluginExtensionPoints.CommandPalette, context, - limitPerPlugin: 40, + limitPerPlugin: 80, }); return useMemo(() => { From 3c76e9ee7203f0cddb7ea0a56a8499f86f679824 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 28 Nov 2025 12:42:36 +0000 Subject: [PATCH 170/423] Teams: Refactor most functionality to use hooks (#113713) --- eslint-suppressions.json | 5 - .../src/clients/rtkq/legacy/endpoints.gen.ts | 14 +- .../src/handlers/all-handlers.ts | 6 +- .../handlers/api/access-control/handlers.ts | 11 ++ .../src/handlers/api/teams/handlers.ts | 21 +++ pkg/services/team/model.go | 10 +- pkg/services/team/teamapi/team.go | 4 + public/api-enterprise-spec.json | 11 +- public/api-merged.json | 17 +- public/app/api/clients/iam/v0alpha1/index.ts | 4 +- .../app/core/components/RolePicker/utils.ts | 12 +- public/app/core/reducers/root.test.ts | 64 -------- public/app/features/teams/CreateTeam.test.tsx | 2 +- public/app/features/teams/CreateTeam.tsx | 52 ++++-- .../app/features/teams/TeamGroupSync.test.tsx | 6 +- public/app/features/teams/TeamGroupSync.tsx | 7 +- public/app/features/teams/TeamList.test.tsx | 6 +- public/app/features/teams/TeamList.tsx | 117 ++++++------- public/app/features/teams/TeamPages.tsx | 19 +-- .../app/features/teams/TeamSettings.test.tsx | 28 ++-- public/app/features/teams/TeamSettings.tsx | 25 ++- public/app/features/teams/hooks.ts | 154 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 130 ++------------- .../app/features/teams/state/reducers.test.ts | 74 --------- public/app/features/teams/state/reducers.ts | 60 +------ .../features/teams/state/selectors.test.ts | 22 --- public/app/features/teams/state/selectors.ts | 10 +- public/app/types/accessControl.ts | 15 +- public/app/types/teams.ts | 53 +----- public/locales/en-US/grafana.json | 2 + public/openapi3.json | 19 ++- 31 files changed, 384 insertions(+), 596 deletions(-) create mode 100644 packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts delete mode 100644 public/app/core/reducers/root.test.ts create mode 100644 public/app/features/teams/hooks.ts delete mode 100644 public/app/features/teams/state/reducers.test.ts delete mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 71b855ae3b9..e3e822abdc4 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3183,11 +3183,6 @@ "count": 1 } }, - "public/app/features/teams/state/reducers.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/templating/fieldAccessorCache.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 45ff0d1e91e..e0f6b885e00 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -1644,7 +1644,12 @@ const injectedRtkApi = api invalidatesTags: ['teams'], }), getTeamById: build.query({ - query: (queryArg) => ({ url: `/teams/${queryArg.teamId}` }), + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}`, + params: { + accesscontrol: queryArg.accesscontrol, + }, + }), providesTags: ['teams'], }), updateTeam: build.mutation({ @@ -3474,6 +3479,7 @@ export type DeleteTeamByIdApiArg = { export type GetTeamByIdApiResponse = /** status 200 (empty) */ TeamDto; export type GetTeamByIdApiArg = { teamId: string; + accesscontrol?: boolean; }; export type UpdateTeamApiResponse = /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; @@ -6051,10 +6057,8 @@ export type SearchTeamGroupsQueryResult = { totalCount?: number; }; export type UpdateTeamCommand = { - Email?: string; - ExternalUID?: string; - ID?: number; - Name?: string; + email?: string; + name?: string; }; export type TeamMemberDto = { auth_module?: string; diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index 5a0f1cdc140..5fa473b55d5 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -1,5 +1,6 @@ import { HttpHandler } from 'msw'; +import accessControlHandlers from './api/access-control/handlers'; import dashboardsHandlers from './api/dashboards/handlers'; import folderHandlers from './api/folders/handlers'; import pluginsHandlers from './api/plugins/handlers'; @@ -14,11 +15,12 @@ import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/hand const allHandlers: HttpHandler[] = [ // Legacy handlers - ...teamsHandlers, + ...accessControlHandlers, ...dashboardsHandlers, ...folderHandlers, - ...searchHandlers, ...pluginsHandlers, + ...searchHandlers, + ...teamsHandlers, ...userHandlers, // App platform handlers diff --git a/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts new file mode 100644 index 00000000000..ce5f8f43d5e --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts @@ -0,0 +1,11 @@ +import { HttpResponse, http } from 'msw'; + +const searchTeamRolesHandler = () => + http.post('/api/access-control/teams/roles/search', async () => { + // TODO: Add better mock roles response as needed + return HttpResponse.json([]); + }); + +const handlers = [searchTeamRolesHandler()]; + +export default handlers; diff --git a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts index e9fb6c0a112..c8a42bf9bba 100644 --- a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts @@ -127,6 +127,26 @@ const createTeamHandler = () => return HttpResponse.json({ message: 'Team created', teamId: 10, uid: 'aethyfifmhwcgd' }, { status: 200 }); }); +const updateTeamHandler = () => + http.put<{ uid: string }, { name: string; email: string }>('/api/teams/:uid', async ({ params, request }) => { + const teamData = mockTeamsMap.get(params.uid); + const body = await request.json(); + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + const updatedTeam = { + ...teamData, + team: { + ...teamData.team, + name: body.name, + email: body.email, + }, + }; + mockTeamsMap.set(params.uid, updatedTeam); + + return HttpResponse.json({ message: 'Team updated' }); + }); + const handlers = [ teamsPreferencesHandler(), teamsGroupsHandler(), @@ -135,6 +155,7 @@ const handlers = [ getTeamHandler(), deleteTeamHandler(), createTeamHandler(), + updateTeamHandler(), ]; export default handlers; diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 4d01c0a3d0b..3696505139a 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -47,11 +47,11 @@ type CreateTeamCommand struct { } type UpdateTeamCommand struct { - ID int64 - Name string - Email string - ExternalUID string - OrgID int64 `json:"-"` + ID int64 `json:"-"` + Name string `json:"name"` + Email string `json:"email"` + ExternalUID string `json:"-"` + OrgID int64 `json:"-"` } type DeleteTeamCommand struct { diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 9fca7089d2b..230d4ab0f2d 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -312,6 +312,10 @@ type GetTeamByIDParams struct { // in:path // required:true TeamID string `json:"team_id"` + // in:query + // required:false + // default: false + AccessControl bool `json:"accesscontrol"` } // swagger:parameters deleteTeamByID diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index c976dcb9e7d..054cf609e40 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -8744,17 +8744,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/api-merged.json b/public/api-merged.json index 0d9c4129adc..12dd95170cf 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10117,6 +10117,12 @@ "name": "team_id", "in": "path", "required": true + }, + { + "type": "boolean", + "default": false, + "name": "accesscontrol", + "in": "query" } ], "responses": { @@ -23164,17 +23170,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/app/api/clients/iam/v0alpha1/index.ts b/public/app/api/clients/iam/v0alpha1/index.ts index 190f7ede868..f54e38f7e15 100644 --- a/public/app/api/clients/iam/v0alpha1/index.ts +++ b/public/app/api/clients/iam/v0alpha1/index.ts @@ -2,7 +2,5 @@ import { generatedAPI } from '@grafana/api-clients/rtkq/iam/v0alpha1'; export const iamAPIv0alpha1 = generatedAPI.enhanceEndpoints({}); -export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = iamAPIv0alpha1; - // eslint-disable-next-line no-barrel-files/no-barrel-files -export type { DisplayList } from '@grafana/api-clients/rtkq/iam/v0alpha1'; +export * from '@grafana/api-clients/rtkq/iam/v0alpha1'; diff --git a/public/app/core/components/RolePicker/utils.ts b/public/app/core/components/RolePicker/utils.ts index 413671f0090..f386634e307 100644 --- a/public/app/core/components/RolePicker/utils.ts +++ b/public/app/core/components/RolePicker/utils.ts @@ -1,3 +1,4 @@ +import { RoleDto } from 'app/api/clients/legacy'; import { Role } from 'app/types/accessControl'; export const isNotDelegatable = (role: Role) => { @@ -23,9 +24,10 @@ export const addDisplayNameForFixedRole = (role: Role) => { // Adds a display name for use when the list of roles is filtered // If either group or displayName are undefined, we fall back (see RoleMenuOption.tsx) -export const addFilteredDisplayName = (role: Role) => { - if (role.group && role.displayName) { - role.filteredDisplayName = role.group + ':' + role.displayName; - } - return role; +export const addFilteredDisplayName = (role: RoleDto): Role => { + const filteredDisplayName = role.group && role.displayName ? `${role.group}:${role.displayName}` : ''; + return { + ...role, + filteredDisplayName, + }; }; diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts deleted file mode 100644 index c9f5d5068bf..00000000000 --- a/public/app/core/reducers/root.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Team } from 'app/types/teams'; - -import { reducerTester } from '../../../test/core/redux/reducerTester'; -import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers'; -import { StoreState } from '../../types/store'; -import { cleanUpAction } from '../actions/cleanUp'; - -import { createRootReducer } from './root'; - -describe('rootReducer', () => { - const rootReducer = createRootReducer(); - - describe('when called with any action except cleanUpAction', () => { - it('then it should not clean state', () => { - const teams = [{ id: 1 } as Team]; - const state = { - teams: { ...initialTeamsState }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state) - .whenActionIsDispatched(teamsLoaded({ teams: teams, page: 1, noTeams: false, perPage: 30, totalCount: 1 })) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ - hasFetched: true, - noTeams: false, - perPage: 30, - totalPages: 1, - query: '', - page: 1, - teams, - }); - return true; - }); - }); - }); - - describe('when called with cleanUpAction', () => { - it('then it should clean state', () => { - const teams = [{ id: 1 }] as Team[]; - const state: StoreState = { - teams: { - hasFetched: true, - query: '', - page: 1, - noTeams: false, - totalPages: 1, - perPage: 30, - teams, - }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state, false, true) - .whenActionIsDispatched( - cleanUpAction({ cleanupAction: (storeState) => (storeState.teams = initialTeamsState) }) - ) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ ...initialTeamsState }); - return true; - }); - }); - }); -}); diff --git a/public/app/features/teams/CreateTeam.test.tsx b/public/app/features/teams/CreateTeam.test.tsx index 519b835cd52..12db809d40f 100644 --- a/public/app/features/teams/CreateTeam.test.tsx +++ b/public/app/features/teams/CreateTeam.test.tsx @@ -8,7 +8,7 @@ import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { CreateTeam } from './CreateTeam'; +import CreateTeam from './CreateTeam'; setBackendSrv(backendSrv); setupMockServer(); diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 050f7aefa1e..82ee804f6d3 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -3,16 +3,19 @@ import { useForm } from 'react-hook-form'; import { NavModelItem } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getBackendSrv, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { Button, Field, Input, FieldSet, Stack } from '@grafana/ui'; +import { extractErrorMessage } from 'app/api/utils'; import { Page } from 'app/core/components/Page/Page'; import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; -import { updateTeamRoles } from 'app/core/components/RolePicker/api'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; +import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; -import { Role, AccessControlAction } from 'app/types/accessControl'; +import { Role } from 'app/types/accessControl'; import { TeamDTO } from 'app/types/teams'; +import { useCreateTeam } from './hooks'; + const pageNav: NavModelItem = { icon: 'users-alt', id: 'team-new', @@ -20,8 +23,11 @@ const pageNav: NavModelItem = { subTitle: 'Create a new team. Teams let you grant permissions to a group of users.', }; -export const CreateTeam = (): JSX.Element => { +const CreateTeam = (): JSX.Element => { const currentOrgId = contextSrv.user.orgId; + + const notifyApp = useAppNotification(); + const [createTeamTrigger] = useCreateTeam(); const [pendingRoles, setPendingRoles] = useState([]); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -30,21 +36,28 @@ export const CreateTeam = (): JSX.Element => { formState: { errors }, } = useForm(); - const canUpdateRoles = - contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && - contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); - const createTeam = async (formModel: TeamDTO) => { try { - const newTeam = await getBackendSrv().post('/api/teams', formModel); - if (newTeam.teamId) { - await contextSrv.fetchUserPermissions(); - if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { - await updateTeamRoles(pendingRoles, newTeam.teamId, newTeam.orgId); - } - locationService.push(`/org/teams/edit/${newTeam.uid}`); + const { data, error } = await createTeamTrigger( + { + email: formModel.email || '', + name: formModel.name, + }, + pendingRoles + ); + + const errorMessage = error ? extractErrorMessage(error) : undefined; + + if (errorMessage) { + notifyApp.error(errorMessage); + return; + } + + if (data && data.uid) { + locationService.push(`/org/teams/edit/${data.uid}`); } } catch (e) { + notifyApp.error(t('teams.create-team.failed-to-create', 'Failed to create team')); console.error(e); } }; @@ -85,8 +98,13 @@ export const CreateTeam = (): JSX.Element => { 'This is optional and is primarily used for allowing custom team avatars' )} > - {/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */} - + diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index e3bd21b7e3a..d3a83d9c5dc 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -4,7 +4,7 @@ import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; -import { Team, TeamGroup, TeamState } from 'app/types/teams'; +import { TeamGroup, TeamState } from 'app/types/teams'; import TeamGroupSync from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; @@ -13,12 +13,10 @@ setBackendSrv(backendSrv); setupMockServer(); const setup = (preloadedTeamState?: Partial) => { - return render(, { + return render(, { preloadedState: { team: { - members: [], groups: [], - team: { uid: MOCK_TEAMS[0].metadata.name } as Team, ...preloadedTeamState, }, }, diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index e87a1d4380f..37887ee8d6a 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -29,6 +29,7 @@ const mapDispatchToProps = { interface OwnProps { isReadOnly: boolean; + teamUid: string; } interface State { @@ -52,7 +53,7 @@ export class TeamGroupSync extends PureComponent { } async fetchTeamGroups() { - this.props.loadTeamGroups(); + this.props.loadTeamGroups(this.props.teamUid); } onToggleAdding = () => { @@ -65,12 +66,12 @@ export class TeamGroupSync extends PureComponent { onAddGroup: FormEventHandler = (event) => { event.preventDefault(); - this.props.addTeamGroup(this.state.newGroupId); + this.props.addTeamGroup(this.props.teamUid, this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.removeTeamGroup(group.groupId); + this.props.removeTeamGroup(this.props.teamUid, group.groupId); }; isNewGroupValid() { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index a78cbe2eceb..017b6ad2d25 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -40,16 +40,16 @@ describe('TeamList', () => { it('should enable the new team button', async () => { render(); - expect(screen.getByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); }); }); describe('when user does not have access to create a team', () => { - it('should disable the new team button', () => { + it('should disable the new team button', async () => { jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); render(); - expect(screen.getByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); }); }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index e2ecbf07c4a..3ffd09b94fe 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; -import { connect, ConnectedProps } from 'react-redux'; +import { SortingRule } from 'react-table'; import { Trans, t } from '@grafana/i18n'; import { @@ -14,6 +14,7 @@ import { InlineField, InteractiveTable, LinkButton, + LoadingPlaceholder, Pagination, Stack, Tag, @@ -24,16 +25,14 @@ import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; import { Role, AccessControlAction } from 'app/types/accessControl'; -import { StoreState } from 'app/types/store'; import { TeamWithRoles } from 'app/types/teams'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { EnterpriseAuthFeaturesCard } from '../admin/EnterpriseAuthFeaturesCard'; -import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions'; +import { useDeleteTeam, useGetTeams } from './hooks'; type Cell = CellProps; -export interface OwnProps {} export interface State { roleOptions: Role[]; @@ -49,26 +48,31 @@ const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) => isProvisioned: false, })); -const TeamList = ({ - teams, - query, - noTeams, - hasFetched, - loadTeams, - deleteTeam, - changeQuery, - totalPages, - page, - rolesLoading, - changePage, - changeSort, -}: Props) => { +const TeamList = () => { + const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); + const displayRolePicker = shouldDisplayRolePicker(); + const pageSize = 20; + const [roleOptions, setRoleOptions] = useState([]); const styles = useStyles2(getStyles); + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [sort, setSort] = useState(); + const { data, isLoading } = useGetTeams({ query, pageSize, page, sort }); + const [deleteTeam] = useDeleteTeam(); - useEffect(() => { - loadTeams(true); - }, [loadTeams]); + const teams = data?.teams || []; + const totalPages = Math.ceil((data?.totalCount || 0) / pageSize) || 0; + const noTeams = teams?.length === 0; + const changeSort = useCallback( + (sort: SortingRule) => { + setSort(`${sort.id}-${sort.desc ? 'desc' : 'asc'}`); + }, + [setSort] + ); + const changePage = (page: number) => { + setPage(page); + }; useEffect(() => { if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { @@ -76,9 +80,6 @@ const TeamList = ({ } }, []); - const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); - const displayRolePicker = shouldDisplayRolePicker(); - const columns: Array> = useMemo( () => [ { @@ -86,7 +87,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ cell: { value } }: Cell<'avatarUrl'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -97,7 +98,7 @@ const TeamList = ({ id: 'name', header: 'Name', cell: ({ cell: { value }, row: { original } }: Cell<'name'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -123,7 +124,7 @@ const TeamList = ({ id: 'email', header: 'Email', cell: ({ cell: { value } }: Cell<'email'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -135,7 +136,7 @@ const TeamList = ({ header: 'Members', disableGrow: true, cell: ({ cell: { value } }: Cell<'memberCount'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -147,8 +148,8 @@ const TeamList = ({ { id: 'role', header: 'Role', - cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => { - if (!hasFetched) { + cell: ({ row: { original } }: Cell<'memberCount'>) => { + if (isLoading) { return ; } const canSeeTeamRoles = contextSrv.hasPermissionInMetadata( @@ -160,7 +161,7 @@ const TeamList = ({ @@ -174,7 +175,7 @@ const TeamList = ({ id: 'isProvisioned', header: '', cell: ({ cell: { value } }: Cell<'isProvisioned'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return !!value && ; @@ -185,7 +186,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ row: { original } }: Cell) => { - if (!hasFetched) { + if (isLoading) { return ( @@ -216,14 +217,14 @@ const TeamList = ({ })} size="sm" disabled={!canDelete} - onConfirm={() => deleteTeam(original.uid)} + onConfirm={() => deleteTeam({ uid: original.uid })} /> ); }, }, ], - [displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles] + [displayRolePicker, isLoading, styles.blockSkeleton, roleOptions, deleteTeam] ); return ( @@ -238,7 +239,7 @@ const TeamList = ({ } > - {noTeams ? ( + {!isLoading && !query && teams.length === 0 ? (
- {hasFetched && teams.length === 0 ? ( + {!isLoading && teams.length === 0 && ( - ) : ( + )} + {isLoading && } + {!isLoading && teams.length > 0 && ( String(team.id)} - fetchData={changeSort} + fetchData={({ sortBy }) => { + const sortingRule = sortBy.at(0); + if (sortingRule) { + return changeSort(sortingRule); + } + }} /> ; -export default connector(TeamList); +export default TeamList; const getStyles = () => ({ blockSkeleton: css({ diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 89a5cb5e032..3391a08c17d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { memo, useRef } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; -import { useAsync } from 'react-use'; import { featureEnabled } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; @@ -10,14 +9,13 @@ import config from 'app/core/config'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; -import { StoreState, useDispatch, useSelector } from 'app/types/store'; +import { StoreState, useSelector } from 'app/types/store'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; -import { loadTeam } from './state/actions'; +import { useGetTeam } from './hooks'; import { getTeamLoadingNav } from './state/navModel'; -import { getTeam } from './state/selectors'; type TeamPageRouteParams = { uid: string; @@ -32,11 +30,6 @@ enum PageTypes { const PAGES = ['members', 'settings', 'groupsync']; -const teamSelector = createSelector( - [(state: StoreState) => state.team, (_: StoreState, teamUid: string) => teamUid], - (team, teamUid) => getTeam(team, teamUid) -); - const pageNavSelector = createSelector( [ (state: StoreState) => state.navIndex, @@ -52,7 +45,8 @@ const pageNavSelector = createSelector( const TeamPages = memo(() => { const isSyncEnabled = useRef(featureEnabled('teamsync')); const { uid: teamUid = '', page } = useParams(); - const team = useSelector((state) => teamSelector(state, teamUid)); + + const { data: team, isLoading } = useGetTeam({ uid: teamUid }); let defaultPage = 'members'; // With RBAC the settings page will always be available @@ -62,9 +56,6 @@ const TeamPages = memo(() => { const pageName = page ?? defaultPage; const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamUid)); - const dispatch = useDispatch(); - const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamUid)), [teamUid]); - const renderPage = () => { const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0]; @@ -89,7 +80,7 @@ const TeamPages = memo(() => { case PageTypes.GroupSync: if (isSyncEnabled.current) { if (canReadTeamPermissions) { - return ; + return ; } } else if (config.featureToggles.featureHighlights) { return ( diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index 9d31644657a..f5cd0a6fcf3 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -1,12 +1,13 @@ -import { render, screen, waitFor } from 'test/test-utils'; +import { render, screen } from 'test/test-utils'; import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { Props, TeamSettings } from './TeamSettings'; +import TeamSettings from './TeamSettings'; jest.spyOn(contextSrv, 'hasPermission').mockImplementation(() => true); jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true); @@ -14,9 +15,9 @@ jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true) setBackendSrv(backendSrv); setupMockServer(); -const setup = (propOverrides?: object) => { +const setup = () => { const team = MOCK_TEAMS[0]; - const props: Props = { + const props = { team: { id: Number(team.metadata.labels['grafana.app/deprecatedInternalID']), uid: team.metadata.name, @@ -25,12 +26,14 @@ const setup = (propOverrides?: object) => { orgId: 1, isProvisioned: false, }, - updateTeam: jest.fn(), }; - Object.assign(props, propOverrides); - - return render(); + return render( + <> + + + + ); }; describe('Team settings', () => { @@ -41,8 +44,7 @@ describe('Team settings', () => { }); it('should validate required fields', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await screen.findByText('Team details'); await user.clear(screen.getByRole('textbox', { name: /Name/ })); @@ -50,12 +52,10 @@ describe('Team settings', () => { await user.click(screen.getByRole('button', { name: 'Save team details' })); expect(await screen.findByText('Name is required')).toBeInTheDocument(); - await waitFor(() => expect(mockUpdate).not.toHaveBeenCalled()); }); it('should submit form with correct values', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await user.clear(screen.getByRole('textbox', { name: /Name/ })); await user.clear(screen.getByLabelText(/Email/i)); @@ -63,6 +63,6 @@ describe('Team settings', () => { await user.type(screen.getByLabelText(/Email/i), 'team@test.com'); await user.click(screen.getByRole('button', { name: 'Save team details' })); - await waitFor(() => expect(mockUpdate).toHaveBeenCalledWith('New team', 'team@test.com')); + expect(await screen.findByText('Team updated')).toBeInTheDocument(); }); }); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index a4779df63cc..7a847d9ebf3 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,5 +1,4 @@ import { useForm } from 'react-hook-form'; -import { ConnectedProps, connect } from 'react-redux'; import { Trans, t } from '@grafana/i18n'; import { Button, Field, FieldSet, Input, Stack } from '@grafana/ui'; @@ -10,22 +9,16 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; import { Team } from 'app/types/teams'; -import { updateTeam } from './state/actions'; +import { useUpdateTeam } from './hooks'; -const mapDispatchToProps = { - updateTeam, -}; - -const connector = connect(null, mapDispatchToProps); - -interface OwnProps { +interface Props { team: Team; } -export type Props = ConnectedProps & OwnProps; -export const TeamSettings = ({ team, updateTeam }: Props) => { +const TeamSettings = ({ team }: Props) => { const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); const currentOrgId = contextSrv.user.orgId; + const [updateTeam] = useUpdateTeam(); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -43,7 +36,13 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { contextSrv.hasPermission(AccessControlAction.ActionRolesList); const onSubmit = async (formTeam: Team) => { - updateTeam(formTeam.name, formTeam.email || ''); + return updateTeam({ + uid: team.uid, + team: { + name: formTeam.name, + email: formTeam.email || '', + }, + }); }; return ( @@ -103,4 +102,4 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { ); }; -export default connector(TeamSettings); +export default TeamSettings; diff --git a/public/app/features/teams/hooks.ts b/public/app/features/teams/hooks.ts new file mode 100644 index 00000000000..2c563ef3d2f --- /dev/null +++ b/public/app/features/teams/hooks.ts @@ -0,0 +1,154 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useEffect, useMemo } from 'react'; + +import { + useSearchTeamsQuery as useLegacySearchTeamsQuery, + useCreateTeamMutation, + useDeleteTeamByIdMutation, + useListTeamsRolesQuery, + CreateTeamCommand, + useSetTeamRolesMutation, + useGetTeamByIdQuery, + useUpdateTeamMutation, + UpdateTeamCommand, +} from 'app/api/clients/legacy'; +import { updateNavIndex } from 'app/core/actions'; +import { addFilteredDisplayName } from 'app/core/components/RolePicker/utils'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { useDispatch } from 'app/types/store'; + +import { buildNavModel } from './state/navModel'; + +const rolesEnabled = + contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList); + +const canUpdateRoles = () => + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); + +/** + * Get list of teams and their associated roles (if roles are enabled) + */ +export const useGetTeams = ({ + query, + pageSize, + page, + sort, +}: { + query?: string; + pageSize?: number; + page?: number; + sort?: string; +}) => { + const legacyResponse = useLegacySearchTeamsQuery({ perpage: pageSize, accesscontrol: true, page, sort, query }); + + const teamIds = useMemo(() => { + const teams = legacyResponse.data?.teams || []; + const ids = teams.map((team) => team.id); + return ids.filter((id): id is number => id !== undefined); + }, [legacyResponse.data?.teams]); + + const teamsRolesResponse = useListTeamsRolesQuery( + rolesEnabled && teamIds.length ? { rolesSearchQuery: { teamIds } } : skipToken + ); + + const teamsWithRoles = useMemo(() => { + if (!rolesEnabled || (rolesEnabled && teamsRolesResponse.isLoading)) { + return legacyResponse.data?.teams || []; + } + return (legacyResponse.data?.teams || []).map((team) => { + const roles = team.id ? teamsRolesResponse.data?.[team.id] || [] : []; + const mappedRoles = roles.map((role) => addFilteredDisplayName(role)); + return { + ...team, + roles: mappedRoles, + }; + }); + }, [legacyResponse, teamsRolesResponse]); + + return { + ...legacyResponse, + isLoading: legacyResponse.isLoading || (rolesEnabled ? teamsRolesResponse.isLoading : false), + data: { + teams: teamsWithRoles, + totalCount: legacyResponse.data?.totalCount, + }, + }; +}; + +/** + * Get a single team by UID + */ +export const useGetTeam = ({ uid }: { uid: string }) => { + const response = useGetTeamByIdQuery({ teamId: uid, accesscontrol: true }); + const dispatch = useDispatch(); + + // TODO: Eventually remove and handle nav index logic elsewhere + useEffect(() => { + if (response.data) { + dispatch(updateNavIndex(buildNavModel(response.data))); + } + }, [response.data, dispatch]); + + return response; +}; + +/** + * Update a team by UID + */ +export const useUpdateTeam = () => { + const [updateTeam, response] = useUpdateTeamMutation(); + + const trigger = async ({ uid, team }: { uid: string; team: UpdateTeamCommand }) => { + const mutationResult = await updateTeam({ + teamId: uid, + updateTeamCommand: team, + }); + + return mutationResult; + }; + + return [trigger, response] as const; +}; + +/** + * Delete a team by UID + */ +export const useDeleteTeam = () => { + const [deleteTeam, response] = useDeleteTeamByIdMutation(); + + return [({ uid }: { uid: string }) => deleteTeam({ teamId: uid }), response] as const; +}; + +/** + * Create a new team, and link any pending roles + */ +export const useCreateTeam = () => { + const [createTeam, response] = useCreateTeamMutation(); + const [setTeamRoles] = useSetTeamRolesMutation(); + + const trigger = async (team: CreateTeamCommand, pendingRoles?: Role[]) => { + const mutationResult = await createTeam({ + createTeamCommand: team, + }); + + const { data } = mutationResult; + + if (data && data.teamId && pendingRoles && pendingRoles.length) { + await contextSrv.fetchUserPermissions(); + if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles()) { + await setTeamRoles({ + teamId: data.teamId, + setTeamRolesCommand: { + roleUids: pendingRoles.map((role) => role.uid), + }, + }); + } + } + + return mutationResult; + }; + + return [trigger, response] as const; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 49c22c2c449..d0d05e0946c 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,134 +1,26 @@ -import { debounce } from 'lodash'; - import { getBackendSrv } from '@grafana/runtime'; -import { FetchDataArgs } from '@grafana/ui'; -import { updateNavIndex } from 'app/core/actions'; -import { contextSrv } from 'app/core/services/context_srv'; -import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { AccessControlAction } from 'app/types/accessControl'; import { ThunkResult } from 'app/types/store'; -import { Team, TeamWithRoles } from 'app/types/teams'; -import { buildNavModel } from './navModel'; -import { - teamGroupsLoaded, - queryChanged, - pageChanged, - teamLoaded, - teamsLoaded, - sortChanged, - rolesFetchBegin, - rolesFetchEnd, -} from './reducers'; +import { teamGroupsLoaded } from './reducers'; -export function loadTeams(initial = false): ThunkResult { - return async (dispatch, getState) => { - const { query, page, perPage, sort } = getState().teams; - // Early return if the user cannot list teams - if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) { - dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true })); - return; - } - - const response = await getBackendSrv().get( - '/api/teams/search', - accessControlQueryParam({ query, page, perpage: perPage, sort }) - ); - - // We only want to check if there is no teams on the initial request. - // A query that returns no teams should not render the empty list banner. - let noTeams = false; - if (initial) { - noTeams = response.teams.length === 0; - } - - if ( - contextSrv.licensedAccessControlEnabled() && - contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) - ) { - dispatch(rolesFetchBegin()); - const teamIds = response?.teams.map((t: TeamWithRoles) => t.id); - const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds }); - response.teams.forEach((t: TeamWithRoles) => { - t.roles = roles ? roles[t.id] || [] : []; - }); - dispatch(rolesFetchEnd()); - } - - dispatch(teamsLoaded({ noTeams, ...response })); - }; -} - -const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500); - -export function loadTeam(uid: string): ThunkResult> { +export function loadTeamGroups(teamUid: string): ThunkResult { return async (dispatch) => { - const response = await getBackendSrv().get(`/api/teams/${uid}`, accessControlQueryParam()); - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }; -} - -export function deleteTeam(uid: string): ThunkResult { - return async (dispatch) => { - await getBackendSrv().delete(`/api/teams/${uid}`); - // Update users permissions in case they lost teams.read with the deletion - await contextSrv.fetchUserPermissions(); - dispatch(loadTeams()); - }; -} - -export function changeQuery(query: string): ThunkResult { - return async (dispatch) => { - dispatch(queryChanged(query)); - loadTeamsWithDebounce(dispatch); - }; -} - -export function changePage(page: number): ThunkResult { - return async (dispatch) => { - dispatch(pageChanged(page)); - dispatch(loadTeams()); - }; -} - -export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { - const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined; - return async (dispatch) => { - dispatch(sortChanged(sort)); - dispatch(loadTeams()); - }; -} - -export function updateTeam(name: string, email: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().put(`/api/teams/${team.uid}`, { name, email }); - dispatch(loadTeam(team.uid)); - }; -} - -export function loadTeamGroups(): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - const response = await getBackendSrv().get(`/api/teams/${team.uid}/groups`); + const response = await getBackendSrv().get(`/api/teams/${teamUid}/groups`); dispatch(teamGroupsLoaded(response)); }; } -export function addTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().post(`/api/teams/${team.uid}/groups`, { groupId: groupId }); - dispatch(loadTeamGroups()); +export function addTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().post(`/api/teams/${teamUid}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups(teamUid)); }; } -export function removeTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; +export function removeTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { // need to use query parameter due to escaped characters in the request - await getBackendSrv().delete(`/api/teams/${team.uid}/groups?groupId=${encodeURIComponent(groupId)}`); - dispatch(loadTeamGroups()); + await getBackendSrv().delete(`/api/teams/${teamUid}/groups?groupId=${encodeURIComponent(groupId)}`); + dispatch(loadTeamGroups(teamUid)); }; } diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts deleted file mode 100644 index 4ea5c963739..00000000000 --- a/public/app/features/teams/state/reducers.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { TeamsState, TeamState } from 'app/types/teams'; - -import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { getMockTeam, getMockTeamGroups } from '../mocks/teamMocks'; - -import { - initialTeamsState, - initialTeamState, - teamGroupsLoaded, - teamLoaded, - queryChanged, - teamReducer, - teamsLoaded, - teamsReducer, -} from './reducers'; - -describe('teams reducer', () => { - describe('when teamsLoaded is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched( - teamsLoaded({ teams: [getMockTeam()], page: 1, perPage: 30, noTeams: false, totalCount: 100 }) - ) - .thenStateShouldEqual({ - ...initialTeamsState, - hasFetched: true, - teams: [getMockTeam()], - noTeams: false, - totalPages: 4, - perPage: 30, - page: 1, - }); - }); - }); - - describe('when setSearchQueryAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched(queryChanged('test')) - .thenStateShouldEqual({ - ...initialTeamsState, - query: 'test', - }); - }); - }); -}); - -describe('team reducer', () => { - describe('when loadTeamsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamLoaded(getMockTeam())) - .thenStateShouldEqual({ - ...initialTeamState, - team: getMockTeam(), - }); - }); - }); - - describe('when loadTeamGroupsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamGroupsLoaded(getMockTeamGroups(1))) - .thenStateShouldEqual({ - ...initialTeamState, - groups: getMockTeamGroups(1), - }); - }); - }); -}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 2ef9dde3838..50011316a72 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,60 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { TeamsState, Team, TeamState, TeamGroup } from 'app/types/teams'; - -export const initialTeamsState: TeamsState = { - teams: [], - page: 1, - query: '', - perPage: 30, - totalPages: 0, - noTeams: false, - hasFetched: false, -}; - -type TeamsFetched = { - teams: Team[]; - page: number; - perPage: number; - noTeams: boolean; - totalCount: number; -}; - -const teamsSlice = createSlice({ - name: 'teams', - initialState: initialTeamsState, - reducers: { - teamsLoaded: (state, action: PayloadAction): TeamsState => { - const { totalCount, perPage, ...rest } = action.payload; - const totalPages = Math.ceil(totalCount / perPage); - return { ...state, ...rest, totalPages, perPage, hasFetched: true }; - }, - queryChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: 1, query: action.payload }; - }, - pageChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: action.payload }; - }, - sortChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, sort: action.payload, page: 1 }; - }, - rolesFetchBegin: (state) => { - return { ...state, rolesLoading: true }; - }, - rolesFetchEnd: (state) => { - return { ...state, rolesLoading: false }; - }, - }, -}); - -export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } = - teamsSlice.actions; - -export const teamsReducer = teamsSlice.reducer; +import { TeamState, TeamGroup } from 'app/types/teams'; export const initialTeamState: TeamState = { - team: {} as Team, - members: [], groups: [], }; @@ -62,20 +10,16 @@ const teamSlice = createSlice({ name: 'team', initialState: initialTeamState, reducers: { - teamLoaded: (state, action: PayloadAction): TeamState => { - return { ...state, team: action.payload }; - }, teamGroupsLoaded: (state, action: PayloadAction): TeamState => { return { ...state, groups: action.payload }; }, }, }); -export const { teamLoaded, teamGroupsLoaded } = teamSlice.actions; +export const { teamGroupsLoaded } = teamSlice.actions; export const teamReducer = teamSlice.reducer; export default { - teams: teamsReducer, team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts deleted file mode 100644 index 983c5d35a71..00000000000 --- a/public/app/features/teams/state/selectors.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { TeamState } from 'app/types/teams'; - -import { getMockTeam } from '../mocks/teamMocks'; - -import { getTeam } from './selectors'; - -describe('Team selectors', () => { - describe('Get team', () => { - const mockTeam = getMockTeam(); - - it('should return team if matching with location team', () => { - const mockState: TeamState = { - team: mockTeam, - members: [], - groups: [], - }; - - const team = getTeam(mockState, 'aaaaaa'); - expect(team).toEqual(mockTeam); - }); - }); -}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index ec99297158c..4ada9ee8d46 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,11 +1,3 @@ -import { Team, TeamState } from 'app/types/teams'; +import { TeamState } from 'app/types/teams'; export const getTeamGroups = (state: TeamState) => state.groups; - -export const getTeam = (state: TeamState, currentTeamUid: string): Team | null => { - if (state.team.uid === currentTeamUid) { - return state.team; - } - - return null; -}; diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index d09459b9f9b..b2e2fc31a9b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -1,3 +1,5 @@ +import { RoleDto } from 'app/api/clients/legacy'; + /** * UserPermission is a map storing permissions in a form of * { @@ -174,17 +176,6 @@ export enum AccessControlAction { MigrationAssistantMigrate = 'migrationassistant:migrate', } -export interface Role { - uid: string; - name: string; - displayName: string; +export interface Role extends RoleDto { filteredDisplayName: string; // name to be shown in filtered role list - description: string; - group: string; - global: boolean; - delegatable?: boolean; - mapped?: boolean; - version: number; - created: string; - updated: string; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index f592b362894..72202a21295 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,4 +1,4 @@ -import { WithAccessControlMetadata } from '@grafana/data'; +import { TeamDto as TeamDtoLegacy } from 'app/api/clients/legacy'; import { Role } from './accessControl'; @@ -13,42 +13,7 @@ export interface TeamDTO { name: string; } -// This is the team resource with permissions and metadata expanded -export interface Team extends WithAccessControlMetadata { - /** - * Internal id of team - * @deprecated use uid instead - */ - id: number; - /** - * A unique identifier for the team. - */ - uid: string; // Prefer UUID - /** - * AvatarUrl is the team's avatar URL. - */ - avatarUrl?: string; - /** - * Email of the team. - */ - email?: string; - /** - * MemberCount is the number of the team members. - */ - memberCount: number; - /** - * Name of the team. - */ - name: string; - /** - * OrgId is the ID of an organisation the team belongs to. - */ - orgId: number; - /** - * isProvisioned is set if the team has been provisioned from IdP. - */ - isProvisioned: boolean; -} +export type Team = TeamDtoLegacy; export interface TeamWithRoles extends Team { /** @@ -73,20 +38,6 @@ export interface TeamGroup { teamId: number; } -export interface TeamsState { - teams: Team[]; - page: number; - query: string; - perPage: number; - noTeams: boolean; - totalPages: number; - hasFetched: boolean; - sort?: string; - rolesLoading?: boolean; -} - export interface TeamState { - team: Team; - members: TeamMember[]; groups: TeamGroup[]; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f2d2366b9cd..6230117bcf2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13277,6 +13277,7 @@ "create-team": { "create": "Create", "description-email": "This is optional and is primarily used for allowing custom team avatars", + "failed-to-create": "Failed to create team", "label-email": "Email", "label-name": "Name", "label-role": "Role" @@ -13308,6 +13309,7 @@ "title-edit-team": "Edit team", "tooltip-edit-team": "Edit team" }, + "loading-teams": "Loading teams...", "new-team": "New Team", "placeholder-search-teams": "Search teams" }, diff --git a/public/openapi3.json b/public/openapi3.json index 20b97b3f14d..52322514490 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12703,17 +12703,10 @@ }, "UpdateTeamCommand": { "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "format": "int64", - "type": "integer" - }, - "Name": { + "name": { "type": "string" } }, @@ -24935,6 +24928,14 @@ "schema": { "type": "string" } + }, + { + "in": "query", + "name": "accesscontrol", + "schema": { + "default": false, + "type": "boolean" + } } ], "responses": { From f0e9c2e8a386b476c16f84f4686507d0389601d8 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 28 Nov 2025 12:53:38 +0000 Subject: [PATCH 171/423] FS: Fix HTML loader jumping a few pixels (#114581) --- pkg/services/frontend/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 0a589f81a49..b0364fcbac9 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -79,6 +79,7 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + line-height: 1; /* prevent shift when css loads in that changes the body line-height */ } .fs-variant-loader, .fs-variant-error, .fs-custom-domain-error { From d3d294c99eee0755e6f24e3dd057834dbf6050c5 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 28 Nov 2025 15:03:11 +0200 Subject: [PATCH 172/423] Dashboards: Per panel GroupBy action (#113445) * wip per panel group by * wip groupBy per panel * wip groupBy per panel * groupBy per panel action tests * fix * fix * fix * fix * CR mods * switch to dropdown * adjust apply * optimise action logic to avoid unnecessary triggers * canary scenes * wip (cherry picked from commit 51a00db93d0805f481a9e48213382468f1eb2986) * optimise action logic to avoid unnecessary triggers (cherry picked from commit c4de2dfff88c02c5aef61d1cbee070b5f6e5ccbc) * refactor * refactor * memoize values/ refactor * refactor * refactor components - do not make async call unless queries/groupByOptions change * canary scenes * fix test * Optimise handlers * Reset options if they are not applied * refactor subscriptions * refactor * scenes bump * fixes * properly deactivate header actions on panel edit * list * refactor showing menu using css, remove header deactivation code from panel-edit * cleanup * cleanup * cleanup + action redesign * i18n * pr mods * translations * fix * fix * fix design --------- Co-authored-by: Sergej-Vlasov Co-authored-by: Dominik Prokop --- .../src/types/featureToggles.gen.ts | 4 + .../src/selectors/components.ts | 3 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../scene/VizPanelHeaderActions.test.tsx | 154 +++++++++++++++++ .../scene/VizPanelHeaderActions.tsx | 136 +++++++++++++++ .../scene/VizPanelSubHeader.tsx | 5 +- .../PanelGroupByAction/PanelGroupByAction.tsx | 150 +++++++++++++++++ .../PanelGroupByActionPopover.tsx | 156 ++++++++++++++++++ .../serialization/layoutSerializers/utils.ts | 7 + .../transformSaveModelToScene.ts | 4 + .../features/dashboard-scene/utils/utils.ts | 4 + public/locales/en-US/grafana.json | 6 + 14 files changed, 648 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 35cccd78375..d853a64395a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -377,6 +377,10 @@ export interface FeatureToggles { */ perPanelNonApplicableDrilldowns?: boolean; /** + * Enabled a group by action per panel + */ + panelGroupBy?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 98fa6053d11..36d0088b258 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -443,6 +443,9 @@ export const versionedComponents = { PanelDataErrorMessage: { '10.4.0': 'data-testid Panel data error message', }, + PanelGroupByHeaderAction: { + '12.4.0': 'data-testid Panel group by header action', + }, }, Visualization: { Graph: { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 950476d1a8f..c112e0b77a0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -607,6 +607,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "panelGroupBy", + Description: "Enabled a group by action per panel", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "panelFilterVariable", Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a7423d583ab..1382204ce0f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -84,6 +84,7 @@ kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true +panelGroupBy,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ea80af0831e..8451faca5d0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2512,6 +2512,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "panelGroupBy", + "resourceVersion": "1764257043719", + "creationTimestamp": "2025-11-27T15:24:03Z" + }, + "spec": { + "description": "Enabled a group by action per panel", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "panelTimeSettings", diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx new file mode 100644 index 00000000000..24b83e853fe --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx @@ -0,0 +1,154 @@ +import { of } from 'rxjs'; + +import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + GroupByVariable, + SceneDataTransformer, + SceneQueryRunner, + SceneVariableSet, + VizPanel, + VizPanelState, +} from '@grafana/scenes'; + +import { activateFullSceneTree } from '../utils/test-utils'; + +import { DashboardScene } from './DashboardScene'; +import { VizPanelHeaderActions } from './VizPanelHeaderActions'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; + +const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { + return of({ + state: LoadingState.Loading, + series: [], + timeRange: request.range, + }); +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { + return runRequestMock(ds, request); + }, + getDataSourceSrv: () => ({ + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds-1', type: 'test' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }), + }), + getPluginImportUtils: () => ({ + getPanelPluginFromCache: jest.fn(() => undefined), + }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('VizPanelHeaderActions', () => { + it('renders PanelGroupByAction when the group by variable applies to the panel', async () => { + const { headerActions } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + }); + + it('does not set when applicability is disabled', async () => { + const { headerActions } = await buildScene({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set when the datasource uid does not match', async () => { + const { headerActions } = await buildScene({ + variableDatasourceUid: 'other-ds', + }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable ds changes to a different type', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable applicability becomes disabled', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('sdoes not set if queryRunner changes datasource to different one than vars', async () => { + const { headerActions, queryRunner } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + queryRunner.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); +}); + +interface BuildSceneOptions { + applicabilityEnabled?: boolean; + variableDatasourceUid?: string; +} + +async function buildScene(options?: BuildSceneOptions) { + const headerActions = new VizPanelHeaderActions({}); + + const queryRunner = new SceneQueryRunner({ + datasource: { uid: 'ds-1' }, + queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }], + }); + + const groupByVariable = new GroupByVariable({ + name: 'group', + label: 'group', + value: [], + text: [], + options: [], + applicabilityEnabled: options?.applicabilityEnabled ?? true, + datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' }, + }); + + const dataProvider = new SceneDataTransformer({ + $data: queryRunner, + transformations: [], + }); + + const panelState: VizPanelState = { + key: 'panel-1', + title: 'Panel A', + pluginId: 'timeseries', + headerActions, + $data: dataProvider, + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + }; + + const panel = new VizPanel(panelState); + + const scene = new DashboardScene({ + $variables: new SceneVariableSet({ + variables: [groupByVariable], + }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + return { headerActions, panel, groupByVariable, queryRunner }; +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx new file mode 100644 index 00000000000..3130d746c94 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx @@ -0,0 +1,136 @@ +import { Unsubscribable } from 'rxjs'; + +import { + GroupByVariable, + SceneComponentProps, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { verifyDrilldownApplicability } from '../utils/drilldownUtils'; + +import { PanelGroupByAction } from './panel-actions/PanelGroupByAction/PanelGroupByAction'; + +export interface VizPanelHeaderActionsState extends SceneObjectState { + hideGroupByAction?: boolean; + supportsApplicability?: boolean; +} + +export class VizPanelHeaderActions extends SceneObjectBase { + static Component = VizPanelHeaderActionsRenderer; + + private _groupByVar?: GroupByVariable; + private _groupBySub?: Unsubscribable; + private _queryRunnerDatasource?: DataSourceRef; + + constructor(state: Partial) { + super({ + hideGroupByAction: state.hideGroupByAction ?? false, + ...state, + }); + + this.addActivationHandler(this._onActivate); + } + + private _onActivate = () => { + if (!this.parent || !(this.parent instanceof VizPanel)) { + throw new Error('VizPanelHeaderActions must be a child of a VizPanel'); + } + + if (!this.state.hideGroupByAction) { + this.subscribeToGroupByChanges(); + } + + return () => { + this._groupBySub?.unsubscribe(); + }; + }; + + private setAplicabilitySupport(groupByDs?: DataSourceRef | null, groupByApplicability?: boolean) { + this.setState({ + supportsApplicability: verifyDrilldownApplicability( + this, + this._queryRunnerDatasource, + groupByDs ?? this._groupByVar?.state.datasource ?? null, + groupByApplicability ?? this._groupByVar?.state.applicabilityEnabled ?? false + ), + }); + } + + private subscribeToGroupByChanges() { + const vars = sceneGraph.getVariables(this); + const queryRunner = this.getQueryRunner(); + + this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable); + this._queryRunnerDatasource = queryRunner?.state.datasource; + + this.setAplicabilitySupport(); + + // check when var set updates and search for groupBy var + this._subs.add( + vars.subscribeToState((n) => { + this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable); + + if (this._groupByVar) { + this._groupBySub?.unsubscribe(); + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + }) + ); + + // update query runner datasource changes + this._subs.add( + queryRunner?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource) { + this._queryRunnerDatasource = n.datasource; + + this.setAplicabilitySupport(); + } + }) + ); + + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + + public getQueryRunner() { + const panel = this.parent; + const dataObject = panel ? sceneGraph.getData(panel) : undefined; + const queryRunner = dataObject?.state.$data; + + if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) { + return null; + } + + return queryRunner; + } +} + +export function VizPanelHeaderActionsRenderer({ model }: SceneComponentProps) { + const { hideGroupByAction, supportsApplicability } = model.useState(); + const variables = sceneGraph.getVariables(model); + const groupByVariable = variables.state.variables.find((variable) => variable instanceof GroupByVariable); + const queryRunner = model.getQueryRunner(); + const queries = queryRunner?.state.data?.request?.targets ?? []; + + return ( + <> + {!hideGroupByAction && supportsApplicability && ( +
+ +
+ )} + + ); +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx index f2247661e04..ff6f6c0712e 100644 --- a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx +++ b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx @@ -116,7 +116,7 @@ export class VizPanelSubHeader extends SceneObjectBase { private refreshDrilldownVarsSubscriptions() { if (this._groupByVar) { this._groupBySub?.unsubscribe(); - this._groupByVar?.subscribeToState((n, p) => { + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper(undefined, { datasource: n.datasource, @@ -127,7 +127,8 @@ export class VizPanelSubHeader extends SceneObjectBase { } if (this._adHocVar) { - this._adHocVar?.subscribeToState((n, p) => { + this._adHocSub?.unsubscribe(); + this._adHocSub = this._adHocVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper({ datasource: n.datasource, diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx new file mode 100644 index 00000000000..b12cc52cbe3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; +import { lastValueFrom } from 'rxjs'; + +import { fuzzySearch } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Trans } from '@grafana/i18n'; +import { GroupByVariable, SceneDataQuery, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Icon, Popover } from '@grafana/ui'; + +import { PanelGroupByActionPopover } from './PanelGroupByActionPopover'; + +interface Props { + groupByVariable: GroupByVariable; + queries: SceneDataQuery[]; +} + +export function PanelGroupByAction({ groupByVariable, queries }: Props) { + const { options: groupByOptions } = groupByVariable.useState(); + + const [options, setOptions] = useState([]); + const [selectedValues, setSelectedValues] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [reloadOptions, setReloadOptions] = useState(false); + const [isPopoverVisible, setPopoverVisible] = useState(false); + + const ref = useRef(null); + + const fetchOptions = useCallback(async () => { + if (!groupByVariable || !reloadOptions) { + return; + } + + setIsLoading(true); + try { + if (groupByOptions.length === 0) { + await lastValueFrom(groupByVariable.validateAndUpdate()); + const options = await getApplicableGroupByOptions(groupByVariable, groupByVariable.state.options, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + return; + } + + const options = await getApplicableGroupByOptions(groupByVariable, groupByOptions, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + } catch (error) { + setSelectedValues([]); + setOptions([]); + } finally { + setIsLoading(false); + setReloadOptions(false); + } + }, [groupByOptions, groupByVariable, queries, reloadOptions]); + + useEffect(() => { + setReloadOptions(true); + }, [groupByOptions, queries]); + + useEffect(() => { + if (isPopoverVisible) { + fetchOptions(); + } + }, [fetchOptions, isPopoverVisible]); + + const filteredOptions = useMemo(() => { + if (!searchValue) { + return options; + } + + const haystack = options.map((option) => option.label); + const indices = fuzzySearch(haystack, searchValue); + return indices.map((idx) => options[idx]); + }, [options, searchValue]); + + const onCancel = () => { + setSearchValue(''); + setPopoverVisible(false); + }; + + const openPopover = () => { + // Reset checked state to match current variable value when opening + setSelectedValues(getGroupByValue(groupByVariable)); + setPopoverVisible(true); + }; + + return ( + + ); +} + +function getGroupByValue(groupByVariable: GroupByVariable) { + return Array.isArray(groupByVariable.state.value) + ? groupByVariable.state.value + : groupByVariable.state.value + ? [groupByVariable.state.value] + : []; +} + +async function getApplicableGroupByOptions( + groupByVariable: GroupByVariable, + options: VariableValueOption[], + queries: SceneDataQuery[] +) { + const values = options.map((option) => option.value); + const applicability = await groupByVariable.getGroupByApplicabilityForQueries(values, queries); + + return applicability + ? applicability.filter((item) => item.applicable).map((item) => ({ label: item.key, value: item.key })) + : options; +} diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx new file mode 100644 index 00000000000..06191e1f7bf --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx @@ -0,0 +1,156 @@ +import { css, cx } from '@emotion/css'; +import { useCallback } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { GroupByVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Checkbox, ClickOutsideWrapper, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; + +interface Props { + groupByVariable: GroupByVariable; + onCancel: () => void; + isLoading: boolean; + searchValue: string; + setSearchValue: (value: string) => void; + options: VariableValueOption[]; + values: VariableValueSingle[]; + onValuesChange: (value: VariableValueSingle[]) => void; +} + +export function PanelGroupByActionPopover({ + groupByVariable, + onCancel, + isLoading, + searchValue, + setSearchValue, + options, + values, + onValuesChange, +}: Props) { + const styles = useStyles2(getStyles); + + const onCheckedChanged = useCallback( + (option: VariableValueOption) => (event: React.FormEvent) => { + const newValues = event.currentTarget.checked + ? values.concat(option.value) + : values.filter((c) => c !== option.value); + + onValuesChange(newValues); + }, + [onValuesChange, values] + ); + + const isChecked = (option: VariableValueOption) => { + return values.includes(option.value); + }; + + const handleApply = useCallback(() => { + if (!values.length) { + return; + } + + groupByVariable.changeValueTo(values, values.map(String), true); + onCancel(); + }, [groupByVariable, onCancel, values]); + + const isAnyOptionChecked = () => { + if (!values.length) { + return false; + } + + return values.some((value) => options.find((option) => option.value === value)); + }; + + return ( + + {/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} +
ev.stopPropagation()}> + +
+ } + placeholder={t('panel-group-by.search-placeholder', 'Search')} + value={searchValue} + onChange={(e) => setSearchValue(e.currentTarget.value)} + /> +
+ +
+ {isLoading ? ( +
+ Loading options +
+ ) : options.length === 0 ? ( +
+ No options found +
+ ) : ( + options.map((option) => { + return ( +
+ +
+ ); + }) + )} +
+ + + + + +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + menuContainer: css({ + display: 'flex', + flexDirection: 'column', + background: theme.colors.background.elevated, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + padding: theme.spacing(2), + }), + searchContainer: css({ + width: '100%', + paddingBottom: theme.spacing(1), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + listContainer: css({ + flex: 1, + overflow: 'auto', + minHeight: '100px', + maxHeight: '300px', + padding: theme.spacing(0.5), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + option: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1), + cursor: 'pointer', + borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.background.secondary, + }, + '&:focus-visible': { + outline: `2px solid ${theme.colors.primary.border}`, + outlineOffset: '-2px', + }, + }), + emptyMessage: css({ + padding: theme.spacing(2), + textAlign: 'center', + color: theme.colors.text.secondary, + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 512631961d3..788dd53d11a 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { PanelNotices } from '../../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader'; import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; @@ -71,6 +72,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { seriesLimit: config.panelSeriesLimit, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), @@ -121,6 +125,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane }), ], extendPanelContext: setDashboardPanelContext, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, title: panel.spec.title, options: {}, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 9194adf6a7b..3c334b022c1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -46,6 +46,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; @@ -442,6 +443,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { hoverHeaderOffset: 0, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7188756b055..6c486021e62 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; @@ -283,6 +284,9 @@ export function getDefaultVizPanel(): VizPanel { menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], }), + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ queries: [{ refId: 'A' }], diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6230117bcf2..d9efaadc4d6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11094,6 +11094,12 @@ "could-anything-matching-query": "Could not find anything matching your query" } }, + "panel-group-by": { + "button": "Group by", + "loading": "Loading options", + "no-options": "No options found", + "search-placeholder": "Search" + }, "panel-type-filter": { "clear-button": "Clear types", "select-aria-label": "Panel type filter", From 8227ecb499784dd466e168be28fe87b8179719d3 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Fri, 28 Nov 2025 06:17:59 -0700 Subject: [PATCH 173/423] Dashboard Controls: Display dashboard links on the right side of the toolbar (#114378) * have dashboard links on the right side * Lets try a compromise (#114389) Try to compromise on white space. Having a lot of links will create unessecary empty space under the time controls, but it is necessary if we want to be able to have the links on the right --------- Co-authored-by: Oscar Kilhed Co-authored-by: Sergej-Vlasov --- .../scene/DashboardControls.tsx | 23 +++++++++++++++---- .../scene/DashboardLinksControls.tsx | 23 +++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 66b87804f76..0679ab32f88 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -176,8 +176,13 @@ function DashboardControlsRenderer({ model }: SceneComponentProps
)} - {!hideDashboardControls && model.hasDashboardControls() && } + {!hideDashboardControls && model.hasDashboardControls() && ( +
+ +
+ )} {config.featureToggles.dashboardNewLayouts && } + {!hideLinksControls && !editPanel && }
{!hideVariableControls && ( <> @@ -185,7 +190,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} - {!hideLinksControls && !editPanel && } {editPanel && } {showDebugger && }
@@ -268,17 +272,26 @@ function getStyles(theme: GrafanaTheme2) { }), rightControls: css({ display: 'flex', - justifyContent: 'flex-end', gap: theme.spacing(1), - marginBottom: theme.spacing(1), float: 'right', - alignItems: 'flex-start', + alignItems: 'center', + flexWrap: 'wrap', + maxWidth: '100%', + minWidth: 0, }), timeControls: css({ display: 'flex', justifyContent: 'flex-end', gap: theme.spacing(1), marginBottom: theme.spacing(1), + order: 2, + marginLeft: 'auto', + flexShrink: 0, + alignSelf: 'flex-start', + }), + dashboardControlsButton: css({ + order: 2, + marginLeft: 'auto', }), rightControlsWrap: css({ flexWrap: 'wrap', diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx index 8053055fe81..11639554e5d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx @@ -1,5 +1,9 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; import { sceneGraph } from '@grafana/scenes'; import { DashboardLink } from '@grafana/schema'; +import { useStyles2 } from '@grafana/ui'; import { DashboardLinkRenderer } from './DashboardLinkRenderer'; import { DashboardScene } from './DashboardScene'; @@ -12,18 +16,33 @@ export interface Props { export function DashboardLinksControls({ links, dashboard }: Props) { sceneGraph.getTimeRange(dashboard).useState(); const uid = dashboard.state.uid; + const styles = useStyles2(getStyles); if (!links || !uid) { return null; } return ( - <> +
{links .filter((link) => link.placement === undefined) .map((link: DashboardLink, index: number) => ( ))} - +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + linksContainer: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + maxWidth: '100%', + minWidth: 0, + order: 1, + flex: '1 1 0%', + }), + }; +} From c75137b2d3706adadb382cd5e4c58891601bf2e3 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:18:14 +0000 Subject: [PATCH 174/423] QueryVariableForm: Refil query variable query on default data source update (#114491) * refil qury variable query on default ds update * refactor datasource update logic * adjust test --- .../components/QueryVariableForm.test.tsx | 2 +- .../variables/components/QueryVariableForm.tsx | 16 +++++++++++----- .../editors/QueryVariableEditor.test.tsx | 10 ++++++---- .../variables/editors/QueryVariableEditor.tsx | 4 ++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 63a1912fde7..d3d23396974 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -178,7 +178,7 @@ describe('QueryVariableEditorForm', () => { await userEvent.click(screen.getByText(/prometheus/i)); expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1); - expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); + expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource); }); it('should call onQueryChange when changing the query', async () => { diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 3849fb0a2c5..030de016a5f 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -1,4 +1,4 @@ -import { FormEvent } from 'react'; +import { FormEvent, useCallback } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; @@ -27,7 +27,7 @@ type VariableQueryType = QueryVariable['state']['query']; interface QueryVariableEditorFormProps { datasource?: DataSourceRef; - onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; + onDataSourceChange: (dsSettings: DataSourceInstanceSettings, preserveQuery?: boolean) => void; query: VariableQueryType; onQueryChange: (query: VariableQueryType) => void; onLegacyQueryChange: (query: VariableQueryType, definition: string) => void; @@ -89,17 +89,23 @@ export function QueryVariableEditorForm({ onQueryChange(query); } + // update data source if it is not defined in variable model if (!datasourceRef) { const instanceSettings = getDataSourceSrv().getInstanceSettings({ type: datasource.type, uid: datasource.uid }); - if (instanceSettings) { - onDataSourceChange(instanceSettings); + onDataSourceChange(instanceSettings, true); } } return { datasource, VariableQueryEditor }; }, [datasourceRef]); + // adjusting type miss match between DataSourcePicker onChange and onDataSourceChange + const datasourceChangeHandler = useCallback( + (dsSettings: DataSourceInstanceSettings) => onDataSourceChange(dsSettings), + [onDataSourceChange] + ); + const { datasource, VariableQueryEditor } = dsConfig ?? {}; return ( @@ -111,7 +117,7 @@ export function QueryVariableEditorForm({ label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')} htmlFor="data-source-picker" > - + {datasource && VariableQueryEditor && ( diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 26f9516e40a..99385438e87 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -152,10 +152,12 @@ describe('QueryVariableEditor', () => { const onRunQueryMock = jest.fn(); const variable = new QueryVariable({ datasource: undefined, query: '' }); - await setup({ - variable, - onRunQuery: onRunQueryMock, - }); + await act(() => + setup({ + variable, + onRunQuery: onRunQueryMock, + }) + ); await waitFor(async () => { expect(variable.state.datasource).not.toBe(undefined); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index da1649776b2..106468b2b31 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -68,10 +68,10 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito const onAllowCustomValueChange = (event: FormEvent) => { variable.setState({ allowCustomValue: event.currentTarget.checked }); }; - const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => { + const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings, preserveQuery = false) => { const datasource = getDataSourceRef(dsInstanceSettings); - if ((variable.state.datasource?.type || '') !== datasource.type) { + if (!preserveQuery && (variable.state.datasource?.type || '') !== datasource.type) { variable.setState({ datasource, query: '', definition: '' }); return; } From 930f7ce48991c2e5eb4b2f2e10d2146f446bfd2c Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 28 Nov 2025 14:21:33 +0100 Subject: [PATCH 175/423] Dashboards: Cover the Switch variable in schema transformations - part 2. (#114549) * feat: add v2alpha_1 conversion for the switch variable * chore: gofmt fixes * chore: update comments in tests * chore: fix gofmt * Update specs * tests: update the v2alpha1 openapi snapshot --------- Co-authored-by: Ivan Ortega --- .../kinds/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec_gen.go | 71 +++- .../v2alpha1/zz_generated.openapi.go | 349 ++++++++++++------ ...enerated.openapi_violation_exceptions.list | 17 +- .../input/v1beta1.variable-conversions.json | 26 ++ ...v1beta1.variable-conversions.v0alpha1.json | 26 ++ ...v1beta1.variable-conversions.v2alpha1.json | 13 + .../v1beta1.variable-conversions.v2beta1.json | 13 + .../conversion/v1beta1_to_v2alpha1.go | 73 ++++ .../conversion/v2alpha1_to_v2beta1.go | 16 + .../conversion/v2alpha1_to_v2beta1_test.go | 144 ++++++++ .../conversion/v2beta1_to_v2alpha1.go | 17 + .../conversion/v2beta1_to_v2alpha1_test.go | 45 +++ .../dashboard/v2alpha1/types.spec.gen.ts | 33 +- .../dashboard.grafana.app-v2alpha1.json | 72 +++- 16 files changed, 809 insertions(+), 148 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5bca05229db..5768c358c24 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -710,9 +710,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -970,6 +970,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 76e0921eb88..9a8a621345f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -714,9 +714,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -974,6 +974,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 37d9ae8897f..b83cbbd6fb3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1291,11 +1291,11 @@ func NewDashboardTimeRangeOption() *DashboardTimeRangeOption { } // +k8s:openapi-gen=true -type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind +type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind // NewDashboardVariableKind creates a new DashboardVariableKind object. func NewDashboardVariableKind() *DashboardVariableKind { - return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() + return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() } // Query variable kind @@ -1814,6 +1814,45 @@ func NewDashboardMetricFindValue() *DashboardMetricFindValue { return &DashboardMetricFindValue{} } +// +k8s:openapi-gen=true +type DashboardSwitchVariableKind struct { + Kind string `json:"kind"` + Spec DashboardSwitchVariableSpec `json:"spec"` +} + +// NewDashboardSwitchVariableKind creates a new DashboardSwitchVariableKind object. +func NewDashboardSwitchVariableKind() *DashboardSwitchVariableKind { + return &DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: *NewDashboardSwitchVariableSpec(), + } +} + +// Switch variable specification +// +k8s:openapi-gen=true +type DashboardSwitchVariableSpec struct { + Name string `json:"name"` + Current string `json:"current"` + EnabledValue string `json:"enabledValue"` + DisabledValue string `json:"disabledValue"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardSwitchVariableSpec creates a new DashboardSwitchVariableSpec object. +func NewDashboardSwitchVariableSpec() *DashboardSwitchVariableSpec { + return &DashboardSwitchVariableSpec{ + Name: "", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + // +k8s:openapi-gen=true type DashboardSpec struct { Annotations []DashboardAnnotationQueryKind `json:"annotations"` @@ -2404,7 +2443,7 @@ func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTab } // +k8s:openapi-gen=true -type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind struct { +type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind struct { QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"` TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"` ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"` @@ -2413,15 +2452,16 @@ type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasou CustomVariableKind *DashboardCustomVariableKind `json:"CustomVariableKind,omitempty"` GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"` AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"` + SwitchVariableKind *DashboardSwitchVariableKind `json:"SwitchVariableKind,omitempty"` } -// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind object. -func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind { - return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind{} +// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind object. +func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind { + return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind{} } -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` as JSON. -func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) MarshalJSON() ([]byte, error) { +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` as JSON. +func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) MarshalJSON() ([]byte, error) { if resource.QueryVariableKind != nil { return json.Marshal(resource.QueryVariableKind) } @@ -2446,12 +2486,15 @@ func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKin if resource.AdhocVariableKind != nil { return json.Marshal(resource.AdhocVariableKind) } + if resource.SwitchVariableKind != nil { + return json.Marshal(resource.SwitchVariableKind) + } return []byte("null"), nil } -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` from JSON. -func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) UnmarshalJSON(raw []byte) error { +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` from JSON. +func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) UnmarshalJSON(raw []byte) error { if raw == nil { return nil } @@ -2524,6 +2567,14 @@ func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKi resource.QueryVariableKind = &dashboardQueryVariableKind return nil + case "SwitchVariable": + var dashboardSwitchVariableKind DashboardSwitchVariableKind + if err := json.Unmarshal(raw, &dashboardSwitchVariableKind); err != nil { + return err + } + + resource.SwitchVariableKind = &dashboardSwitchVariableKind + return nil case "TextVariable": var dashboardTextVariableKind DashboardTextVariableKind if err := json.Unmarshal(raw, &dashboardTextVariableKind); err != nil { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index a167aa6d36e..f595b8a3040 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -14,125 +14,127 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), } } @@ -3457,7 +3459,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref common.Re } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3503,11 +3505,16 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariable Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind"), }, }, + "SwitchVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind"), + }, + }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, } } @@ -4064,7 +4071,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind"), }, }, }, @@ -4075,7 +4082,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, } } @@ -4184,6 +4191,102 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref common.Refe } } +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Switch variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "enabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "enabledValue", "disabledValue", "hide", "skipUrlSync"}, + }, + }, + } +} + func schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index 25c9bf58f1c..31ac411ad28 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -55,14 +55,15 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardIntervalVariableSpec,AutoMin API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,PanelKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,AdhocVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,ConstantVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,CustomVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,DatasourceVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,GroupByVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,IntervalVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,QueryVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,SwitchVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,TextVariableKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,ArrayOfString API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,String API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrFloat64,Float64 diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json index a8d22b09eee..ae1ef7cd04e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -258,6 +258,32 @@ "multi": true, "skipUrlSync": false }, + { + "name": "switch_var", + "type": "switch", + "label": "Enable Feature", + "description": "Toggle feature on/off", + "query": "", + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "hide": 0, + "skipUrlSync": false + }, { "name": "legacy_string_var", "type": "query", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index e4a5b7b74eb..4e12e6982ef 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -277,6 +277,32 @@ "skipUrlSync": false, "type": "groupby" }, + { + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "description": "Toggle feature on/off", + "hide": 0, + "label": "Enable Feature", + "name": "switch_var", + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "query": "", + "skipUrlSync": false, + "type": "switch" + }, { "current": { "selected": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index a4740089365..c7c6c646a94 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -317,6 +317,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index d35e57fa601..7b1a899d7fa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -318,6 +318,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index d20855a9509..7ef4d6ed475 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -956,6 +956,10 @@ func transformVariables(ctx context.Context, dashboard map[string]interface{}, d if textVar, err := buildTextVariable(varMap, commonProps); err == nil { variables = append(variables, textVar) } + case "switch": + if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil { + variables = append(variables, switchVar) + } case "groupby": if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil { variables = append(variables, groupByVar) @@ -1415,6 +1419,75 @@ func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariable }, nil } +// Helper function to extract string value from an option map (value or text field) +func getOptionValue(optMap map[string]interface{}) string { + if val, ok := optMap["value"].(string); ok && val != "" { + return val + } + if val, ok := optMap["text"].(string); ok && val != "" { + return val + } + return "" +} + +// Switch Variable +func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + current := "" + if currentVal, exists := varMap["current"]; exists { + if currentMap, ok := currentVal.(map[string]interface{}); ok { + current = getOptionValue(currentMap) + } + } + + // In V1 the enabled value is the first value of the options array, + // while the disabled value is second one. + // (Falling back to "true" and "false" if options are not available) + enabledValue := "true" + disabledValue := "false" + + if options, ok := varMap["options"].([]interface{}); ok { + // Get enabledValue from first option + if len(options) > 0 { + if opt1, ok := options[0].(map[string]interface{}); ok { + if val := getOptionValue(opt1); val != "" { + enabledValue = val + } + } + } + // Get disabledValue from second option + if len(options) > 1 { + if opt2, ok := options[1].(map[string]interface{}); ok { + if val := getOptionValue(opt2); val != "" { + disabledValue = val + } + } + } + } + + // Set current to disabledValue if not set + if current == "" { + current = disabledValue + } + + switchVar := &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: commonProps.Name, + Current: current, + EnabledValue: enabledValue, + DisabledValue: disabledValue, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + }, + } + + return dashv2alpha1.DashboardVariableKind{ + SwitchVariableKind: switchVar, + }, nil +} + // Adhoc Variable func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) { datasource := varMap["datasource"] diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index e5bd1f834d3..b09198c72e2 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -733,6 +733,22 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2beta1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go new file mode 100644 index 00000000000..3451234cdec --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -0,0 +1,144 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" +) + +// TestV2alpha1ToV2beta1 tests the conversion logic for v2alpha1 to v2beta1. +func TestV2alpha1ToV2beta1(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + testCases := []struct { + name string + createV2alpha1 func() *dashv2alpha1.Dashboard + validateV2beta1 func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) + }{ + { + name: "dashboard with switch variable", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2beta1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + { + name: "dashboard with switch variable - custom values for enabled and disable states", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "true", + EnabledValue: "enabled", + DisabledValue: "disabled", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideHideLabel, + SkipUrlSync: true, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "enabled", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "disabled", variable.SwitchVariableKind.Spec.DisabledValue) + assert.Equal(t, dashv2beta1.DashboardVariableHideHideLabel, variable.SwitchVariableKind.Spec.Hide) + assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create v2alpha1 dashboard + v2alpha1 := tc.createV2alpha1() + + // Collect original statistics + originalStats := collectStatsV2alpha1(v2alpha1.Spec) + + // Convert to v2beta1 + var v2beta1 dashv2beta1.Dashboard + err := scheme.Convert(v2alpha1, &v2beta1, nil) + require.NoError(t, err, "Failed to convert v2alpha1 to v2beta1") + + // Collect v2beta1 statistics + v2beta1Stats := collectStatsV2beta1(v2beta1.Spec) + + // Verify no data loss + err = detectConversionDataLoss(originalStats, v2beta1Stats, "V2alpha1", "V2beta1") + assert.NoError(t, err, "Data loss detected in conversion") + + // Run custom validation + tc.validateV2beta1(t, &v2beta1) + }) + } +} diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 580fef1b3ea..f0f06957b63 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -771,6 +771,23 @@ func convertVariable_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + return nil + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 773961a31fb..25703b56aa7 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -479,6 +479,51 @@ func TestV2beta1ToV2alpha1(t *testing.T) { assert.Equal(t, "", variable.QueryVariableKind.Spec.Query.Kind, "Empty group should result in empty kind") }, }, + { + name: "dashboard with switch variable", + createV2beta1: func() *dashv2beta1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2beta1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2beta1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2beta1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Variables, 1) + variable := v2alpha1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2alpha1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, { name: "dashboard with rows layout", createV2beta1: func() *dashv2beta1.Dashboard { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 3cb2c8aab5b..060a0e3945f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1050,7 +1050,7 @@ export const defaultTimeRangeOption = (): TimeRangeOption => ({ to: "now", }); -export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; +export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind; export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); @@ -1436,6 +1436,37 @@ export const defaultMetricFindValue = (): MetricFindValue => ({ text: "", }); +export interface SwitchVariableKind { + kind: "SwitchVariable"; + spec: SwitchVariableSpec; +} + +export const defaultSwitchVariableKind = (): SwitchVariableKind => ({ + kind: "SwitchVariable", + spec: defaultSwitchVariableSpec(), +}); + +// Switch variable specification +export interface SwitchVariableSpec { + name: string; + current: string; + enabledValue: string; + disabledValue: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultSwitchVariableSpec = (): SwitchVariableSpec => ({ + name: "", + current: "false", + enabledValue: "true", + disabledValue: "false", + hide: "dontHide", + skipUrlSync: false, +}); + export interface Spec { annotations: AnnotationQueryKind[]; // Configuration of dashboard cursor sync behavior. diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 90e70f63baf..1d5fff1367b 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3100,7 +3100,7 @@ } } }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": { "type": "object", "properties": { "AdhocVariableKind": { @@ -3124,6 +3124,9 @@ "QueryVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" }, + "SwitchVariableKind": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" + }, "TextVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" } @@ -3520,7 +3523,7 @@ "description": "Configured template variables.", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind" } } } @@ -3587,6 +3590,71 @@ } } }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string", + "default": "" + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec": { + "description": "Switch variable specification", + "type": "object", + "required": [ + "name", + "current", + "enabledValue", + "disabledValue", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "type": "string", + "default": "" + }, + "description": { + "type": "string" + }, + "disabledValue": { + "type": "string", + "default": "" + }, + "enabledValue": { + "type": "string", + "default": "" + }, + "hide": { + "type": "string", + "default": "" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + } + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { "type": "object", "required": [ From ce72dc6224329ca2b95730ac71abc859d609f89d Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 28 Nov 2025 15:41:19 +0200 Subject: [PATCH 176/423] API clients: Remove duplicate config entries (#114569) --- packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts index 776ad4131ae..06e6f5739b8 100644 --- a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -108,8 +108,6 @@ const config: ConfigFile = { ...createAPIConfig('preferences', 'v1alpha1'), ...createAPIConfig('provisioning', 'v0alpha1'), ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), // PLOP_INJECT_API_CLIENT - Used by the API client generator }, }; From b5eac7baadbbb5b9a17574d0d69805d9a22dfe00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 28 Nov 2025 15:07:15 +0100 Subject: [PATCH 177/423] Sidebar: Clickout side to close pane (#114523) * Sidebar: Clickout side to close pane * fixes * fixing tests * fix * ignore clicks in portals * fix e2e * fix test * Fix click away issues * add missing file --- e2e-playwright/dashboard-new-layouts/utils.ts | 1 + .../src/selectors/components.ts | 3 ++ .../src/components/Sidebar/Sidebar.story.tsx | 7 ++-- .../src/components/Sidebar/Sidebar.test.tsx | 3 +- .../src/components/Sidebar/Sidebar.tsx | 18 +++++++++-- .../components/Sidebar/SidebarPaneHeader.tsx | 16 +++++++--- .../src/components/Sidebar/useSidebar.tsx | 6 ++++ .../components/Sidebar/useSidebarClickAway.ts | 32 +++++++++++++++++++ .../DashboardEditPaneRenderer.test.tsx | 4 +-- .../edit-pane/DashboardEditPaneSplitter.tsx | 1 + .../edit-pane/DashboardOutline.test.tsx | 12 +++++-- .../edit-pane/DashboardOutline.tsx | 5 +-- .../edit-pane/EditPaneHeader.test.tsx | 19 +++++++++-- .../edit-pane/EditPaneHeader.tsx | 2 +- 14 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 89f4de0eec3..83508063ef5 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -50,6 +50,7 @@ export const flows = { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.dockToggle).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) .click(); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 36d0088b258..c643a0d7f2c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -61,6 +61,9 @@ export const versionedComponents = { closePane: { '12.4.0': 'data-testid Sidebar close pane', }, + dockToggle: { + '12.4.0': 'data-testid sidebar-dock-toggle', + }, }, EditPaneHeader: { deleteButton: { diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx index 8590f91a8b4..175e792d451 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx @@ -62,6 +62,7 @@ export const Example: StoryFn = (args) => { position: args.position, bottomMargin: 0, edgeMargin: 0, + onClosePane: () => setOpenPane(''), }); return ( @@ -79,7 +80,7 @@ export const Example: StoryFn = (args) => { {openPane === 'settings' && ( - togglePane('')}> + @@ -88,12 +89,12 @@ export const Example: StoryFn = (args) => { )} {openPane === 'outline' && ( - togglePane('')} /> + )} {openPane === 'add' && ( - togglePane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx index 187fbc90c4b..5e5116b3ada 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx @@ -30,6 +30,7 @@ function TestSetup() { const contextValue = useSidebar({ position: 'right', hasOpenPane: openPane !== '', + onClosePane: () => setOpenPane(''), }); return ( @@ -37,7 +38,7 @@ function TestSetup() { {openPane === 'settings' && ( - setOpenPane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx index 8c1bf784e53..311e11f390c 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -2,14 +2,17 @@ import { css, cx } from '@emotion/css'; import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { getPortalContainer } from '../Portal/Portal'; import { SidebarButton } from './SidebarButton'; import { SidebarPaneHeader } from './SidebarPaneHeader'; import { SidebarResizer } from './SidebarResizer'; import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar'; +import { useCustomClickAway } from './useSidebarClickAway'; export interface Props { children?: ReactNode; @@ -30,9 +33,20 @@ export function SidebarComp({ children, contextValue }: Props) { const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) }; + const ref = useCustomClickAway((evt) => { + const portalContainer = getPortalContainer(); + // ignore clicks inside portal container + if (evt.target instanceof Node && portalContainer && portalContainer.contains(evt.target)) { + return; + } + if (!isDocked && hasOpenPane) { + contextValue.onClosePane?.(); + } + }); + return ( -
+
{!tabsMode && } {children}
@@ -61,7 +75,7 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) { icon={'web-section-alt'} onClick={context.onToggleDock} title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')} - data-testid="sidebar-dock-toggle" + data-testid={selectors.components.Sidebar.dockToggle} /> )}
diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx index 42fe30b083c..e1e5e0bca59 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { ReactNode } from 'react'; +import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -9,23 +9,29 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { IconButton } from '../IconButton/IconButton'; import { Text } from '../Text/Text'; +import { SidebarContext } from './useSidebar'; + export interface Props { children?: ReactNode; title: string; - onClose?: () => void; } -export function SidebarPaneHeader({ children, onClose, title }: Props) { +export function SidebarPaneHeader({ children, title }: Props) { const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('SidebarPaneHeader must be used within a Sidebar'); + } return (
- {onClose && ( + {context.onClosePane && ( void; onResize: (diff: number) => void; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SidebarContext: React.Context = React.createContext< @@ -35,6 +37,8 @@ export interface UseSideBarOptions { edgeMargin?: number; /** defaults to 2 grid units (16px) */ contentMargin?: number; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SIDE_BAR_WIDTH_ICON_ONLY = 5; @@ -48,6 +52,7 @@ export function useSidebar({ bottomMargin = 2, edgeMargin = 2, contentMargin = 2, + onClosePane, }: UseSideBarOptions): SidebarContextValue { const theme = useTheme2(); const [isDocked, setIsDocked] = React.useState(false); @@ -109,5 +114,6 @@ export function useSidebar({ edgeMargin, bottomMargin, contentMargin, + onClosePane, }; } diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts new file mode 100644 index 00000000000..5f1f2ccfb89 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts @@ -0,0 +1,32 @@ +import React from 'react'; + +/** + * Cannot use the react-use useClickAway directly as it relies on mousedown event which is not ideal as the element selection uses pointerdown + * @param ref + * @param onClickAway + */ +export function useCustomClickAway(onClickAway: (evt: MouseEvent | TouchEvent) => void) { + const ref = React.useRef(null); + const refCb = React.useRef(onClickAway); + + React.useLayoutEffect(() => { + refCb.current = onClickAway; + }); + + React.useEffect(() => { + const handler = (e: MouseEvent | TouchEvent) => { + const element = ref.current; + if (element && e.target instanceof Node && !element.contains(e.target)) { + refCb.current(e); + } + }; + + document.addEventListener('pointerdown', handler); + + return () => { + document.removeEventListener('pointerdown', handler); + }; + }, []); + + return ref; +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx index 41c81222661..d7f5c73280d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx @@ -63,9 +63,9 @@ describe('DashboardEditPaneRenderer', () => { act(() => screen.getByLabelText('Outline').click()); - expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.components.Sidebar.dockToggle)).toBeInTheDocument(); - act(() => screen.getByTestId('sidebar-dock-toggle').click()); + act(() => screen.getByTestId(selectors.components.Sidebar.dockToggle).click()); expect(scene.state.editPane.state.isDocked).toBe(true); }); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index ce7df7a460b..59af972e3b0 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls hasOpenPane: Boolean(openPane), contentMargin: 1, position: 'right', + onClosePane: () => editPane.closePane(), }); /** diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx index 7b461551773..f743f0a3ae7 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx @@ -5,7 +5,7 @@ import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; import { setPluginImportUtils } from '@grafana/runtime'; import { SceneVariableSet, VizPanel } from '@grafana/scenes'; -import { ElementSelectionContext } from '@grafana/ui'; +import { ElementSelectionContext, Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; @@ -86,6 +86,12 @@ function buildTestScene() { return testScene; } +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('DashboardOutline', () => { afterEach(() => { jest.clearAllMocks(); @@ -101,7 +107,9 @@ describe('DashboardOutline', () => { render( - + + + ); // select Row lvl 1 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 97e17c34780..de80db73254 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -25,10 +25,7 @@ export function DashboardOutline({ editPane, isEditing }: Props) { return ( <> - editPane.closePane()} - /> + diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx index 627ceb96d3b..e3dc190a073 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { selectors } from '@grafana/e2e-selectors'; import { SceneTimeRange } from '@grafana/scenes'; +import { Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { RowItem } from '../scene/layout-rows/RowItem'; @@ -53,6 +54,12 @@ const buildTestScene = (scene: DashboardScene) => { return scene; }; +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('EditPaneHeader', () => { const mockEditPane = { state: { selection: null }, @@ -71,7 +78,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['row-test', row.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled(); @@ -84,7 +95,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled(); diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index ea14464fc64..2f096293171 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -30,7 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { }; return ( - editPane.closePane()}> + {element.renderActions && element.renderActions()} {(onCopy || onDuplicate) && ( From 0856cac9ad164b34f3418d0d8018fb876ec20fa4 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:25:28 +0000 Subject: [PATCH 178/423] QueryEditorRows: Clear hideSeriesFrom override on query edit (#114315) clear hideSeriesFrom override on query edit --- .../PanelDataPane/PanelDataQueriesTab.tsx | 1 + .../query/components/QueryEditorRows.tsx | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index cea1dfc6cb0..d70fbf9c9c4 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -403,6 +403,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 2cbdc1aba87..e0dacee2669 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -9,8 +9,10 @@ import { HistoryItem, PanelData, getDataSourceRef, + isSystemOverrideWithRef, } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -44,6 +46,7 @@ export interface Props { queryLibraryRef?: string; onCancelQueryLibraryEdit?: () => void; isOpen?: boolean; + panelRef?: SceneObjectRef; } export class QueryEditorRows extends PureComponent { @@ -63,6 +66,20 @@ export class QueryEditorRows extends PureComponent { return item; }) ); + + if (this.props.panelRef) { + const panel = this.props.panelRef.resolve(); + const hideSeriesOverrideIndex = panel.state.fieldConfig.overrides.findIndex( + isSystemOverrideWithRef('hideSeriesFrom') + ); + + if (hideSeriesOverrideIndex !== -1) { + const newOverrides = [...panel.state.fieldConfig.overrides]; + newOverrides.splice(hideSeriesOverrideIndex, 1); + + panel.setState({ fieldConfig: { ...panel.state.fieldConfig, overrides: newOverrides } }); + } + } } onReplaceQuery(query: DataQuery, index: number) { From a2e34d229667b0a857b9a0c192cc7edcef0e06b8 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:13:02 +0100 Subject: [PATCH 179/423] Alerting: Fetch only silenced alerts on the Silences page (#114564) --- .../alerting/unified/Silences.test.tsx | 23 +++++++++++++++++++ .../components/silences/SilencesTable.tsx | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 4ff786b2bc4..e5bce1433f2 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,3 +1,4 @@ +import { HttpResponse, http } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; @@ -154,6 +155,28 @@ describe('Silences', () => { TEST_TIMEOUT ); + it( + 'fetches silenced alerts with correct filter parameters', + async () => { + let capturedParams: URLSearchParams | undefined; + server.use( + http.get('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ request }) => { + capturedParams = new URL(request.url).searchParams; + return HttpResponse.json([]); + }) + ); + + renderSilences(); + + await waitFor(() => expect(capturedParams).toBeDefined()); + + expect(capturedParams?.get('silenced')).toBe('true'); + expect(capturedParams?.get('active')).toBe('false'); + expect(capturedParams?.get('inhibited')).toBe('false'); + }, + TEST_TIMEOUT + ); + it( 'shows the correct number of silenced alerts', async () => { diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 8d3c80acad1..a06acda7c35 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -56,7 +56,7 @@ const SilencesTable = () => { const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( - { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, + { amSourceName: alertManagerSourceName, filter: { silenced: true, active: false, inhibited: false } }, { ...API_QUERY_OPTIONS, skip: !canPreview } ); From a5d00d6264b8c83a56b898258957a13c62063bde Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Fri, 28 Nov 2025 16:17:38 +0100 Subject: [PATCH 180/423] Alerting: Fix make update-workspace for historian app (#114578) --- apps/iam/go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/iam/go.mod b/apps/iam/go.mod index ad6c6039cc5..894e28c7a61 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -20,6 +20,8 @@ replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/no replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules +replace github.com/grafana/grafana/apps/alerting/historian => ../alerting/historian + replace github.com/grafana/grafana/apps/correlations => ../correlations replace github.com/grafana/grafana/apps/investigations => ../investigations From e53236e9cdbe315beee75146a5df0fb16ab1325e Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:53:00 +0100 Subject: [PATCH 181/423] Alerting: Add historian app to go.work (#114591) --- Dockerfile | 1 + apps/alerting/historian/go.mod | 29 ++++++++------ apps/alerting/historian/go.sum | 70 +++++++++++++++++----------------- go.work | 1 + 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/Dockerfile b/Dockerfile index c07eb0360e6..5bcca3643b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,7 @@ COPY apps/iam apps/iam COPY apps apps COPY kindsv2 kindsv2 COPY apps/alerting/alertenrichment apps/alerting/alertenrichment +COPY apps/alerting/historian apps/alerting/historian COPY apps/alerting/notifications apps/alerting/notifications COPY apps/alerting/rules apps/alerting/rules COPY pkg/codegen pkg/codegen diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index eec687eb79f..234cca1eabe 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) require ( @@ -15,16 +15,20 @@ require ( 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.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect @@ -32,20 +36,23 @@ require ( 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.7.7 // 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.66.1 // 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/rogpeppe/go-internal v1.14.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 @@ -58,20 +65,20 @@ require ( 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.2 // 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.30.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.9.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-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.8 // 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 diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index a0f2396b250..7eadc30e628 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 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 v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +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= @@ -23,16 +23,18 @@ 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.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +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.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +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= @@ -42,8 +44,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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= @@ -71,8 +73,8 @@ 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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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= @@ -87,10 +89,10 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J 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.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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= @@ -102,14 +104,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h 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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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= @@ -148,8 +150,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV 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.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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= @@ -163,8 +165,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL 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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +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= @@ -181,8 +183,8 @@ 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.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= @@ -197,14 +199,14 @@ 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-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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= @@ -224,8 +226,8 @@ 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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +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= diff --git a/go.work b/go.work index f0f5d9acc19..a33973ac23f 100644 --- a/go.work +++ b/go.work @@ -7,6 +7,7 @@ use ( . // skip:golangci-lint ./apps/advisor ./apps/alerting/alertenrichment + ./apps/alerting/historian ./apps/alerting/notifications ./apps/alerting/rules ./apps/annotation From 32c1ad1b53cce639732f30d257de3db62b4165f7 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 18:20:57 +0100 Subject: [PATCH 182/423] chore: update grafana-app-sdk to v0.48.4 (#114563) (chore): update grafana-app-sdk to v0.48.4 --- apps/advisor/go.mod | 18 ++++---- apps/advisor/go.sum | 60 +++++++++++++-------------- apps/alerting/alertenrichment/go.mod | 4 +- apps/alerting/alertenrichment/go.sum | 8 ++-- apps/alerting/historian/go.mod | 15 ++++--- apps/alerting/historian/go.sum | 28 ++++++------- apps/alerting/notifications/go.mod | 17 ++++---- apps/alerting/notifications/go.sum | 28 ++++++------- apps/alerting/rules/go.mod | 15 ++++--- apps/alerting/rules/go.sum | 28 ++++++------- apps/annotation/go.mod | 15 ++++--- apps/annotation/go.sum | 28 ++++++------- apps/collections/go.mod | 6 +-- apps/collections/go.sum | 12 +++--- apps/correlations/go.mod | 15 ++++--- apps/correlations/go.sum | 28 ++++++------- apps/dashboard/go.mod | 17 ++++---- apps/dashboard/go.sum | 34 +++++++-------- apps/example/go.mod | 17 ++++---- apps/example/go.sum | 34 +++++++-------- apps/folder/go.mod | 6 +-- apps/folder/go.sum | 12 +++--- apps/iam/go.mod | 29 +++++++------ apps/iam/go.sum | 62 ++++++++++++++-------------- apps/investigations/go.mod | 15 ++++--- apps/investigations/go.sum | 28 ++++++------- apps/logsdrilldown/go.mod | 15 ++++--- apps/logsdrilldown/go.sum | 28 ++++++------- apps/playlist/go.mod | 15 ++++--- apps/playlist/go.sum | 28 ++++++------- apps/plugins/go.mod | 16 +++---- apps/plugins/go.sum | 32 +++++++------- apps/preferences/go.mod | 6 +-- apps/preferences/go.sum | 12 +++--- apps/provisioning/go.mod | 14 +++---- apps/provisioning/go.sum | 28 ++++++------- apps/scope/go.mod | 2 +- apps/scope/go.sum | 4 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 11 +++-- apps/secret/go.sum | 24 +++++------ apps/shorturl/go.mod | 17 ++++---- apps/shorturl/go.sum | 34 +++++++-------- go.mod | 29 +++++++------ go.sum | 62 ++++++++++++++-------------- go.work.sum | 9 ++++ pkg/aggregator/go.mod | 10 ++--- pkg/aggregator/go.sum | 20 ++++----- pkg/apimachinery/go.mod | 12 +++--- pkg/apimachinery/go.sum | 22 +++++----- pkg/apiserver/go.mod | 14 +++---- pkg/apiserver/go.sum | 28 ++++++------- pkg/build/go.mod | 8 ++-- pkg/build/go.sum | 16 +++---- pkg/promlib/go.mod | 10 ++--- pkg/promlib/go.sum | 24 +++++------ 56 files changed, 554 insertions(+), 577 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 6e944d56c3b..2db5b66ac8c 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,8 +8,8 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 @@ -43,7 +43,7 @@ replace github.com/grafana/grafana/apps/plugins => ../plugins replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect @@ -104,7 +104,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -256,7 +256,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -290,9 +290,9 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -314,7 +314,7 @@ require ( modernc.org/sqlite v1.39.1 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 61d3d00b8c2..eb864bd4834 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -49,8 +49,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -107,8 +107,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= @@ -266,8 +266,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= @@ -346,9 +346,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= @@ -384,8 +384,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -616,10 +616,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -1074,8 +1074,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1134,8 +1134,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1160,12 +1158,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -1707,10 +1705,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -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/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/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-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.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1742,8 +1740,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1863,8 +1861,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index c717b9bf364..7a015ce4580 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -36,5 +36,5 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index fc4692a07a9..ba98672f90c 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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/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/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 234cca1eabe..934f9ac5772 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/historian go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -30,7 +30,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -52,11 +52,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -75,9 +74,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 24c17cc900a..c0ea062e70b 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -37,7 +37,6 @@ require ( 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/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -62,14 +61,13 @@ require ( 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/rogpeppe/go-internal v1.14.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.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -84,7 +82,6 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // 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 @@ -93,9 +90,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -109,6 +106,6 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 9b84e11f3d6..7a8ccd1d4e8 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,10 +71,10 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -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/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 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/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/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -187,8 +187,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -279,13 +279,13 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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/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/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-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.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -324,7 +324,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 17aa567f077..dceeb576100 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -52,11 +52,10 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -75,9 +74,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 988575c7ce1..5173ad8c627 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/annotation go 1.24.0 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + 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 ) @@ -52,11 +52,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -75,9 +74,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 281a6579c67..621440d3bfe 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -25,7 +25,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -64,6 +64,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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 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/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/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 988f18b89d6..42072c1cb2b 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/correlations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + 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 ) @@ -52,11 +52,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -75,9 +74,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index e5a1352e7d8..56d8b7715a1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,8 +5,8 @@ go 1.25.3 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 @@ -30,7 +30,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // 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 @@ -92,7 +92,7 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -106,7 +106,6 @@ require ( 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/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect @@ -119,9 +118,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -130,6 +129,6 @@ require ( k8s.io/klog/v2 v2.130.1 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 135eec59afc..17fbe2222d2 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -39,8 +39,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa 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-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -85,10 +85,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -239,8 +239,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -280,8 +280,6 @@ 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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -337,12 +335,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -375,7 +373,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/example/go.mod b/apps/example/go.mod index 66b3b93f6df..61574462d8a 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/example go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -21,7 +21,7 @@ require ( 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-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // 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 @@ -62,7 +62,7 @@ require ( 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.1.0 // 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 @@ -73,7 +73,6 @@ require ( 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/crypto v0.45.0 // 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 @@ -82,9 +81,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -95,6 +94,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/example/go.sum b/apps/example/go.sum index aaf74c34d44..3211e083601 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa 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-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe/go.mod h1:zn/yoxKpWA2KUsxOhQbSbL8OCkF2JNLpSEHs+hQYvdM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -169,8 +169,6 @@ 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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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= @@ -213,12 +211,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 1c9249745a9..4e336935537 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -64,6 +64,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 944e0d69721..ab70319f432 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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 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/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/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 894e28c7a61..0821652b3d8 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,8 +52,8 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/prometheus/client_golang v1.23.2 @@ -68,7 +68,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.55.0 // indirect @@ -84,7 +84,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect @@ -139,7 +139,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -159,7 +159,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -169,7 +169,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -362,7 +362,7 @@ require ( github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect @@ -375,15 +375,14 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect @@ -432,9 +431,9 @@ require ( gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/api v0.242.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -461,7 +460,7 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c91c571fa7d..5bab810e8d0 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -53,8 +53,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -153,8 +153,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -405,8 +405,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -519,10 +519,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -565,8 +565,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -833,10 +833,10 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -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/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 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/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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -1452,8 +1452,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= @@ -1543,8 +1543,6 @@ github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1577,8 +1575,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= @@ -1587,8 +1585,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -2153,10 +2151,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -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/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/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-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.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2190,8 +2188,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2315,8 +2313,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index d4f3685a2aa..6741017390f 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -31,7 +31,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -53,11 +53,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -76,9 +75,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 712d99405d0..f7d81af4b9d 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + 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 ) @@ -54,11 +54,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -77,9 +76,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -90,6 +89,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 8a254e980d1..bdc831e87ba 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 @@ -32,7 +32,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -54,11 +54,10 @@ require ( 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/rogpeppe/go-internal v1.14.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.1.0 // 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 @@ -77,9 +76,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -88,6 +87,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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/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 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/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/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= @@ -128,8 +128,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -199,12 +199,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index ef1b58517a3..cebf6594566 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -11,8 +11,8 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -46,7 +46,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -146,7 +146,7 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -175,9 +175,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/ini.v1 v1.67.0 // indirect @@ -190,6 +190,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 06683624e65..de829f9be57 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -71,8 +71,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa 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-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -161,10 +161,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -412,8 +412,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -536,12 +536,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB 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/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -594,7 +594,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index cd702672de6..18bc0d81e19 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -64,6 +64,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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 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/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/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 0fb19b230c8..65293ff0f33 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/google/uuid v1.6.0 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/grafana/nanogit v0.3.0 @@ -18,7 +18,7 @@ require ( k8s.io/apiserver v0.34.2 k8s.io/client-go v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -29,7 +29,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // 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 @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.2 // indirect + github.com/grafana/grafana-app-sdk v0.48.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -62,7 +62,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect @@ -75,8 +75,8 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // 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/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index db6bd3cf414..76238334303 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -16,8 +16,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -62,10 +62,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= @@ -133,8 +133,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -224,10 +224,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -260,7 +260,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/scope/go.mod b/apps/scope/go.mod index 9b739eb3d0b..c7d21d96f4a 100644 --- a/apps/scope/go.mod +++ b/apps/scope/go.mod @@ -39,5 +39,5 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/scope/go.sum b/apps/scope/go.sum index 1796d287d60..3e1f8bc7d54 100644 --- a/apps/scope/go.sum +++ b/apps/scope/go.sum @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/sdk.mk b/apps/sdk.mk index 6956168a7df..62d89ed8ed3 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.2 +APP_SDK_VERSION = v0.48.4 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 2ac03ea3f8b..d8de93c5cff 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/grafana/apps/secret go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -29,7 +29,7 @@ require ( 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/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // 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 @@ -51,7 +51,6 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.47.0 // indirect @@ -60,13 +59,13 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 74024f8df34..a18c6f39a89 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,10 +37,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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 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/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/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -111,8 +111,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -168,10 +168,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -199,7 +199,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 740aea32a72..eb9936ab643 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -22,7 +22,7 @@ require ( 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-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // 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 @@ -63,7 +63,7 @@ require ( 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.1.0 // 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 @@ -74,7 +74,6 @@ require ( 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/crypto v0.45.0 // 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 @@ -83,9 +82,9 @@ require ( 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-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 @@ -95,6 +94,6 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index f8b394c0f37..4508fbd82a6 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa 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-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -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 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/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/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba/go.mod h1:rlJ/mmE0RQolOB2+HV3+bw+ZifHyPDQurBwZEus+Wm0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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= @@ -169,8 +169,6 @@ 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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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= @@ -213,12 +211,12 @@ 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/go.mod b/go.mod index 3d277125a1f..371cd742c72 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage github.com/getkin/kin-openapi v0.133.0 // @grafana/grafana-app-platform-squad - github.com/go-jose/go-jose/v4 v4.1.2 // @grafana/identity-access-team + github.com/go-jose/go-jose/v4 v4.1.3 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team github.com/go-logfmt/logfmt v0.6.1 // @grafana/oss-big-tent @@ -97,8 +97,8 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.48.2 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.48.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad @@ -211,7 +211,7 @@ require ( golang.org/x/tools v0.39.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent google.golang.org/api v0.242.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -227,7 +227,7 @@ require ( modernc.org/sqlite v1.39.1 // @grafana/grafana-backend-group pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // @grafana/grafana-app-platform-squad + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // @grafana/grafana-app-platform-squad xorm.io/builder v0.3.13 // @grafana/grafana-backend-group ) @@ -298,7 +298,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect @@ -316,7 +316,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/FZambia/eagle v0.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -391,7 +391,7 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -414,7 +414,7 @@ require ( github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -583,7 +583,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -601,18 +601,17 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.16.3 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/bbolt v1.4.2 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector/featuregate v1.44.0 // indirect go.opentelemetry.io/collector/semconv v0.124.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect @@ -638,8 +637,8 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // 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 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index 971b7b06f74..ff568126e42 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -725,8 +725,8 @@ github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -1040,8 +1040,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -1169,10 +1169,10 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -1232,8 +1232,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1633,10 +1633,10 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -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/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 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/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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -2454,8 +2454,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= @@ -2589,8 +2589,6 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -2627,8 +2625,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= @@ -2640,8 +2638,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -3517,15 +3515,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -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/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -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.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3572,8 +3570,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -3763,8 +3761,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh 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 v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/go.work.sum b/go.work.sum index c4b732eaadb..3bc86d10781 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,6 +72,7 @@ cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36Geo cloud.google.com/go/compute v1.40.0 h1:dlEzKo/BtyEGNc+SflXwwoBh52dNl/A5BaSYurT0k0k= cloud.google.com/go/compute v1.40.0/go.mod h1:P1doTJnlwurJDzIQFMp4mgU+vyCe9HU2NWTlqTfq3MY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w= @@ -1535,6 +1536,8 @@ github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -1917,6 +1920,7 @@ golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1932,6 +1936,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2059,6 +2064,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= 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-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= @@ -2066,6 +2072,7 @@ google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFL google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= @@ -2079,6 +2086,8 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr 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= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 24f0d25efae..264fb259202 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -101,7 +101,7 @@ require ( go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -130,9 +130,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 313c0b7c200..1c52723a0ac 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -269,8 +269,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -382,13 +382,13 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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/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/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-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.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -426,7 +426,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index fdcf1562f1f..3a111a9028b 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.25.3 require ( - github.com/go-jose/go-jose/v4 v4.1.2 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/stretchr/testify v1.11.1 @@ -34,27 +34,25 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // 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/klog/v2 v2.130.1 // 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 3b59ab0b8b9..42e2598d7f5 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -6,8 +6,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -77,8 +77,8 @@ 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -96,8 +96,6 @@ 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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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= @@ -130,10 +128,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -157,7 +155,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4a3a9d7a84b..08a647a145b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -31,7 +31,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // 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 @@ -71,7 +71,7 @@ require ( go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect @@ -92,9 +92,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 52c046f42a0..ff63cdc69ca 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -25,8 +25,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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= @@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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/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 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -170,8 +170,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -260,13 +260,13 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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/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/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-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.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -303,7 +303,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 80952661b2b..e08dfce7d74 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -16,7 +16,7 @@ require ( golang.org/x/net v0.47.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/sync v0.18.0 // @grafana/alerting-backend golang.org/x/text v0.31.0 // indirect; @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // indirect; @grafana/plugins-platform-backend ) @@ -30,8 +30,8 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // 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 ) require ( @@ -51,7 +51,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b5a55f1d488..3d794bfccf8 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -59,8 +59,8 @@ github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTd github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +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/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= @@ -105,12 +105,12 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 314d19580a4..6aea33c1df8 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -86,7 +86,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -111,9 +111,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.242.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.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 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect @@ -122,5 +122,5 @@ require ( 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.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 85d212bc772..eaa9d903015 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= @@ -278,8 +278,8 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -378,12 +378,12 @@ 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/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -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/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/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +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-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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +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= @@ -410,7 +410,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np 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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +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= From 1c3795d97c278d19842820717bfa166b7653520d Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 21:22:36 +0100 Subject: [PATCH 183/423] Alerting: Add spec.title selector to the receivers endpoint (#114599) --- .../alerting/notifications/kinds/receiver.cue | 8 +- .../v0alpha1/receiver_schema_gen.go | 15 +- .../v0alpha1/zz_openapi_gen.go | 904 ++++++++++++++++++ .../apis/alertingnotifications_manifest.go | 3 + 4 files changed, 925 insertions(+), 5 deletions(-) create mode 100644 apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go diff --git a/apps/alerting/notifications/kinds/receiver.cue b/apps/alerting/notifications/kinds/receiver.cue index 719dd8ad699..ed87ad8aec1 100644 --- a/apps/alerting/notifications/kinds/receiver.cue +++ b/apps/alerting/notifications/kinds/receiver.cue @@ -13,7 +13,7 @@ receiverv0alpha1: receiverKind & { schema: { spec: v0alpha1.ReceiverSpec } -// selectableFields: [ // TODO revisit when custom field selectors are supported -// "spec.title", -// ] -} \ No newline at end of file + selectableFields: [ + "spec.title", + ] +} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go index 0987b4504bc..ea4e3b8e363 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go @@ -5,13 +5,26 @@ package v0alpha1 import ( + "errors" + "github.com/grafana/grafana-app-sdk/resource" ) // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope)) + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ + FieldSelector: "spec.title", + FieldValueFunc: func(o resource.Object) (string, error) { + cast, ok := o.(*Receiver) + if !ok { + return "", errors.New("provided object must be of type *Receiver") + } + + return cast.Spec.Title, nil + }, + }, + })) kindReceiver = resource.Kind{ Schema: schemaReceiver, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go new file mode 100644 index 00000000000..33b088eca5c --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go @@ -0,0 +1,904 @@ +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver": schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverList": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeList": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref), + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Receiver", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "uid": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disableResolveMessage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "settings": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "secureFields": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"type", "version", "settings"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "integrations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"), + }, + }, + }, + }, + }, + }, + Required: []string{"title", "integrations"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the RoutingTree", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "label", "value"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "receiver": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "matchers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher"), + }, + }, + }, + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "mute_time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "active_time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), + }, + }, + }, + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"continue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "receiver": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"receiver"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "defaults": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"), + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), + }, + }, + }, + }, + }, + }, + Required: []string{"defaults", "routes"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the TemplateGroup", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "content": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "content"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the TimeInterval", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "times": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"), + }, + }, + }, + }, + }, + "weekdays": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "days_of_month": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "months": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "years": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "location": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "time_intervals"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "start_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "end_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"start_time", "end_time"}, + }, + }, + } +} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index cebfbec12f1..e16f443a4f6 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -49,6 +49,9 @@ var appManifestData = app.ManifestData{ Scope: "Namespaced", Conversion: false, Schema: &versionSchemaReceiverv0alpha1, + SelectableFields: []string{ + "spec.title", + }, }, { From 66b72b659f87fff8a2531c02a0dde0450e467429 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 00:39:42 +0000 Subject: [PATCH 184/423] I18n: Download translations from Crowdin (#114604) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 12 ++++++++++++ public/locales/de-DE/grafana.json | 12 ++++++++++++ public/locales/es-ES/grafana.json | 12 ++++++++++++ public/locales/fr-FR/grafana.json | 12 ++++++++++++ public/locales/hu-HU/grafana.json | 12 ++++++++++++ public/locales/id-ID/grafana.json | 12 ++++++++++++ public/locales/it-IT/grafana.json | 12 ++++++++++++ public/locales/ja-JP/grafana.json | 12 ++++++++++++ public/locales/ko-KR/grafana.json | 12 ++++++++++++ public/locales/nl-NL/grafana.json | 12 ++++++++++++ public/locales/pl-PL/grafana.json | 12 ++++++++++++ public/locales/pt-BR/grafana.json | 12 ++++++++++++ public/locales/pt-PT/grafana.json | 12 ++++++++++++ public/locales/ru-RU/grafana.json | 12 ++++++++++++ public/locales/sv-SE/grafana.json | 12 ++++++++++++ public/locales/tr-TR/grafana.json | 12 ++++++++++++ public/locales/zh-Hans/grafana.json | 12 ++++++++++++ public/locales/zh-Hant/grafana.json | 12 ++++++++++++ 18 files changed, 216 insertions(+) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 73a8378bd20..0f54e455b41 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Načítání a inicializace nástěnky", "title-not-found": "Panel s ID {{panelId}} nebyl nalezen" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Proměnné šablony" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Nenašly jsme žádnou shodu s vaším dotazem" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Vymazat typy", "select-aria-label": "Filtr typu panelu", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Vytvořit", "description-email": "Toto je volitelné a používá se především k povolení vlastních týmových avatarů", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Název", "label-role": "Role" @@ -13418,6 +13429,7 @@ "title-edit-team": "Upravit tým", "tooltip-edit-team": "Upravit tým" }, + "loading-teams": "", "new-team": "Nový tým", "placeholder-search-teams": "Hledat týmy" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 469a0ddfef8..075df04b5d0 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Laden und Initialisieren des Dashboards", "title-not-found": "Das Panel mit der ID {{panelId}} wurde nicht gefunden" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Vorlagenvariablen" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Es konnte nichts gefunden werden, was Ihrer Abfrage entspricht" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Typen zurücksetzen", "select-aria-label": "Panel-Typ-Filter", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Erstellen", "description-email": "Dies ist optional und wird hauptsächlich genutzt, um individuelle Team-Avatare zu ermöglichen", + "failed-to-create": "", "label-email": "E-Mail-Adresse", "label-name": "Name", "label-role": "Rolle" @@ -13304,6 +13315,7 @@ "title-edit-team": "Team bearbeiten", "tooltip-edit-team": "Team bearbeiten" }, + "loading-teams": "", "new-team": "Neues Team", "placeholder-search-teams": "Teams suchen" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index db55d7dbe2b..d5ef82955ed 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Cargando e iniciando el dashboard", "title-not-found": "Panel con ID {{panelId}} no encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variables de plantilla" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "No se ha encontrado nada que coincida con tu consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Borrar tipos", "select-aria-label": "Filtro de tipo de panel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Crear", "description-email": "Esto es opcional y se utiliza principalmente para permitir avatares de equipo personalizados", + "failed-to-create": "", "label-email": "Correo electrónico", "label-name": "Nombre", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipo", "tooltip-edit-team": "Editar equipo" }, + "loading-teams": "", "new-team": "Nuevo equipo", "placeholder-search-teams": "Buscar equipos" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e0b0cd18363..7849934975c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Chargement et initialisation du tableau de bord", "title-not-found": "Panneau avec l’ID {{panelId}} introuvable" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variables de modèle" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Impossible de trouver quoi que ce soit correspondant à votre requête" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Effacer les types", "select-aria-label": "Filtre par type de panneau", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Créer", "description-email": "Ceci est facultatif et est principalement utilisé pour autoriser les avatars d’équipe personnalisés", + "failed-to-create": "", "label-email": "Adresse e-mail", "label-name": "Nom", "label-role": "Rôle" @@ -13304,6 +13315,7 @@ "title-edit-team": "Modifier l’équipe", "tooltip-edit-team": "Modifier l’équipe" }, + "loading-teams": "", "new-team": "Nouvelle équipe", "placeholder-search-teams": "Rechercher les équipes" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 3ff7e30485b..800a0de556e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Irányítópult betöltése és inicializálása", "title-not-found": "A(z) {{panelId}} azonosítójú panel nem található" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Sablonváltozók" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Nincs találat a lekérdezésre" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Típusok törlése", "select-aria-label": "Paneltípusszűrő", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Létrehozás", "description-email": "Ez nem kötelező, és elsősorban egyéni csapatavatárok engedélyezésére szolgál", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Név", "label-role": "Szerepkör" @@ -13304,6 +13315,7 @@ "title-edit-team": "Csapat szerkesztése", "tooltip-edit-team": "Csapat szerkesztése" }, + "loading-teams": "", "new-team": "Új csapat", "placeholder-search-teams": "Csapatok keresése" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index a6708a09472..edf158ffbd1 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "Memuat & menginisialisasi dasbor", "title-not-found": "Panel dengan id {{panelId}} tidak ditemukan" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variabel templat" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "Tidak dapat menemukan sesuatu yang cocok dengan kueri Anda" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Hapus jenis", "select-aria-label": "Filter jenis panel", @@ -13216,6 +13226,7 @@ "create-team": { "create": "Buat", "description-email": "Hal ini opsional dan secara spesifik digunakan untuk mengizinkan avatar tim kustom", + "failed-to-create": "", "label-email": "Email", "label-name": "Nama", "label-role": "Peran" @@ -13247,6 +13258,7 @@ "title-edit-team": "Edit tim", "tooltip-edit-team": "Edit tim" }, + "loading-teams": "", "new-team": "Tim Baru", "placeholder-search-teams": "Cari tim" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index e5fc8647a20..c0c2e9cfe0d 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Caricamento e inizializzazione della dashboard", "title-not-found": "Pannello con ID {{panelId}} non trovato" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variabili del modello" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Non è stato possibile trovare nulla che corrispondesse alla tua query" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Cancella tipi", "select-aria-label": "Filtro del tipo di pannello", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Crea", "description-email": "Questo è facoltativo e viene utilizzato principalmente per consentire avatar di team personalizzati", + "failed-to-create": "", "label-email": "Email", "label-name": "Nome", "label-role": "Ruolo" @@ -13304,6 +13315,7 @@ "title-edit-team": "Modifica team", "tooltip-edit-team": "Modifica team" }, + "loading-teams": "", "new-team": "Nuovo team", "placeholder-search-teams": "Cerca team" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index fa3f87eabf4..6736b3e5fb0 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "ダッシュボードの読み込みと初期化", "title-not-found": "ID{{panelId}}のパネルが見つかりません" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "テンプレートの変数" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "クエリに一致するものが見つかりませんでした" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "タイプをクリア", "select-aria-label": "パネルタイプフィルター", @@ -13216,6 +13226,7 @@ "create-team": { "create": "作成", "description-email": "これは任意であり、主にカスタムチームのアバターに使用されます", + "failed-to-create": "", "label-email": "メール", "label-name": "名前", "label-role": "ロール" @@ -13247,6 +13258,7 @@ "title-edit-team": "チームを編集", "tooltip-edit-team": "チームを編集" }, + "loading-teams": "", "new-team": "新しいチーム", "placeholder-search-teams": "チームを検索" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index cf9773184cd..d17b3191e98 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "대시보드 로딩 및 초기화", "title-not-found": "ID가 {{panelId}}인 패널을 찾을 수 없음" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "템플릿 변수" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "쿼리와 일치하는 항목을 찾을 수 없습니다" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "유형 초기화", "select-aria-label": "패널 유형 필터", @@ -13216,6 +13226,7 @@ "create-team": { "create": "생성", "description-email": "이 항목은 선택 사항이며 주로 사용자 지정 팀 아바타를 허용하는 데 사용됩니다", + "failed-to-create": "", "label-email": "이메일", "label-name": "이름", "label-role": "역할" @@ -13247,6 +13258,7 @@ "title-edit-team": "팀 편집", "tooltip-edit-team": "팀 편집" }, + "loading-teams": "", "new-team": "새 팀", "placeholder-search-teams": "팀 검색" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 8a7dcc3b2d0..5339e9cc1ba 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Dashboard laden en initialiseren", "title-not-found": "Paneel met id {{panelId}} niet gevonden" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Sjabloonvariabelen" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Kon niets vinden dat overeenkomt met je query" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Types wissen", "select-aria-label": "Paneeltypefilter", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Aanmaken", "description-email": "Dit is optioneel en wordt voornamelijk gebruikt voor het toestaan van aangepaste teamavatars", + "failed-to-create": "", "label-email": "E-mailadres", "label-name": "Naam", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Team bewerken", "tooltip-edit-team": "Team bewerken" }, + "loading-teams": "", "new-team": "Nieuw team", "placeholder-search-teams": "Teams zoeken" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index d988de6fdee..9a90219b85f 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Wczytywanie i inicjowanie pulpitu", "title-not-found": "Nie znaleziono panelu o identyfikatorze {{panelId}}" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Zmienne szablonu" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Nie znaleziono wyników pasujących do Twojego zapytania" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Wyczyść typy", "select-aria-label": "Filtr typu panelu", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Utwórz", "description-email": "Jest to ustawienie opcjonalne i służy przede wszystkim do zezwalania na niestandardowe awatary zespołu", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nazwa", "label-role": "Rola" @@ -13418,6 +13429,7 @@ "title-edit-team": "Edytuj zespół", "tooltip-edit-team": "Edytuj zespół" }, + "loading-teams": "", "new-team": "Nowy zespół", "placeholder-search-teams": "Szukaj zespołów" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index e17ed3dade4..b3d6aa4c02f 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Carregando e inicializando o painel", "title-not-found": "Painel com ID {{panelId}} não encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variáveis do modelo" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Limpar tipos", "select-aria-label": "Filtro de tipo de painel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Criar", "description-email": "Isso é opcional e é usado principalmente para permitir avatares de equipe personalizados", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nome", "label-role": "Função" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipe", "tooltip-edit-team": "Editar equipe" }, + "loading-teams": "", "new-team": "Nova equipe", "placeholder-search-teams": "Pesquisar equipes" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 34cc8be7717..744661bf91c 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "A carregar e inicializar o painel de controlo", "title-not-found": "Painel com ID {{panelId}} não encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variáveis do modelo" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Limpar tipos", "select-aria-label": "Filtro do tipo de painel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Criar", "description-email": "Isto é opcional e é utilizado principalmente para permitir avatares de equipa personalizados", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nome", "label-role": "Função" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipa", "tooltip-edit-team": "Editar equipa" }, + "loading-teams": "", "new-team": "Nova equipa", "placeholder-search-teams": "Pesquisar equipas" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 3a8724d3098..0a2820e3e6c 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Загрузка и инициализация дашборда", "title-not-found": "Панель с идентификатором {{panelId}} не найдена" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Переменные шаблона" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Не удалось найти данные, соответствующие вашему запросу" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Очистить типы", "select-aria-label": "Фильтр типов панелей", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Создать", "description-email": "Это необязательное действие, которое используется в основном для создания пользовательских аватаров команд.", + "failed-to-create": "", "label-email": "Адрес электронной почты", "label-name": "Имя", "label-role": "Роль" @@ -13418,6 +13429,7 @@ "title-edit-team": "Редактирование команды", "tooltip-edit-team": "Редактировать команду" }, + "loading-teams": "", "new-team": "Новая команда", "placeholder-search-teams": "Поиск команд" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index c0bd748a996..b7b1cdf6cfa 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Laddar och initierar instrumentpanel", "title-not-found": "Panel med ID {{panelId}} hittades inte" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Mallvariabler" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Kunde inte hitta något som matchar din fråga" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Rensa typer", "select-aria-label": "Filter för paneltyp", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Skapa", "description-email": "Detta är valfritt och används främst för att tillåta anpassade teamavatarer", + "failed-to-create": "", "label-email": "E-post", "label-name": "Namn", "label-role": "Roll" @@ -13304,6 +13315,7 @@ "title-edit-team": "Redigera team", "tooltip-edit-team": "Redigera team" }, + "loading-teams": "", "new-team": "Nytt team", "placeholder-search-teams": "Sök team" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index cfeb21bd5aa..6c49826ac02 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Pano yükleniyor ve başlatılıyor", "title-not-found": "{{panelId}} kimliğine sahip panel bulunamadı" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Şablon değişkenleri" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Sorgunuzla eşleşen hiçbir şey bulunamadı" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Türleri temizle", "select-aria-label": "Panel tipi filtresi", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Oluştur", "description-email": "Bu isteğe bağlıdır ve esas olarak özel takım avatarlarına izin vermek için kullanılır", + "failed-to-create": "", "label-email": "E-posta", "label-name": "Ad", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Ekibi düzenle", "tooltip-edit-team": "Ekibi düzenle" }, + "loading-teams": "", "new-team": "Yeni ekip", "placeholder-search-teams": "Ekip ara" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index e96f271b082..7168af3ce09 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "正在加载和初始化数据面板", "title-not-found": "未找到 ID 为 {{panelId}} 的面板" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "模板变量" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "找不到与您的查询匹配的内容" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "清除类型", "select-aria-label": "面板类型筛选器", @@ -13216,6 +13226,7 @@ "create-team": { "create": "创建", "description-email": "这是可选项,主要用于允许自定义团队头像", + "failed-to-create": "", "label-email": "电子邮箱", "label-name": "名称", "label-role": "角色" @@ -13247,6 +13258,7 @@ "title-edit-team": "编辑团队", "tooltip-edit-team": "编辑团队" }, + "loading-teams": "", "new-team": "新建团队", "placeholder-search-teams": "搜索团队" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 561d6b53c3a..dddcce4ef55 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "正在載入並初始化儀表板", "title-not-found": "未找到 ID 為 {{panelId}} 的面板" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "範本變數" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "找不到符合您查詢的內容" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "清除類型", "select-aria-label": "面板類型篩選器", @@ -13216,6 +13226,7 @@ "create-team": { "create": "建立", "description-email": "這是可選項目,主要用於允許自訂團隊頭像", + "failed-to-create": "", "label-email": "電子郵件", "label-name": "名稱", "label-role": "角色" @@ -13247,6 +13258,7 @@ "title-edit-team": "編輯團隊", "tooltip-edit-team": "編輯團隊" }, + "loading-teams": "", "new-team": "新團隊", "placeholder-search-teams": "搜尋團隊" }, From 3b481f8687afa842508d77595d1c4c0baaeacb3d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Sun, 30 Nov 2025 07:53:19 -0600 Subject: [PATCH 185/423] Docs: Add dashboard dto information (#114459) --------- Co-authored-by: Anna Urbiztondo --- .../api-reference/http-api/dashboard.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/developer-resources/api-reference/http-api/dashboard.md b/docs/sources/developer-resources/api-reference/http-api/dashboard.md index 2cc7aa84bb3..abef6a8f5e6 100644 --- a/docs/sources/developer-resources/api-reference/http-api/dashboard.md +++ b/docs/sources/developer-resources/api-reference/http-api/dashboard.md @@ -628,6 +628,14 @@ Status Codes: - **403** – Access denied - **404** – Not Found +### Retrieve additional access information + +`GET /apis/dashboard.grafana.app/v1beta1/namespaces/:namespace/dashboards/:uid/dto` + +Retrieves a dashboard with additional access information. + +The `GET` response includes an additional `access` section with data such as if it's a public dashboard, or the dashboard permissions (admin, editor) of the user who made the request. + ## List Dashboards `GET /apis/dashboard.grafana.app/v1beta1/namespaces/:namespace/dashboards` From db9afe31e46c52cfbc93f80042c7fa0cb7a2396b Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Sun, 30 Nov 2025 23:24:03 -0600 Subject: [PATCH 186/423] Provisioning: Fix panic on watcher when channel is closed (#114439) --- .../pkg/repository/local/watch.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/provisioning/pkg/repository/local/watch.go b/apps/provisioning/pkg/repository/local/watch.go index 91caed9a1d3..6ad3fdbaa31 100644 --- a/apps/provisioning/pkg/repository/local/watch.go +++ b/apps/provisioning/pkg/repository/local/watch.go @@ -28,6 +28,7 @@ type fileWatcher struct { timers map[string]*time.Timer watcher *fsnotify.Watcher logger logging.Logger + closed bool } // File watcher that buffers events for 100ms before actually firing them @@ -77,22 +78,21 @@ func NewFileWatcher(path string, accept func(string) bool) (FileWatcher, error) // Keep watching for changes until the context is done func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { + defer f.cleanup(events) + for { select { case <-ctx.Done(): - close(events) return case _, ok := <-f.watcher.Errors: if !ok { // Channel was closed (i.e. Watcher.Close() was called). - close(events) return } // Read from Events. case e, ok := <-f.watcher.Events: if !ok { // Channel was closed (i.e. Watcher.Close() was called). - close(events) return } name := filepath.Base(e.Name) @@ -114,6 +114,11 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { if !ok { nameCopy := e.Name t = time.AfterFunc(math.MaxInt64, func() { + // before sending the event, check if the watcher has been closed + if f.closed { + return + } + path, _ := strings.CutPrefix(nameCopy, f.prefix) events <- path @@ -128,3 +133,17 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { } } } + +// stop all pending timers and close the event channel +func (f *fileWatcher) cleanup(events chan<- string) { + f.timersMu.Lock() + defer f.timersMu.Unlock() + + for _, timer := range f.timers { + timer.Stop() + } + f.timers = make(map[string]*time.Timer) + + close(events) + f.closed = true +} From aaa5d02a3e45f89605b1223cf6dc8f349fbce442 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 1 Dec 2025 01:29:04 -0700 Subject: [PATCH 187/423] AuthZ: Set span errors (#114460) --- pkg/services/authz/zanzana/server/server_batch_check.go | 9 +++++++++ pkg/services/authz/zanzana/server/server_check.go | 3 +++ pkg/services/authz/zanzana/server/server_list.go | 3 +++ pkg/services/authz/zanzana/server/server_mutate.go | 3 +++ pkg/services/authz/zanzana/server/server_query.go | 3 +++ pkg/services/authz/zanzana/server/server_read.go | 3 +++ pkg/services/authz/zanzana/server/server_write.go | 3 +++ 7 files changed, 27 insertions(+) diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index ecc86712950..62994c58973 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -5,6 +5,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -15,6 +16,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest defer span.End() if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } @@ -24,11 +27,15 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest store, err := s.getStoreInfo(ctx, r.GetNamespace()) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } contextuals, err := s.getContextuals(r.GetSubject()) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } @@ -37,6 +44,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest for _, item := range r.GetItems() { res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index e9712b6fe21..d8c1a2ec0c9 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -9,6 +9,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "google.golang.org/protobuf/types/known/structpb" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -25,6 +26,8 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C res, err := s.check(ctx, r) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform check request", "error", err, "namespace", r.GetNamespace()) return nil, errors.New("failed to perform check request") } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index a5fa19e3896..46f7a9167ad 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -13,6 +13,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) @@ -28,6 +29,8 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis res, err := s.list(ctx, r) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform list request", "error", err, "namespace", r.GetNamespace()) return nil, errors.New("failed to perform list request") } diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index cdd8c5b2f50..bd339534fe5 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -7,6 +7,7 @@ import ( "time" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "go.opentelemetry.io/otel/codes" ) type OperationGroup string @@ -30,6 +31,8 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au res, err := s.mutate(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform mutate request") } diff --git a/pkg/services/authz/zanzana/server/server_query.go b/pkg/services/authz/zanzana/server/server_query.go index 596c3805a5a..585c64f8ddd 100644 --- a/pkg/services/authz/zanzana/server/server_query.go +++ b/pkg/services/authz/zanzana/server/server_query.go @@ -8,6 +8,7 @@ import ( "time" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "go.opentelemetry.io/otel/codes" ) func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { @@ -20,6 +21,8 @@ func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*auth res, err := s.query(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform query request") } diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index d78d2fcbc0b..07cb7902b1a 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -7,6 +7,7 @@ import ( "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -22,6 +23,8 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze res, err := s.read(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform read request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform read request") } diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index 2f7df49d688..d16d706a320 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -7,6 +7,7 @@ import ( "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -22,6 +23,8 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth res, err := s.write(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform write request") } From dd77107ed4007816f85a84fbf8ff6ecdc217e1e6 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Mon, 1 Dec 2025 09:25:57 +0000 Subject: [PATCH 188/423] Folders: Add additional query param to legacy API for tracking purposes (#114505) --- public/app/api/clients/folder/v1beta1/hooks.ts | 17 +++++++++++++---- .../api/browseDashboardsAPI.ts | 14 +++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/public/app/api/clients/folder/v1beta1/hooks.ts b/public/app/api/clients/folder/v1beta1/hooks.ts index 1793818bdd6..1fea62c40d0 100644 --- a/public/app/api/clients/folder/v1beta1/hooks.ts +++ b/public/app/api/clients/folder/v1beta1/hooks.ts @@ -103,10 +103,17 @@ const combineFolderResponses = ( export async function getFolderByUidFacade(uid: string): Promise { const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid); - // We need the legacy API call regardless, for now - const legacyApiCall = dispatch(browseDashboardsAPI.endpoints.getFolder.initiate(uid)); - const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI); + + // We need the legacy API call regardless, for now + const legacyApiCall = dispatch( + browseDashboardsAPI.endpoints.getFolder.initiate({ + folderUID: uid, + accesscontrol: true, + isLegacyCall: shouldUseAppPlatformAPI, + }) + ); + if (shouldUseAppPlatformAPI) { let virtualFolderResponse; if (isVirtualFolder) { @@ -165,7 +172,9 @@ export function useGetFolderQueryFacade(uid?: string) { // This may look weird that we call the legacy folder anyway all the time, but the issue is we don't have good API // for the access control metadata yet, and so we still take it from the old api. // see https://github.com/grafana/identity-access-team/issues/1103 - const legacyFolderResult = useGetFolderQueryLegacy(uid || skipToken); + const legacyFolderResult = useGetFolderQueryLegacy( + uid ? { folderUID: uid, accesscontrol: true, isLegacyCall: true } : skipToken + ); let resultFolder = useGetFolderQuery(shouldUseAppPlatformAPI && !isVirtualFolder ? params : skipToken); // We get parents and folders for virtual folders too. Parents should just return empty array but it's easier to // stitch the responses this way and access can actually return different response based on the grafana setup. diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 51ff4423bc8..90d255cead9 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -94,9 +94,17 @@ export const browseDashboardsAPI = createApi({ }), // get folder info (e.g. title, parents) but *not* children - getFolder: builder.query({ - providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }], - query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }), + getFolder: builder.query({ + providesTags: (_result, _error, { folderUID }) => [{ type: 'getFolder', id: folderUID }], + query: ({ folderUID, accesscontrol, isLegacyCall }) => ({ + url: `/folders/${folderUID}`, + params: { + accesscontrol, + // Add additional query param so we can tell when + // this was called for app platform compatibility purposes vs. actually needing to use the legacy API + isLegacyCall, + }, + }), }), // create a new folder From 2da171595a17ce21067da4d419013d05e1cf3264 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 1 Dec 2025 09:51:11 +0000 Subject: [PATCH 189/423] FieldColor: Add accessible color palettes (#114424) * add viridis and others, allow passing interpolator directly to FieldColorSchemeMode * fix bug * be more defensive when getting the fieldcolor * backend changes * add d3-scale-chromatic to list of esModules --- .../kinds/v2alpha1/dashboard_spec.cue | 13 ++-- .../kinds/v2beta1/dashboard_spec.cue | 15 ++-- .../dashboard/v0alpha1/dashboard_kind.cue | 17 +++-- .../apis/dashboard/v1beta1/dashboard_kind.cue | 17 +++-- .../dashboard/v2alpha1/dashboard_spec.cue | 13 ++-- .../dashboard/v2alpha1/dashboard_spec_gen.go | 12 +++- .../apis/dashboard/v2beta1/dashboard_spec.cue | 15 ++-- .../dashboard/v2beta1/dashboard_spec_gen.go | 12 +++- kinds/dashboard/dashboard_kind.cue | 17 +++-- packages/grafana-data/package.json | 1 + packages/grafana-data/src/field/fieldColor.ts | 70 +++++++++++++++++-- packages/grafana-data/src/types/fieldColor.ts | 5 ++ packages/grafana-plugin-configs/jest/utils.js | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 12 +++- .../dashboard/v2alpha0/dashboard.schema.cue | 9 ++- .../schema/dashboard/v2alpha0/types.gen.ts | 11 ++- .../dashboard/v2alpha1/types.spec.gen.ts | 9 ++- .../dashboard/v2beta1/types.spec.gen.ts | 9 ++- pkg/kinds/dashboard/dashboard_spec_gen.go | 12 +++- yarn.lock | 1 + 20 files changed, 217 insertions(+), 54 deletions(-) diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5768c358c24..df99f88581c 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -340,7 +340,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -352,7 +357,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -377,7 +382,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -387,7 +392,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index 426fb179fd8..b81175db77d 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -113,7 +113,7 @@ DashboardLink: { placement?: DashboardLinkPlacement } -// Dashboard Link placement. Defines where the link should be displayed. +// Dashboard Link placement. Defines where the link should be displayed. // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" @@ -342,7 +342,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -354,7 +359,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -379,7 +384,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -389,7 +394,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index 9222dad2bc7..d2a65bbbf24 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -301,8 +301,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -318,7 +318,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -330,7 +330,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -381,7 +381,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -393,7 +398,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index 9222dad2bc7..d2a65bbbf24 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -301,8 +301,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -318,7 +318,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -330,7 +330,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -381,7 +381,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -393,7 +398,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 9a8a621345f..b9e8420d2bb 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -344,7 +344,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -356,7 +361,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -381,7 +386,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -391,7 +396,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index b83cbbd6fb3..e095d7f6c7b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -583,7 +583,12 @@ func NewDashboardFieldColor() *DashboardFieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -602,6 +607,11 @@ const ( DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds" DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic" DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name" + DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis" + DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma" + DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma" + DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno" + DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis" DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd" DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr" DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd" diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index d9df0dd3ee3..b2340d5c22e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -117,7 +117,7 @@ DashboardLink: { placement?: DashboardLinkPlacement } -// Dashboard Link placement. Defines where the link should be displayed. +// Dashboard Link placement. Defines where the link should be displayed. // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" @@ -346,7 +346,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -358,7 +363,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -383,7 +388,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -393,7 +398,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index fc0a8fcf26f..ffdad0045c6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -587,7 +587,12 @@ func NewDashboardFieldColor() *DashboardFieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -606,6 +611,11 @@ const ( DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds" DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic" DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name" + DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis" + DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma" + DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma" + DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno" + DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis" DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd" DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr" DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd" diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index ce2510d2cfe..346edbb4753 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -297,8 +297,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -314,7 +314,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -326,7 +326,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -377,7 +377,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -389,7 +394,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 59d3c58a042..ff90b2c295d 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -63,6 +63,7 @@ "@types/string-hash": "1.1.3", "@types/systemjs": "6.15.3", "d3-interpolate": "3.0.1", + "d3-scale-chromatic": "3.1.0", "date-fns": "4.1.0", "dompurify": "3.3.0", "eventemitter3": "5.0.1", diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index a7957f9f44e..7016c208994 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -1,4 +1,11 @@ import { interpolateRgbBasis } from 'd3-interpolate'; +import { + interpolateViridis, + interpolateMagma, + interpolatePlasma, + interpolateInferno, + interpolateCividis, +} from 'd3-scale-chromatic'; import stringHash from 'string-hash'; import tinycolor from 'tinycolor2'; @@ -75,6 +82,41 @@ export const fieldColorModeRegistry = new Registry(() => { ); }, }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousViridis, + name: 'Viridis', + isContinuous: true, + isByValue: true, + interpolator: interpolateViridis, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousMagma, + name: 'Magma', + isContinuous: true, + isByValue: true, + interpolator: interpolateMagma, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousPlasma, + name: 'Plasma', + isContinuous: true, + isByValue: true, + interpolator: interpolatePlasma, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousInferno, + name: 'Inferno', + isContinuous: true, + isByValue: true, + interpolator: interpolateInferno, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousCividis, + name: 'Cividis', + isContinuous: true, + isByValue: true, + interpolator: interpolateCividis, + }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousGrYlRd, name: 'Green-Yellow-Red', @@ -148,16 +190,27 @@ export const fieldColorModeRegistry = new Registry(() => { ]; }); -interface FieldColorSchemeModeOptions { +interface BaseFieldColorSchemeModeOptions { id: FieldColorModeId; name: string; description?: string; - getColors: (theme: GrafanaTheme2) => string[]; isContinuous: boolean; isByValue: boolean; useSeriesName?: boolean; } +interface FieldColorSchemeModeInterpolator extends BaseFieldColorSchemeModeOptions { + interpolator: (value: number) => string; + getColors?: never; +} + +interface FieldColorSchemeModeGetColors extends BaseFieldColorSchemeModeOptions { + getColors: (theme: GrafanaTheme2) => string[]; + interpolator?: never; +} + +type FieldColorSchemeModeOptions = FieldColorSchemeModeGetColors | FieldColorSchemeModeInterpolator; + export class FieldColorSchemeMode implements FieldColorMode { id: FieldColorModeId; name: string; @@ -178,11 +231,15 @@ export class FieldColorSchemeMode implements FieldColorMode { this.isContinuous = options.isContinuous; this.isByValue = options.isByValue; this.useSeriesName = options.useSeriesName; + this.interpolator = options.interpolator; } getColors(theme: GrafanaTheme2): string[] { if (!this.getNamedColors) { - return []; + if (!this.interpolator) { + return []; + } + this.getNamedColors = () => new Array(9).fill(0).map((_, i) => this.getInterpolator()(i / 8)); } if (this.colorCache && this.colorCacheTheme === theme) { @@ -231,12 +288,15 @@ export class FieldColorSchemeMode implements FieldColorMode { /** @beta */ export function getFieldColorModeForField(field: Field): FieldColorMode { - return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds); + return ( + fieldColorModeRegistry.getIfExists(field.config.color?.mode) ?? + fieldColorModeRegistry.get(FieldColorModeId.Thresholds) + ); } /** @beta */ export function getFieldColorMode(mode?: FieldColorModeId | string): FieldColorMode { - return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds); + return fieldColorModeRegistry.getIfExists(mode) ?? fieldColorModeRegistry.get(FieldColorModeId.Thresholds); } /** diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index 26d5cc2a662..7b07abfb337 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -16,6 +16,11 @@ export enum FieldColorModeId { ContinuousReds = 'continuous-reds', ContinuousGreens = 'continuous-greens', ContinuousPurples = 'continuous-purples', + ContinuousViridis = 'continuous-viridis', + ContinuousMagma = 'continuous-magma', + ContinuousPlasma = 'continuous-plasma', + ContinuousInferno = 'continuous-inferno', + ContinuousCividis = 'continuous-cividis', Fixed = 'fixed', Shades = 'shades', } diff --git a/packages/grafana-plugin-configs/jest/utils.js b/packages/grafana-plugin-configs/jest/utils.js index 439617819bb..1d9da1c50ce 100644 --- a/packages/grafana-plugin-configs/jest/utils.js +++ b/packages/grafana-plugin-configs/jest/utils.js @@ -8,6 +8,7 @@ export const grafanaESModules = [ 'd3', 'd3-color', 'd3-interpolate', + 'd3-scale-chromatic', 'delaunator', 'get-user-locale', 'internmap', diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 3466df584b8..c6a722cbae7 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -475,7 +475,12 @@ export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'dataso * `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold * `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations * `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - * `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + * `continuous-viridis`: Continuous Viridis palette mode + * `continuous-magma`: Continuous Magma palette mode + * `continuous-plasma`: Continuous Plasma palette mode + * `continuous-inferno`: Continuous Inferno palette mode + * `continuous-cividis`: Continuous Cividis palette mode + * `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode * `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode * `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode * `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -492,11 +497,16 @@ export enum FieldColorModeId { ContinuousBlPu = 'continuous-BlPu', ContinuousBlYlRd = 'continuous-BlYlRd', ContinuousBlues = 'continuous-blues', + ContinuousCividis = 'continuous-cividis', ContinuousGrYlRd = 'continuous-GrYlRd', ContinuousGreens = 'continuous-greens', + ContinuousInferno = 'continuous-inferno', + ContinuousMagma = 'continuous-magma', + ContinuousPlasma = 'continuous-plasma', ContinuousPurples = 'continuous-purples', ContinuousRdYlGr = 'continuous-RdYlGr', ContinuousReds = 'continuous-reds', + ContinuousViridis = 'continuous-viridis', ContinuousYlBl = 'continuous-YlBl', ContinuousYlRd = 'continuous-YlRd', Fixed = 'fixed', diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index ba637656ed8..052e626cdfc 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -334,7 +334,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -346,7 +351,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index d38a6693ac9..34af3332332 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -327,7 +327,7 @@ export interface FieldConfig { description?: string; // An explicit path to the field in the datasource. When the frame meta includes a path, // This will default to `${frame.meta.path}/${field.name} - // + // // When defined, this value can be used as an identifier within the datasource scope, and // may be used to update the results path?: string; @@ -529,7 +529,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -541,7 +546,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 060a0e3945f..b264bce9e18 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -489,7 +489,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -501,7 +506,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 00e9111453a..95c0dac4230 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -496,7 +496,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -508,7 +513,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 786fabd8de8..fd8dbae7b77 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -619,7 +619,12 @@ func NewFieldColor() *FieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -637,6 +642,11 @@ const ( FieldColorModeIdThresholds FieldColorModeId = "thresholds" FieldColorModeIdPaletteClassic FieldColorModeId = "palette-classic" FieldColorModeIdPaletteClassicByName FieldColorModeId = "palette-classic-by-name" + FieldColorModeIdContinuousViridis FieldColorModeId = "continuous-viridis" + FieldColorModeIdContinuousMagma FieldColorModeId = "continuous-magma" + FieldColorModeIdContinuousPlasma FieldColorModeId = "continuous-plasma" + FieldColorModeIdContinuousInferno FieldColorModeId = "continuous-inferno" + FieldColorModeIdContinuousCividis FieldColorModeId = "continuous-cividis" FieldColorModeIdContinuousGrYlRd FieldColorModeId = "continuous-GrYlRd" FieldColorModeIdContinuousRdYlGr FieldColorModeId = "continuous-RdYlGr" FieldColorModeIdContinuousBlYlRd FieldColorModeId = "continuous-BlYlRd" diff --git a/yarn.lock b/yarn.lock index abe69dda8b7..356d0eaacf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3122,6 +3122,7 @@ __metadata: "@types/systemjs": "npm:6.15.3" "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" + d3-scale-chromatic: "npm:3.1.0" date-fns: "npm:4.1.0" dompurify: "npm:3.3.0" esbuild: "npm:0.25.8" From 7fce2d9516c59b410d1862e370f8f0ed27dd38c6 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Mon, 1 Dec 2025 11:30:22 +0100 Subject: [PATCH 190/423] fix(unified): set SQLite path in cfg for reusing shared DB (#114580) --- pkg/tests/testinfra/testinfra.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 25b2d4b177b..cb38d0cd768 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -107,6 +107,9 @@ func StartGrafanaEnvWithDB(t *testing.T, grafDir, cfgPath string) (string, *serv dbCfg.Key("user").SetValue(testDB.User) dbCfg.Key("password").SetValue(testDB.Password) dbCfg.Key("name").SetValue(testDB.Database) + if testDB.Path != "" { + dbCfg.Key("path").SetValue(testDB.Path) + } t.Log("Using test database", "type", testDB.DriverName, "host", testDB.Host, "port", testDB.Port, "user", testDB.User, "name", testDB.Database, "path", testDB.Path) From 32b9bebc75f4f5aca9956d12f7d6fa8596dbdcd7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 1 Dec 2025 11:17:28 +0000 Subject: [PATCH 191/423] Swagger: Load embedded icons from CDN (#114632) --- pkg/api/swagger.go | 6 ++---- public/swagger/SwaggerPage.tsx | 5 ++--- public/views/swagger.html | 10 +++++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/api/swagger.go b/pkg/api/swagger.go index 6e34efdf82a..1cc1ba4447d 100644 --- a/pkg/api/swagger.go +++ b/pkg/api/swagger.go @@ -30,10 +30,8 @@ func (hs *HTTPServer) registerSwaggerUI(r routing.RouteRegister) { } data := map[string]any{ - "Nonce": c.RequestNonce, - "Assets": assets, - "FavIcon": "public/img/fav32.png", - "AppleTouchIcon": "public/img/apple-touch-icon.png", + "Nonce": c.RequestNonce, + "Assets": assets, } if hs.Cfg.CSPEnabled { data["CSPEnabled"] = true diff --git a/public/swagger/SwaggerPage.tsx b/public/swagger/SwaggerPage.tsx index 6c322e3884b..f97aafedc36 100644 --- a/public/swagger/SwaggerPage.tsx +++ b/public/swagger/SwaggerPage.tsx @@ -5,10 +5,9 @@ import SwaggerUI from 'swagger-ui-react'; import { createTheme, monacoLanguageRegistry, SelectableValue } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { Stack, Select, UserIcon, UserView, Button } from '@grafana/ui'; +import { Icon, Stack, Select, UserIcon, UserView, Button } from '@grafana/ui'; import { setMonacoEnv } from 'app/core/monacoEnv'; import { ThemeProvider } from 'app/core/utils/ConfigProvider'; -import grafanaIconSvg from 'img/grafana_icon.svg'; import { NamespaceContext, WrappedPlugins } from './plugins'; @@ -85,7 +84,7 @@ export const Page = () => {
- Grafana + - - - - - {!ds?.meta.annotations && ( - - - The selected data source does not support annotations. Please select a different data source. - - - )} - - - - - - - - - - - - - <> - - {panelFilter !== PanelFilterType.AllPanels && ( - annotation.filter?.ids.includes(panel.value!))} - onChange={onAddFilterPanelID} - isClearable={true} - placeholder={t('dashboard-scene.annotation-settings-edit.placeholder-choose-panels', 'Choose panels')} - width={100} - closeMenuOnSelect={false} - className={styles.select} - data-testid={selectors.components.Annotations.annotationsChoosePanelInput} - /> + + + {/* Data source */} + + + + {!ds?.meta.annotations && ( + + + The selected data source does not support annotations. Please select a different data source. + + + )} + + {/* Enabled */} + - + > + + + + {/* Color */} + + + + + + + {/* Annotation controls display */} + + + + + {/* Show in */} + + <> + - - - + + + {/* Type */} + + - - - - - - + + + {/* Tooltip */} + + + + + {/* Icon */} + + updateValue('target', target)} placeholder="events.eventname" /> -
-
+ + Show Global Annotations? updateIsGlobal(isGlobal)} /> -
-
+ + ); }; From 8a0fa93aecd8e85c82305b834b726572102ab691 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 5 Dec 2025 13:55:56 +0100 Subject: [PATCH 310/423] Zanzana: Fix duplicated writes in one request (#114900) * Zanzana: Fix duplicated writes * add tests --- .../authz/zanzana/server/server_mutate.go | 66 ++++++++++++++++++- .../zanzana/server/server_mutate_folder.go | 19 +----- .../zanzana/server/server_mutate_org_role.go | 19 +----- .../server_mutate_resourcepermissions.go | 19 +----- .../server/server_mutate_rolebindings.go | 19 +----- .../zanzana/server/server_mutate_roles.go | 19 +----- .../server/server_mutate_teambindings.go | 19 +----- .../zanzana/server/server_mutate_test.go | 64 ++++++++++++++++++ 8 files changed, 135 insertions(+), 109 deletions(-) diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index bd339534fe5..15a57404bbc 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -6,8 +6,10 @@ import ( "fmt" "time" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/codes" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) type OperationGroup string @@ -119,3 +121,65 @@ func groupByOperation(operations []*authzextv1.MutateOperation) (map[OperationGr return grouped, nil } + +func deduplicateTupleKeys(writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) ([]*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition) { + deduplicatedWriteTuples := make([]*openfgav1.TupleKey, 0) + deduplicatedDeleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + writeTupleMap := make(map[string]bool) + + for _, writeTuple := range writeTuples { + id := getTupleKeyID(writeTuple) + if !writeTupleMap[id] { + writeTupleMap[id] = true + deduplicatedWriteTuples = append(deduplicatedWriteTuples, writeTuple) + } + } + + // Prioritize writes over deletes. Deletes do not have a condition, so we don't know if write tuple is different from delete one. + for _, deleteTuple := range deleteTuples { + id := getTupleKeyID(deleteTuple) + if !writeTupleMap[id] { + writeTupleMap[id] = true + deduplicatedDeleteTuples = append(deduplicatedDeleteTuples, deleteTuple) + } + } + + return deduplicatedWriteTuples, deduplicatedDeleteTuples +} + +func (s *Server) writeTuples(ctx context.Context, store *storeInfo, writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) error { + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + + writeTuples, deleteTuples = deduplicateTupleKeys(writeTuples, deleteTuples) + + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + return err +} + +type TupleKey interface { + GetUser() string + GetRelation() string + GetObject() string +} + +func getTupleKeyID(t TupleKey) string { + return fmt.Sprintf("%s:%s:%s", t.GetUser(), t.GetRelation(), t.GetObject()) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder.go b/pkg/services/authz/zanzana/server/server_mutate_folder.go index 3d92347f404..6d07492b788 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_folder.go +++ b/pkg/services/authz/zanzana/server/server_mutate_folder.go @@ -52,24 +52,7 @@ func (s *Server) mutateFolders(ctx context.Context, store *storeInfo, operations return nil } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write folder tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_org_role.go b/pkg/services/authz/zanzana/server/server_mutate_org_role.go index bda9decb3d5..843c62859dd 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_org_role.go +++ b/pkg/services/authz/zanzana/server/server_mutate_org_role.go @@ -50,24 +50,7 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation return nil } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write user org role tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go index fa8b5467235..f85f31900f6 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go @@ -47,24 +47,7 @@ func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource permission tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go index 3b18566bee2..faf23d1f1ed 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go @@ -44,24 +44,7 @@ func (s *Server) mutateRoleBindings(ctx context.Context, store *storeInfo, opera } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_roles.go b/pkg/services/authz/zanzana/server/server_mutate_roles.go index 4c19b1fd288..c0471fdbf17 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_roles.go +++ b/pkg/services/authz/zanzana/server/server_mutate_roles.go @@ -41,24 +41,7 @@ func (s *Server) mutateRoles(ctx context.Context, store *storeInfo, operations [ } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_teambindings.go b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go index 81e1c9cb437..96690bb96d8 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_teambindings.go +++ b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go @@ -43,24 +43,7 @@ func (s *Server) mutateTeamBindings(ctx context.Context, store *storeInfo, opera } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_test.go b/pkg/services/authz/zanzana/server/server_mutate_test.go index 70dc1ea2fb8..c1fcfabbe43 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_test.go @@ -5,6 +5,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" @@ -133,3 +134,66 @@ func testMutate(t *testing.T, srv *Server) { require.Len(t, res.Tuples, 0) }) } + +func TestDeduplicateTupleKeys(t *testing.T) { + t.Run("should deduplicate write tuples", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:1", Relation: "get", Object: "object:2"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:2", Relation: "get", Object: "object:2"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 2) + require.ElementsMatch(t, deduplicatedWriteTuples, []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:1", Relation: "get", Object: "object:2"}, + }) + + require.Len(t, deduplicatedDeleteTuples, 1) + require.ElementsMatch(t, deduplicatedDeleteTuples, []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:2", Relation: "get", Object: "object:2"}, + }) + }) + + t.Run("should deduplicate write tuples with conditions", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1", Condition: &openfgav1.RelationshipCondition{Name: "condition:1", Context: &structpb.Struct{Fields: map[string]*structpb.Value{ + "field:1": structpb.NewStringValue("value:1"), + }}}}, + {User: "user:1", Relation: "get", Object: "object:2"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:1", Relation: "get", Object: "object:1"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 2) + require.ElementsMatch(t, deduplicatedWriteTuples, []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1", Condition: &openfgav1.RelationshipCondition{Name: "condition:1", Context: &structpb.Struct{Fields: map[string]*structpb.Value{ + "field:1": structpb.NewStringValue("value:1"), + }}}}, + {User: "user:1", Relation: "get", Object: "object:2"}, + }) + + require.Len(t, deduplicatedDeleteTuples, 0) + }) + + t.Run("should do nothing for no duplicates", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:2", Relation: "get", Object: "object:2"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 1) + require.ElementsMatch(t, deduplicatedWriteTuples, writeTuples) + require.Len(t, deduplicatedDeleteTuples, 1) + require.ElementsMatch(t, deduplicatedDeleteTuples, deleteTuples) + }) +} From 5ac702a4c16b17ac08f067622ffe98ca4d855f1b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 5 Dec 2025 17:21:30 +0300 Subject: [PATCH 311/423] Dashboards: update manifest to avoid useoldmanifestkinds (#114910) --- apps/dashboard/Makefile | 3 +- apps/dashboard/kinds/dashboard.cue | 54 ------------ apps/dashboard/kinds/manifest.cue | 84 +++++++++++++++++-- apps/dashboard/kinds/snapshot.cue | 62 +++++--------- apps/dashboard/pkg/apis/dashboard/utils.go | 2 + .../conversion/v1beta1_to_v2alpha1.go | 4 +- .../conversion/v1beta1_to_v2alpha1_test.go | 4 +- pkg/tsdb/grafanads/grafana.go | 3 +- 8 files changed, 110 insertions(+), 106 deletions(-) diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index fa75513d964..3d5c7060199 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -12,8 +12,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati --grouping=group \ --defencoding=none \ --genoperatorstate=false \ - --noschemasinmanifest \ - --useoldmanifestkinds + --noschemasinmanifest .PHONY: post-generate-cleanup post-generate-cleanup: ## Clean up the generated code diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index e8dfea3bf98..5d04cf1e331 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -1,12 +1,5 @@ package kinds -import ( - v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" - v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" - v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" - v2beta1 "github.com/grafana/grafana/sdkkinds/dashboard/v2beta1" -) - // Status is the shared status of all dashboard versions. DashboardStatus: { // Optional conversion status. @@ -31,50 +24,3 @@ ConversionStatus: { // The original value map[string]any source?: _ } - -dashboard: { - kind: "Dashboard" - pluralName: "Dashboards" - current: "v1beta1" - codegen: { - ts: { - enabled: true - config: { - enumsAsUnionTypes: true - } - } - go: { - enabled: true - config: { - allowMarshalEmptyDisjunctions: true - } - } - } - - versions: { - "v0alpha1": { - schema: { - spec: v0.DashboardSpec - status: DashboardStatus - } - } - "v1beta1": { - schema: { - spec: v1.DashboardSpec - status: DashboardStatus - } - } - "v2alpha1": { - schema: { - spec: v2alpha1.DashboardSpec - status: DashboardStatus - } - } - "v2beta1": { - schema: { - spec: v2beta1.DashboardSpec - status: DashboardStatus - } - } - } -} diff --git a/apps/dashboard/kinds/manifest.cue b/apps/dashboard/kinds/manifest.cue index f1044a39e24..9fb17910664 100644 --- a/apps/dashboard/kinds/manifest.cue +++ b/apps/dashboard/kinds/manifest.cue @@ -1,10 +1,82 @@ package kinds +import ( + v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" + v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" + v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" + v2beta1 "github.com/grafana/grafana/sdkkinds/dashboard/v2beta1" +) + manifest: { - appName: "dashboard" - groupOverride: "dashboard.grafana.app" - kinds: [ - dashboard, - snapshot, - ] + appName: "dashboard" + groupOverride: "dashboard.grafana.app" + preferredVersion: "v1beta1" + + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v0.DashboardSpec + status: DashboardStatus + } + }, + snapshotV0alpha1, // Only exists in v0alpha (for now) + ] + } + "v1beta1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v1.DashboardSpec + status: DashboardStatus + } + } + ] + } + "v2alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v2alpha1.DashboardSpec + status: DashboardStatus + } + } + ] + } + "v2beta1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v2beta1.DashboardSpec + status: DashboardStatus + } + } + ] + } + } } diff --git a/apps/dashboard/kinds/snapshot.cue b/apps/dashboard/kinds/snapshot.cue index c224daf8492..00f445881b5 100644 --- a/apps/dashboard/kinds/snapshot.cue +++ b/apps/dashboard/kinds/snapshot.cue @@ -1,46 +1,30 @@ package kinds -snapshot: { +snapshotV0alpha1: { kind: "Snapshot" pluralName: "Snapshots" - scope: "Namespaced" - current: "v0alpha1" - - codegen: { - ts: { - enabled: true - } - go: { - enabled: true - } - } - - versions: { - "v0alpha1": { - schema: { - spec: { - // Snapshot title - title?: string - - // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) - expires?: int64 | *0 - - // When set to true, the snapshot exists in a remote server - external?: bool | *false - - // The external URL where the snapshot can be seen - externalUrl?: string - - // The URL that created the dashboard originally - originalUrl?: string - - // Snapshot creation timestamp - timestamp?: string + schema: { + spec: { + // Snapshot title + title?: string + + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + expires?: int64 | *0 + + // When set to true, the snapshot exists in a remote server + external?: bool | *false + + // The external URL where the snapshot can be seen + externalUrl?: string + + // The URL that created the dashboard originally + originalUrl?: string + + // Snapshot creation timestamp + timestamp?: string - // The raw dashboard (unstructured for now) - dashboard?: [string]: _ - } - } + // The raw dashboard (unstructured for now) + dashboard?: [string]: _ } } -} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/apis/dashboard/utils.go b/apps/dashboard/pkg/apis/dashboard/utils.go index a5979151a90..f1453d548a0 100644 --- a/apps/dashboard/pkg/apis/dashboard/utils.go +++ b/apps/dashboard/pkg/apis/dashboard/utils.go @@ -6,6 +6,8 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) +const GrafanaDatasourceUID = "grafana" + // SetPluginIDMeta sets the repo name to "plugin" and the path to the plugin ID func SetPluginIDMeta(obj *unstructured.Unstructured, pluginID string) { if pluginID == "" { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index e6599b1d8eb..231c0ad4131 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -12,11 +12,11 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // getDefaultDatasourceType gets the default datasource type using the datasource provider @@ -58,7 +58,7 @@ func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemavers // datasource: { type: "datasource" } with no UID, it should resolve to uid: "grafana". func resolveGrafanaDatasourceUID(dsType, dsUID string) string { if dsType == "datasource" && dsUID == "" { - return grafanads.DatasourceUID + return dashboard.GrafanaDatasourceUID } return dsUID } diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go index 6bbdf1ca214..3dad9188fe7 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" 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" migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" - "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // TestV1beta1ToV2alpha1 tests conversion from v1beta1 to v2alpha1 with various datasource scenarios @@ -77,7 +77,7 @@ func TestV1beta1ToV2alpha1(t *testing.T) { // Verify datasource UID is resolved to "grafana" assert.NotNil(t, query.Spec.Datasource.Uid) - assert.Equal(t, grafanads.DatasourceUID, *query.Spec.Datasource.Uid, "type: 'datasource' with no UID should resolve to uid: 'grafana'") + assert.Equal(t, dashboard.GrafanaDatasourceUID, *query.Spec.Datasource.Uid, "type: 'datasource' with no UID should resolve to uid: 'grafana'") // Verify query kind matches datasource type assert.Equal(t, "datasource", query.Spec.Query.Kind) diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index 06254b7f0b8..68dc792342c 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" @@ -31,7 +32,7 @@ const DatasourceID = -1 // DatasourceUID is the fake datasource uid used in requests to identify it as a // Grafana DS command. -const DatasourceUID = "grafana" +const DatasourceUID = dashboard.GrafanaDatasourceUID // Make sure Service implements required interfaces. // This is important to do since otherwise we will only get a From b19e5462545c80705e26a1d0489bae2ccac1b702 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 5 Dec 2025 16:04:42 +0100 Subject: [PATCH 312/423] Remote Alertmanager: Remove X-Remote-Alertmanager header (#114917) Remote Alertmanager: Remove X-Remote-Alertmanager haeder --- pkg/services/ngalert/remote/alertmanager_test.go | 7 ------- .../ngalert/remote/client/mimir_auth_round_tripper.go | 4 +--- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 953ef3ef2d6..d6f66756454 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -153,7 +153,6 @@ func TestGetRemoteState(t *testing.T) { getOkHandler := func(state string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) res := map[string]any{ "status": "success", @@ -268,7 +267,6 @@ func TestIntegrationApplyConfig(t *testing.T) { errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("content-type", "application/json") w.WriteHeader(http.StatusInternalServerError) require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"status": "error"})) @@ -278,7 +276,6 @@ func TestIntegrationApplyConfig(t *testing.T) { var configSyncs, stateSyncs int okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) res := map[string]any{"status": "success"} if r.Method == http.MethodPost { @@ -432,7 +429,6 @@ func TestCompareAndSendConfiguration(t *testing.T) { var got string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("content-type", "application/json") b, err := io.ReadAll(r.Body) @@ -639,7 +635,6 @@ func Test_TestReceiversDecryptsSecureSettings(t *testing.T) { var got apimodels.TestReceiversConfigBodyParams server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("Content-Type", "application/json") require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) require.NoError(t, r.Body.Close()) @@ -746,7 +741,6 @@ func TestApplyConfigWithExtraConfigs(t *testing.T) { var configSent client.UserGrafanaConfig server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { require.NoError(t, json.NewDecoder(r.Body).Decode(&configSent)) @@ -828,7 +822,6 @@ func TestCompareAndSendConfigurationWithExtraConfigs(t *testing.T) { var configSent client.UserGrafanaConfig server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { require.NoError(t, json.NewDecoder(r.Body).Decode(&configSent)) diff --git a/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go b/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go index 3a9ff94f4a6..2a7a6314e1d 100644 --- a/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go +++ b/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go @@ -5,8 +5,7 @@ import ( ) const ( - MimirTenantHeader = "X-Scope-OrgID" - RemoteAlertmanagerHeader = "X-Remote-Alertmanager" + MimirTenantHeader = "X-Scope-OrgID" ) type MimirAuthRoundTripper struct { @@ -19,7 +18,6 @@ type MimirAuthRoundTripper struct { // It adds an `X-Scope-OrgID` header with the TenantID if only provided with a tenantID or sets HTTP Basic Authentication if both // a tenantID and a password are provided. func (r *MimirAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set(RemoteAlertmanagerHeader, "true") if r.TenantID != "" && r.Password == "" { req.Header.Set(MimirTenantHeader, r.TenantID) } From bf042afa9878fcbe91a14da88fb6373f0946c0eb Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 5 Dec 2025 17:12:12 +0200 Subject: [PATCH 313/423] Dashboard: Fix dropping panels in tabs and rows (#114893) --- .../dashboard-scene/scene/layout-rows/RowItemRenderer.tsx | 3 ++- .../dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index 4c74ee35d99..80c16b2f2a1 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -13,6 +13,7 @@ import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState, useInterpolatedTitle } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { useSoloPanelContext } from '../SoloPanelContext'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { RowItem } from './RowItem'; @@ -83,7 +84,7 @@ export function RowItemRenderer({ model }: SceneComponentProps) { dragProvided.innerRef(ref); model.containerRef.current = ref; }} - data-dashboard-drop-target-key={model.state.key} + data-dashboard-drop-target-key={isDashboardLayoutGrid(layout) ? model.state.key : undefined} className={cx( styles.wrapper, !isCollapsed && styles.wrapperNotCollapsed, diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 0fa22e9d305..e618fb21c7e 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -11,6 +11,7 @@ import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useI import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; import { useSoloPanelContext } from '../SoloPanelContext'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { TabItem } from './TabItem'; @@ -91,7 +92,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { onSelect?.(evt); }} label={titleInterpolated} - data-dashboard-drop-target-key={model.state.key} + data-dashboard-drop-target-key={isDashboardLayoutGrid(layout) ? model.state.key : undefined} {...titleCollisionProps} />
From 7cd10aa49ed22435b0aef9fc04d471818b7108dc Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Fri, 5 Dec 2025 10:14:02 -0500 Subject: [PATCH 314/423] SQL Expressions: Fix alerts with sql expressions that have a cte (#114852) Fix for #114377 - fix alerts with sql expressions that have a cte --- .../components/rule-editor/dag.test.ts | 55 +++++++++++++++++++ .../unified/components/rule-editor/dag.ts | 44 +++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/dag.test.ts b/public/app/features/alerting/unified/components/rule-editor/dag.test.ts index 189bcb6f25e..423f035ddc9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/dag.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/dag.test.ts @@ -293,6 +293,61 @@ SELECT * FROM table1`) expect(parseRefsFromSqlExpression('SELECT * FROM\ntable1')).toEqual(['table1']); }); }); + + describe('CTE (Common Table Expression) handling', () => { + it('should exclude single CTE name from results', () => { + const query = 'WITH my_cte AS (SELECT * FROM table1) SELECT * FROM my_cte'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should exclude multiple CTE names from results', () => { + const query = ` + WITH cte1 AS (SELECT * FROM table1), + cte2 AS (SELECT * FROM table2) + SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTEs with external table references in main query', () => { + const query = ` + WITH summary AS (SELECT id, count FROM table1) + SELECT * FROM summary JOIN table2 ON summary.id = table2.id + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTE names case-insensitively', () => { + const query = 'WITH MyCte AS (SELECT * FROM table1) SELECT * FROM mycte'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should handle RECURSIVE CTEs', () => { + const query = ` + WITH RECURSIVE cte AS ( + SELECT * FROM table1 + UNION ALL + SELECT * FROM cte WHERE depth < 10 + ) + SELECT * FROM cte + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should handle queries without CTEs normally', () => { + const query = 'SELECT * FROM table1 JOIN table2 ON table1.id = table2.id'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTE that references another CTE', () => { + const query = ` + WITH cte1 AS (SELECT * FROM table1), + cte2 AS (SELECT * FROM cte1) + SELECT * FROM cte2 + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + }); }); describe('fingerprints', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/dag.ts b/public/app/features/alerting/unified/components/rule-editor/dag.ts index 9666186f039..c7078ca7517 100644 --- a/public/app/features/alerting/unified/components/rule-editor/dag.ts +++ b/public/app/features/alerting/unified/components/rule-editor/dag.ts @@ -132,10 +132,15 @@ export function parseRefsFromSqlExpression(input: string): string[] { .replace(/\s+/g, ' ') // Remove any potential multi line comments .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Extract CTE names to exclude them from table references + const cteNames = parseCteNames(query); + const tableMatches = []; // Extract tables after FROM - case insensitive with /i flag - const fromRegex = /from\s+([^;]*?)(?:\s+(?:join|where|group|having|order|limit)|\s*$)/gi; + // Terminate on: SQL keywords, closing paren (for CTEs/subqueries), or end of string + const fromRegex = /from\s+([^;)]*?)(?:\s+(?:join|where|group|having|order|limit|on|select)|\)|$)/gi; for (const match of query.matchAll(fromRegex)) { const fromClause = match[1].trim(); @@ -153,13 +158,44 @@ export function parseRefsFromSqlExpression(input: string): string[] { tableMatches.push(cleanTableName(match[1])); } - return compact(uniq(tableMatches)); + // Filter out CTE names - they're local definitions, not external references + const externalRefs = tableMatches.filter((table) => !cteNames.has(table.toLowerCase())); + + return compact(uniq(externalRefs)); +} + +/** + * Parse CTE (Common Table Expression) names from a SQL query. + * CTEs are defined with: WITH cte_name AS (...), another_cte AS (...) + */ +function parseCteNames(query: string): Set { + const cteNames = new Set(); + + // Match the WITH clause - handles both regular and RECURSIVE CTEs + const withMatch = query.match(/^\s*with\s+(?:recursive\s+)?(.*?)(?:\s+select\s)/i); + + if (!withMatch) { + return cteNames; + } + + const withClause = withMatch[1]; + + // Match CTE names - they appear before "AS" keyword followed by opening paren + // This handles: cte_name AS (, "quoted_name" AS ( + const cteNameRegex = /([a-zA-Z0-9_]+|"[^"]+"|'[^']+')\s+as\s*\(/gi; + + for (const match of withClause.matchAll(cteNameRegex)) { + const cteName = match[1].replace(/['"]/g, '').toLowerCase(); + cteNames.add(cteName); + } + + return cteNames; } // Helper function to clean table names function cleanTableName(tableName: string): string { - // Remove quotes - let name = tableName.replace(/['"]/g, ''); + // Remove quotes and parentheses + let name = tableName.replace(/['"()]/g, ''); // Remove alias if present (both "AS alias" and "alias" forms) if (name.includes(' as ')) { From 0adb2461e9bdd21c56412a2b9d3695fad6e06902 Mon Sep 17 00:00:00 2001 From: "Marc M." <146180665+grafakus@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:48:34 +0100 Subject: [PATCH 315/423] Dashboards: Improve custom variable editor and undo/redo (#114559) --- .../dashboards-edit-custom-variables.spec.ts | 6 +- .../src/selectors/pages.ts | 3 + .../components/VariableValuesPreview.tsx | 2 +- .../CustomVariableEditor/ModalEditor.tsx | 92 +++++++++++++++---- .../editors/CustomVariableEditor/PaneItem.tsx | 2 +- .../CustomVariableEditor/ValuesBuilder.tsx | 52 ----------- .../CustomVariableEditor/ValuesPreview.tsx | 13 --- public/locales/en-US/grafana.json | 4 +- 8 files changed, 85 insertions(+), 89 deletions(-) delete mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx delete mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index 4715dfc7128..e11f2dd099a 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -84,9 +84,9 @@ test.describe( refetchItems(dashboardPage, selectors); }; - const closeModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + const applyAndcloseModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { await dashboardPage - .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton) + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.applyButton) .click(); }; @@ -149,7 +149,7 @@ test.describe( await removeItem(dashboardPage, selectors, 2); await checkRows(3); await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']); - await closeModal(dashboardPage, selectors); + await applyAndcloseModal(dashboardPage, selectors); // assert variable is visible and has the correct values const variableLabel = dashboardPage.getByGrafanaSelector( diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 47a5573b00d..1fa640a2563 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -567,6 +567,9 @@ export const versionedPages = { closeButton: { [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-close-button', }, + applyButton: { + [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-apply-button', + }, }, IntervalVariable: { intervalsValueInput: { diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx index 73d57bd811a..ac59419cda7 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx @@ -37,7 +37,7 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) = {previewOptions.map((o, index) => ( -
{o.label}
+
{o.label || String(o.value)}
))} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx index 3e8a8aa57b1..aed926a6809 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx @@ -1,47 +1,103 @@ -import { useCallback, useRef } from 'react'; +import { useRef, useState } from 'react'; +import { lastValueFrom } from 'rxjs'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { CustomVariable } from '@grafana/scenes'; +import { CustomVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; import { Button, Modal, Stack } from '@grafana/ui'; -import { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; +import { dashboardEditActions } from '../../../../edit-pane/shared'; +import { VariableStaticOptionsForm, VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton'; - -import { ValuesBuilder } from './ValuesBuilder'; -import { ValuesPreview } from './ValuesPreview'; +import { VariableValuesPreview } from '../../components/VariableValuesPreview'; interface ModalEditorProps { variable: CustomVariable; - isOpen: boolean; onClose: () => void; } -export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) { - const formRef = useRef(null); - - const handleOnAdd = useCallback(() => formRef.current?.addItem(), []); +export function ModalEditor(props: ModalEditorProps) { + const { formRef, onCloseModal, options, onChangeOptions, onAddNewOption, onSaveOptions } = useModalEditor(props); return ( - - + + - }> + }> + ); } + +function useModalEditor({ variable, onClose }: ModalEditorProps) { + const { query } = variable.state; + const [options, setOptions] = useState(() => transformQueryToOptions(variable, query)); + const initialQueryRef = useRef(query); + const formRef = useRef(null); + + return { + formRef, + onCloseModal: onClose, + options, + onChangeOptions: setOptions, + onAddNewOption() { + formRef.current?.addItem(); + }, + onSaveOptions() { + dashboardEditActions.edit({ + source: variable, + description: t('dashboard.edit-pane.variable.custom-options.change-value', 'Change variable value'), + perform: () => { + variable.setState({ query: transformOptionsToQuery(options) }); + lastValueFrom(variable.validateAndUpdate!()); + }, + undo: () => { + variable.setState({ query: initialQueryRef.current }); + lastValueFrom(variable.validateAndUpdate!()); + }, + }); + + onClose(); + }, + }; +} + +const transformQueryToOptions = (variable: ModalEditorProps['variable'], query: string) => + variable.transformCsvStringToOptions(query, false).map(({ label, value }) => ({ + value, + label: value === label ? '' : label, + })); + +const formatOption = (option: VariableValueOption) => { + if (!option.label || option.label === option.value) { + return escapeEntities(option.value); + } + return `${escapeEntities(option.label)} : ${escapeEntities(String(option.value))}`; +}; + +const escapeEntities = (text: VariableValueSingle) => String(text).trim().replaceAll(',', '\\,'); + +const transformOptionsToQuery = (options: VariableValueOption[]) => options.map(formatOption).join(', '); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx index e453fc6b8b8..d1dab1e554f 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx @@ -31,7 +31,7 @@ export function PaneItem({ variable }: PaneItemProps) { Open variable editor - setIsOpen(false)} /> + {isOpen && setIsOpen(false)} />} ); } diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx deleted file mode 100644 index e2eceea5fd3..00000000000 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { forwardRef, useCallback } from 'react'; -import { lastValueFrom } from 'rxjs'; - -import { CustomVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; - -import { VariableStaticOptionsForm, VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; - -interface ValuesBuilderProps { - variable: CustomVariable; -} - -export const ValuesBuilder = forwardRef(function ( - { variable }: ValuesBuilderProps, - ref -) { - const { query } = variable.useState(); - - const options = variable.transformCsvStringToOptions(query, false).map(({ label, value }) => ({ - value, - label: value === label ? '' : label, - })); - - const escapeEntities = useCallback((text: VariableValueSingle) => String(text).trim().replaceAll(',', '\\,'), []); - - const formatOption = useCallback( - (option: VariableValueOption) => { - if (!option.label || option.label === option.value) { - return escapeEntities(option.value); - } - - return `${escapeEntities(option.label)} : ${escapeEntities(String(option.value))}`; - }, - [escapeEntities] - ); - - const generateQuery = useCallback( - (options: VariableValueOption[]) => options.map(formatOption).join(', '), - [formatOption] - ); - - const handleOptionsChange = useCallback( - async (options: VariableValueOption[]) => { - variable.setState({ query: generateQuery(options) }); - await lastValueFrom(variable.validateAndUpdate!()); - }, - [variable, generateQuery] - ); - - return ; -}); - -ValuesBuilder.displayName = 'ValuesBuilder'; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx deleted file mode 100644 index 49a3e8dd55b..00000000000 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CustomVariable } from '@grafana/scenes'; - -import { VariableValuesPreview } from '../../components/VariableValuesPreview'; -import { hasVariableOptions } from '../../utils'; - -export function ValuesPreview({ variable }: { variable: CustomVariable }) { - // Workaround to toggle a component refresh when values change so that the preview is updated - variable.useState(); - - const isHasVariableOptions = hasVariableOptions(variable); - - return isHasVariableOptions ? : null; -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6ee3b9e50e6..cc056e38526 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "Close", + "apply": "Apply", + "change-value": "Change variable value", + "discard": "Discard", "modal-title": "Custom Variable", "values": "Values separated by comma" }, From 74c7b5a29220301d8809d2fc0caad2efe9f4e853 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 5 Dec 2025 18:02:11 +0100 Subject: [PATCH 316/423] Alerting: Fix creating a new alert rule vesion when only keep_firing_for changes (#114926) Alerting: Create alert rule vesion when keep_firing_for changes --- pkg/services/ngalert/store/models.go | 1 + pkg/services/ngalert/store/models_test.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index 24167a225dd..25b81053e66 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -88,6 +88,7 @@ func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool { a.NoDataState == b.NoDataState && a.ExecErrState == b.ExecErrState && a.For == b.For && + a.KeepFiringFor == b.KeepFiringFor && a.Annotations == b.Annotations && a.Labels == b.Labels && a.IsPaused == b.IsPaused && diff --git a/pkg/services/ngalert/store/models_test.go b/pkg/services/ngalert/store/models_test.go index ddbc34b4036..98703609007 100644 --- a/pkg/services/ngalert/store/models_test.go +++ b/pkg/services/ngalert/store/models_test.go @@ -21,6 +21,7 @@ func TestAlertRuleVersion_EqualSpec(t *testing.T) { NoDataState: "state1", ExecErrState: "state2", For: time.Minute, + KeepFiringFor: 2 * time.Minute, Annotations: `{ "test": "annotation" }`, Labels: `{ "test": "label" }`, IsPaused: true, @@ -119,6 +120,12 @@ func TestAlertRuleVersion_EqualSpec(t *testing.T) { b: func() alertRuleVersion { v := baseVersion; v.For = 2 * time.Minute; return v }(), expect: false, }, + { + name: "different KeepFiringFor durations", + a: baseVersion, + b: func() alertRuleVersion { v := baseVersion; v.KeepFiringFor = 5 * time.Minute; return v }(), + expect: false, + }, { name: "exact match including bools and other types", a: func() alertRuleVersion { From 5b89d3b807d06836a4d7e281bb37cbd1d5dae715 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:56:01 -0500 Subject: [PATCH 317/423] Plugins App: Add access control (#114869) --- pkg/registry/apps/plugins/accesscontrol.go | 127 ++++++++++++++++++ pkg/registry/apps/plugins/register.go | 21 ++- pkg/server/wire_gen.go | 4 +- pkg/services/accesscontrol/permreg/permreg.go | 2 + 4 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 pkg/registry/apps/plugins/accesscontrol.go diff --git a/pkg/registry/apps/plugins/accesscontrol.go b/pkg/registry/apps/plugins/accesscontrol.go new file mode 100644 index 00000000000..d41efa86f97 --- /dev/null +++ b/pkg/registry/apps/plugins/accesscontrol.go @@ -0,0 +1,127 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" +) + +const ( + // Plugins + ActionPluginsPluginsCreate = "plugins.plugins:create" // CREATE. + ActionPluginsPluginsWrite = "plugins.plugins:write" // UPDATE. + ActionPluginsPluginsRead = "plugins.plugins:read" // GET + LIST. + ActionPluginsPluginsDelete = "plugins.plugins:delete" // DELETE. + + // PluginMetas + ActionPluginsPluginsMetaCreate = "plugins.pluginsmeta:create" // CREATE. + ActionPluginsPluginsMetaWrite = "plugins.pluginsmeta:write" // UPDATE. + ActionPluginsPluginsMetaRead = "plugins.pluginsmeta:read" // GET + LIST. + ActionPluginsPluginsMetaDelete = "plugins.pluginsmeta:delete" // DELETE. +) + +var ( + ScopeProviderPluginsPlugins = accesscontrol.NewScopeProvider("plugins.plugins") + ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.pluginsmeta") + + ScopeAllPluginsPlugins = ScopeProviderPluginsPlugins.GetResourceAllScope() + ScopeAllPluginsPluginsMeta = ScopeProviderPluginsPluginsMeta.GetResourceAllScope() +) + +func registerAccessControlRoles(service accesscontrol.Service) error { + // Plugins + pluginsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.plugins:reader", + DisplayName: "Plugins Reader", + Description: "Read and list plugins.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsRead, + Scope: ScopeAllPluginsPlugins, + }, + }, + }, + Grants: []string{string(org.RoleViewer), string(org.RoleEditor), string(org.RoleAdmin)}, + } + + pluginsWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.plugins:writer", + DisplayName: "Plugins Writer", + Description: "Create, update and delete plugins.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsCreate, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsRead, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsWrite, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsDelete, + Scope: ScopeAllPluginsPlugins, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // PluginMetas + pluginsMetaReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.pluginsmeta:reader", + DisplayName: "Plugin Metas Reader", + Description: "Read and list plugin metadata.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsMetaRead, + Scope: ScopeAllPluginsPluginsMeta, + }, + }, + }, + Grants: []string{string(org.RoleViewer), string(org.RoleEditor), string(org.RoleAdmin)}, + } + + pluginsMetaWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.pluginsmeta:writer", + DisplayName: "Plugin Metas Writer", + Description: "Create, update and delete plugin metadata.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsMetaCreate, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaRead, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaWrite, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaDelete, + Scope: ScopeAllPluginsPluginsMeta, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + return service.DeclareFixedRoles( + pluginsReader, + pluginsWriter, + pluginsMetaReader, + pluginsMetaWriter, + ) +} diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index 5d452cbe67c..6831d31ef9b 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -1,14 +1,16 @@ package plugins import ( + "fmt" "os" - "k8s.io/apiserver/pkg/authorization/authorizer" - + authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "k8s.io/apiserver/pkg/authorization/authorizer" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" ) @@ -18,10 +20,14 @@ var ( ) type AppInstaller struct { - appsdkapiserver.AppInstaller + *pluginsapp.PluginAppInstaller } -func ProvideAppInstaller() (*AppInstaller, error) { +func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClient authlib.AccessClient) (*AppInstaller, error) { + if err := registerAccessControlRoles(accessControlService); err != nil { + return nil, fmt.Errorf("registering access control roles: %w", err) + } + grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL") if grafanaComAPIURL == "" { grafanaComAPIURL = "https://grafana.com/api/plugins" @@ -36,12 +42,13 @@ func ProvideAppInstaller() (*AppInstaller, error) { return nil, err } + i.WithAccessChecker(accessClient) + return &AppInstaller{ - AppInstaller: i, + PluginAppInstaller: i, }, nil } -// GetAuthorizer returns the authorizer for the plugins app. -func (p *AppInstaller) GetAuthorizer() authorizer.Authorizer { +func (a *AppInstaller) GetAuthorizer() authorizer.Authorizer { return pluginsapp.GetAuthorizer() } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index e920bdbec61..d1a2ecee64f 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -783,7 +783,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller() + appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) if err != nil { return nil, err } @@ -1436,7 +1436,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller() + appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/permreg/permreg.go b/pkg/services/accesscontrol/permreg/permreg.go index 5d025a1258b..c9f010c2909 100644 --- a/pkg/services/accesscontrol/permreg/permreg.go +++ b/pkg/services/accesscontrol/permreg/permreg.go @@ -84,6 +84,8 @@ func newPermissionRegistry() *permissionRegistry { "annotations": "annotations:type:", "orgs": "orgs:id:", "plugins": "plugins:id:", + "plugins.plugins": "plugins.plugins:uid:", + "plugins.pluginsmeta": "plugins.pluginsmeta:uid:", "provisioners": "provisioners:", "reports": "reports:id:", "permissions": "permissions:type:", From d1cbef9157ef70282e56b83b1a2f91ac418c6caa Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:53:31 -0800 Subject: [PATCH 318/423] K8s: use runtime config for API Builders (#114601) * Reapply "K8s: read resource configs from API Enablement for API Builders" (#114475) This reverts commit 4130bd9cd300ec7ce0fb492b3a7229968c9167b4. * revert part that broke things * FF service changes are gonna come later --- pkg/services/apiserver/builder/helper.go | 85 +++++++++++++------ pkg/services/apiserver/builder/openapi.go | 19 ++++- .../apiserver/builder/request_handler.go | 10 ++- pkg/services/apiserver/service.go | 8 +- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 603c49f3cb2..a76a01dffba 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -22,6 +22,7 @@ import ( k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" + serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stracing "k8s.io/component-base/tracing" @@ -78,7 +79,9 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. delegateHandler, c.LoopbackClientConfig, builders, - reg) + reg, + c.MergedResourceConfig, + ) if err != nil { panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error())) } @@ -105,6 +108,8 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. } } +// SetupConfig sets up the server config for the API server +// specify isAggregator=true, if the chain is being constructed for kube-aggregator func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, @@ -114,6 +119,7 @@ func SetupConfig( gvs []schema.GroupVersion, additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions, reg prometheus.Registerer, + apiResourceConfig *serverstorage.ResourceConfig, ) error { serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...) @@ -126,7 +132,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { meta := r.Metadata() kind := "" @@ -287,6 +293,7 @@ func InstallAPIs( features featuremgmt.FeatureToggles, dualWriterMetrics *grafanarest.DualWriterMetrics, builderMetrics *BuilderMetrics, + apiResourceConfig *serverstorage.ResourceConfig, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -401,34 +408,9 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ - Scheme: scheme, - OptsGetter: optsGetter, - DualWriteBuilder: dualWrite, - MetricsRegister: reg, - StorageOptsRegister: optsregister, - StorageOpts: storageOpts, - }); err != nil { + if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil { return err } - if len(g.PrioritizedVersions) < 1 { - continue - } - - // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { - for name := range resources { - if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { - delete(resources, name) - } - } - if len(resources) == 0 { - delete(g.VersionedResourcesStorageMap, "v0alpha1") - } - } - } } // skip installing the group if there are no resources left after filtering @@ -445,6 +427,53 @@ func InstallAPIs( return nil } +func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme, + optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister, + storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error { + if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + StorageOptsRegister: optsregister, + StorageOpts: storageOpts, + }); err != nil { + return err + } + if len(g.PrioritizedVersions) < 1 { + return nil + } + + // filter out api groups that are disabled in APIEnablementOptions + for version := range g.VersionedResourcesStorageMap { + gvr := schema.GroupVersionResource{ + Group: group, + Version: version, + } + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String()) + delete(g.VersionedResourcesStorageMap, version) + } + } + + // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { + for name := range resources { + if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { + delete(resources, name) + } + } + if len(resources) == 0 { + delete(g.VersionedResourcesStorageMap, "v0alpha1") + } + } + } + + return nil +} + // AddPostStartHooks adds post start hooks to a generic API server config func AddPostStartHooks( config *genericapiserver.RecommendedConfig, diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 9cb7cc17f7c..6d3b33f4baf 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,6 +9,8 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" + serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/klog/v2" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -76,6 +78,7 @@ func addBuilderRoutes( targetGroupVersion schema.GroupVersion, openAPISpec *spec3.OpenAPI, apiGroupBuilders []APIGroupBuilder, + apiResourceConfig *serverstorage.ResourceConfig, ) (*spec3.OpenAPI, error) { for _, apiGroupBuilder := range apiGroupBuilders { // Optionally include raw http handlers for all builders @@ -107,12 +110,24 @@ func addBuilderRoutes( } } } + + // filter out api groups that are disabled in APIEnablementOptions + for path := range openAPISpec.Paths.Paths { + if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") { + gv := targetGroupVersion.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) { + klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String()) + delete(openAPISpec.Paths.Paths, path) + } + } + } + return openAPISpec, nil } // Modify the OpenAPI spec to include the additional routes. // nolint:gocyclo -func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -227,7 +242,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s } } } - return addBuilderRoutes(gv, ©, builders) + return addBuilderRoutes(gv, ©, builders, apiResourceConfig) } } return s, nil diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/services/apiserver/builder/request_handler.go index 80fbf6d0eaa..50761f1d42c 100644 --- a/pkg/services/apiserver/builder/request_handler.go +++ b/pkg/services/apiserver/builder/request_handler.go @@ -6,7 +6,9 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" + serverstorage "k8s.io/apiserver/pkg/server/storage" restclient "k8s.io/client-go/rest" + klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/spec3" ) @@ -14,7 +16,7 @@ type requestHandler struct { router *mux.Router } -func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) { +func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { useful := false // only true if any routes exist anywhere router := mux.NewRouter() @@ -27,6 +29,12 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient } for _, gv := range GetGroupVersions(builder) { + // filter out api groups that are disabled in APIEnablementOptions + gvr := gv.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) + continue + } routes := provider.GetAPIRoutes(gv) if routes == nil { continue diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 28da416536c..605016c112d 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -316,7 +316,11 @@ func (s *service) start(ctx context.Context) error { s.cfg.BuildBranch, ) - if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil { + apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers) + // add the builder group versions to the api resource config + apiResourceConfig.EnableVersions(groupVersions...) + + if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil { return err } @@ -359,6 +363,7 @@ func (s *service) start(ctx context.Context) error { groupVersions, defGetters, s.metrics, + apiResourceConfig, ) if err != nil { return err @@ -400,6 +405,7 @@ func (s *service) start(ctx context.Context) error { s.features, s.dualWriterMetrics, s.builderMetrics, + apiResourceConfig, ) if err != nil { return err From 0f9d0317dc2a2e2b362e48e25acbd992633029bd Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:40:23 +0000 Subject: [PATCH 319/423] I18n: Download translations from Crowdin (#114938) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 7 +++++-- public/locales/de-DE/grafana.json | 7 +++++-- public/locales/es-ES/grafana.json | 7 +++++-- public/locales/fr-FR/grafana.json | 7 +++++-- public/locales/hu-HU/grafana.json | 7 +++++-- public/locales/id-ID/grafana.json | 7 +++++-- public/locales/it-IT/grafana.json | 7 +++++-- public/locales/ja-JP/grafana.json | 7 +++++-- public/locales/ko-KR/grafana.json | 7 +++++-- public/locales/nl-NL/grafana.json | 7 +++++-- public/locales/pl-PL/grafana.json | 7 +++++-- public/locales/pt-BR/grafana.json | 7 +++++-- public/locales/pt-PT/grafana.json | 7 +++++-- public/locales/ru-RU/grafana.json | 7 +++++-- public/locales/sv-SE/grafana.json | 7 +++++-- public/locales/tr-TR/grafana.json | 7 +++++-- public/locales/zh-Hans/grafana.json | 7 +++++-- public/locales/zh-Hant/grafana.json | 7 +++++-- 18 files changed, 90 insertions(+), 36 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 99f381a244e..e90a588053c 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Hodnoty oddělené čárkou" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index f84c77dd97f..e0a149baab7 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Werte werden durch Komma getrennt" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 70b7097bcc7..ff65f840041 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por coma" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 098050412e8..01bbb43fff9 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valeurs séparées par une virgule" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 28130cb2ec2..146b2a78d04 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Értékek vesszővel elválasztva" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 72598112d52..7b8e043e947 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Nilai dipisahkan dengan koma" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 5b8c69dff0b..68364a78d6f 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valori separati da virgola" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 14c46a83f7f..c023581e3cf 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "カンマで区切った値" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index df4cad85150..e9cb03b8350 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "쉼표로 구분된 값" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 126ad3f53f9..7872baf058a 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Waarden gescheiden door komma" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 0f441a67b12..acd3b23e77f 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Wartości rozdzielone przecinkami" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index a96c531b03c..e091960845a 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por vírgula" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index dc7e3c11c34..3a69ffa3194 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por vírgulas" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index cb1af71f6ed..a85c8b00dd5 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Значения, разделенные запятыми" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 883cac40ddf..733bfdd230b 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Värden åtskilda med kommatecken" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 2037475ec8a..4b26d75fcb5 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Virgülle ayrılmış değerler" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 8d34dcd8286..45cbea59c45 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "以逗号分隔的值" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 97699c7bdd8..5548f9f3d56 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "以逗號分隔的值" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", From e9ba45ca4fd7f28d34cd2590762b75c6a7afd7c6 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Sat, 6 Dec 2025 08:34:18 +0100 Subject: [PATCH 320/423] Update grafana-app-sdk to v0.48.5 (#114810) Co-authored-by: Owen Smallwood Co-authored-by: Ryan McKinley --- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 +- apps/alerting/alertenrichment/go.mod | 2 +- apps/alerting/alertenrichment/go.sum | 4 +- apps/alerting/historian/go.mod | 2 +- apps/alerting/historian/go.sum | 4 +- apps/alerting/notifications/go.mod | 2 +- apps/alerting/notifications/go.sum | 4 +- .../v0alpha1/receiver_object_gen.go | 6 + .../v0alpha1/receiver_schema_gen.go | 2 +- .../v0alpha1/routingtree_object_gen.go | 6 + .../v0alpha1/routingtree_schema_gen.go | 2 +- .../v0alpha1/templategroup_object_gen.go | 6 + .../v0alpha1/templategroup_schema_gen.go | 2 +- .../v0alpha1/timeinterval_object_gen.go | 6 + .../v0alpha1/timeinterval_schema_gen.go | 2 +- apps/alerting/rules/go.mod | 2 +- apps/alerting/rules/go.sum | 4 +- apps/annotation/go.mod | 2 +- apps/annotation/go.sum | 4 +- apps/collections/go.mod | 2 +- apps/collections/go.sum | 4 +- apps/correlations/go.mod | 2 +- apps/correlations/go.sum | 4 +- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 +- .../v0alpha1/dashboard_object_gen.go | 7 ++ .../v0alpha1/dashboard_schema_gen.go | 2 +- .../dashboard/v0alpha1/snapshot_object_gen.go | 6 + .../dashboard/v0alpha1/snapshot_schema_gen.go | 2 +- .../dashboard/v1beta1/dashboard_object_gen.go | 7 ++ .../dashboard/v1beta1/dashboard_schema_gen.go | 2 +- .../v2alpha1/dashboard_object_gen.go | 7 ++ .../v2alpha1/dashboard_schema_gen.go | 2 +- .../dashboard/v2beta1/dashboard_object_gen.go | 7 ++ .../dashboard/v2beta1/dashboard_schema_gen.go | 2 +- apps/example/go.mod | 2 +- apps/example/go.sum | 4 +- apps/folder/go.mod | 27 +++- apps/folder/go.sum | 69 ++++++++++- .../apis/folder/v1beta1/folder_object_gen.go | 6 + .../apis/folder/v1beta1/folder_schema_gen.go | 2 +- .../pkg/apis/manifestdata/folder_manifest.go | 116 ++++++++++++++++++ apps/iam/go.mod | 2 +- apps/iam/go.sum | 4 +- apps/investigations/go.mod | 2 +- apps/investigations/go.sum | 4 +- apps/logsdrilldown/go.mod | 2 +- apps/logsdrilldown/go.sum | 4 +- apps/playlist/go.mod | 2 +- apps/playlist/go.sum | 4 +- apps/plugins/go.mod | 17 +-- apps/plugins/go.sum | 24 ++-- apps/preferences/go.mod | 2 +- apps/preferences/go.sum | 4 +- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 2 +- apps/secret/go.sum | 4 +- apps/shorturl/go.mod | 2 +- apps/shorturl/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- go.work.sum | 3 +- 65 files changed, 365 insertions(+), 91 deletions(-) create mode 100644 apps/folder/pkg/apis/manifestdata/folder_manifest.go diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 2318df205ce..941d8b9bc0f 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 76208d30349..4695785bbd1 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -618,8 +618,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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.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.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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index 95466bad6bb..bb020e52bff 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index 5dff965c88b..ef36568611c 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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/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/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 5963ac8139c..21ad42c90af 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-kit/log v0.2.1 github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/client_golang v1.23.2 github.com/spf13/pflag v1.0.10 diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index 9e44308979b..b4d2d1dc2e2 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -216,14 +216,14 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= 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.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/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= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 80dd39f9ca7..f72c4c9775b 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + 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/apiserver v0.34.2 diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 6ab34248ef8..57b099ab2f1 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,8 +71,8 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go index 00bb1a34745..de0a0d5320f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go @@ -23,6 +23,12 @@ type Receiver struct { Spec ReceiverSpec `json:"spec" yaml:"spec"` } +func NewReceiver() *Receiver { + return &Receiver{ + Spec: *NewReceiverSpec(), + } +} + func (o *Receiver) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go index ea4e3b8e363..27047b00601 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go @@ -12,7 +12,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), + schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"), resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go index e59f0dada5c..354e009d77f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go @@ -23,6 +23,12 @@ type RoutingTree struct { Spec RoutingTreeSpec `json:"spec" yaml:"spec"` } +func NewRoutingTree() *RoutingTree { + return &RoutingTree{ + Spec: *NewRoutingTreeSpec(), + } +} + func (o *RoutingTree) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go index 6838c9cdebd..2a1812a2846 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &RoutingTree{}, &RoutingTreeList{}, resource.WithKind("RoutingTree"), + schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewRoutingTree(), &RoutingTreeList{}, resource.WithKind("RoutingTree"), resource.WithPlural("routingtrees"), resource.WithScope(resource.NamespacedScope)) kindRoutingTree = resource.Kind{ Schema: schemaRoutingTree, diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go index 0866bcb258c..d755a887a3f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go @@ -23,6 +23,12 @@ type TemplateGroup struct { Spec TemplateGroupSpec `json:"spec" yaml:"spec"` } +func NewTemplateGroup() *TemplateGroup { + return &TemplateGroup{ + Spec: *NewTemplateGroupSpec(), + } +} + func (o *TemplateGroup) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go index 0be8cb1c6de..ba92e2c4c4c 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), + schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTemplateGroup(), &TemplateGroupList{}, resource.WithKind("TemplateGroup"), resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope)) kindTemplateGroup = resource.Kind{ Schema: schemaTemplateGroup, diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go index 0ef813ee40c..e87b49dc958 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go @@ -23,6 +23,12 @@ type TimeInterval struct { Spec TimeIntervalSpec `json:"spec" yaml:"spec"` } +func NewTimeInterval() *TimeInterval { + return &TimeInterval{ + Spec: *NewTimeIntervalSpec(), + } +} + func (o *TimeInterval) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go index 715bfbc0fe7..d342cb90637 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), + schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTimeInterval(), &TimeIntervalList{}, resource.WithKind("TimeInterval"), resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope)) kindTimeInterval = resource.Kind{ Schema: schemaTimeInterval, diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 7286ccf5376..63da00536a6 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index a042d852413..946c42fe38f 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/annotation go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + 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 diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 81872ad4505..00575d7e6d4 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 22bf6a8cbcd..75a19848d73 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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.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.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/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index e1c9242aa14..29bc91e70a6 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/correlations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + 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 diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 4baf37a3657..0a19120fe80 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,7 +5,7 @@ go 1.25.5 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 9c8cde10af6..0faeaaf78ba 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -85,8 +85,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.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.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/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go index a267e0c8df8..ac8a0a61685 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go index 5b2da44ec05..1ec0884202f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go index d917cebc0bf..64924eac264 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go @@ -23,6 +23,12 @@ type Snapshot struct { Spec SnapshotSpec `json:"spec" yaml:"spec"` } +func NewSnapshot() *Snapshot { + return &Snapshot{ + Spec: *NewSnapshotSpec(), + } +} + func (o *Snapshot) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go index b6086c5fd1f..596c5bb2890 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Snapshot{}, &SnapshotList{}, resource.WithKind("Snapshot"), + schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewSnapshot(), &SnapshotList{}, resource.WithKind("Snapshot"), resource.WithPlural("snapshots"), resource.WithScope(resource.NamespacedScope)) kindSnapshot = resource.Kind{ Schema: schemaSnapshot, diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go index be021b5f003..35bb8900ab0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go index e944e0afc33..006312837e0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go index 99cf7df0da9..6a06594656e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go index 136698cf70f..1a5f27d0fb5 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go index 5076b7d9b0c..bb64a321a1d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go index 30c2237ca31..35d87fd07f7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/example/go.mod b/apps/example/go.mod index deb8763474c..d63d23be302 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/example go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe k8s.io/apimachinery v0.34.2 diff --git a/apps/example/go.sum b/apps/example/go.sum index 4a43a44d46c..70c83a9fe37 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.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.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/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index c40b11f7add..476c6949c73 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,42 +3,67 @@ module github.com/grafana/grafana/apps/folder go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e 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/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/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-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag v0.23.1 // 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/grafana/grafana-app-sdk/logging v0.48.3 // 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/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/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // 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/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 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/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 ) diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 286d9142c77..d8454185267 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -1,3 +1,7 @@ +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/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= @@ -6,6 +10,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= @@ -16,6 +22,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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-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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -23,20 +31,33 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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/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/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.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/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= +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= @@ -45,9 +66,29 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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/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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= @@ -58,10 +99,20 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ 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/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/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +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= @@ -77,16 +128,24 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL 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/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= @@ -100,12 +159,18 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j 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/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= diff --git a/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go b/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go index 226af606f6f..6fabb8f8958 100644 --- a/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go +++ b/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go @@ -23,6 +23,12 @@ type Folder struct { Spec FolderSpec `json:"spec" yaml:"spec"` } +func NewFolder() *Folder { + return &Folder{ + Spec: *NewFolderSpec(), + } +} + func (o *Folder) GetSpec() any { return o.Spec } diff --git a/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go b/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go index f0d4fffe6b0..e626e4773ee 100644 --- a/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go +++ b/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", &Folder{}, &FolderList{}, resource.WithKind("Folder"), + schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", NewFolder(), &FolderList{}, resource.WithKind("Folder"), resource.WithPlural("folders"), resource.WithScope(resource.NamespacedScope)) kindFolder = resource.Kind{ Schema: schemaFolder, diff --git a/apps/folder/pkg/apis/manifestdata/folder_manifest.go b/apps/folder/pkg/apis/manifestdata/folder_manifest.go new file mode 100644 index 00000000000..7c053e52c38 --- /dev/null +++ b/apps/folder/pkg/apis/manifestdata/folder_manifest.go @@ -0,0 +1,116 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +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" + + v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" +) + +var appManifestData = app.ManifestData{ + AppName: "folder", + Group: "folder.grafana.app", + PreferredVersion: "v1beta1", + Versions: []app.ManifestVersion{ + { + Name: "v1beta1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Folder", + Plural: "Folders", + Scope: "Namespaced", + Conversion: false, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + 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("folder") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Folder/v1beta1": v1beta1.FolderKind(), +} + +// 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{} + +// 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 "/" 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) +} diff --git a/apps/iam/go.mod b/apps/iam/go.mod index bfe1fa1c50d..474779e0efe 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,7 +52,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 4c25bd92d43..7f2fd8f462f 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -835,8 +835,8 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -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.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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 0faf2c40efb..3844fec2689 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 k8s.io/apimachinery v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index b70fb8a42f5..2a35278d19a 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/logsdrilldown go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + 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 diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 4bdbe8c003c..ae38e3d0cd3 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J 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.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.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= 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.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index c55674cc950..669c7c46844 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -10,8 +10,9 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/emicklei/go-restful/v3 v3.13.0 + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 @@ -59,7 +60,7 @@ require ( 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 v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/go-test/deep v1.1.1 // indirect @@ -75,9 +76,8 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect + github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect @@ -142,7 +142,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/alertmanager v0.28.0 // indirect + github.com/prometheus/alertmanager v0.28.2 // 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 @@ -194,8 +194,8 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // 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/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -215,3 +215,6 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +// Use our fork of the upstream Alertmanager. +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index d582270e43b..1d7387b28b3 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -110,8 +110,8 @@ github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo 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 v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= @@ -174,8 +174,8 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= -github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= +github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= +github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -184,8 +184,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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.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.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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= @@ -196,6 +196,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= @@ -368,8 +370,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/alertmanager v0.28.0 h1:sLN+6HhZet8hrbmGHLAHWsTXgZSVCvq9Ix3U3wvivqc= -github.com/prometheus/alertmanager v0.28.0/go.mod h1:/okSnb2LlodbMlRoOWQEKtqI/coOo2NKZDm2Hu9QHLQ= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -611,10 +611,10 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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-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/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 9a5c8088c0f..661002a3b59 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 22bf6a8cbcd..75a19848d73 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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.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.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/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index e84ab0d35da..3cccd7dd8be 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.4 // indirect + github.com/grafana/grafana-app-sdk v0.48.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 791bf68e797..0e1ba180fad 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -62,8 +62,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.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.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/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= diff --git a/apps/sdk.mk b/apps/sdk.mk index 62d89ed8ed3..68cf4c1c9c9 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.4 +APP_SDK_VERSION = v0.48.5 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index da71ef8e666..a1ed48be6e7 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 950843f4a7d..166e281f5af 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,8 +37,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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.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.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/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 128609888ba..4a414de5b4e 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index c7f57094324..58ecc31fd82 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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.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.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/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= diff --git a/go.mod b/go.mod index 7ef730e391d..e589e5a18a5 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.48.4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.5 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 7723cfa19a9..91104bfeaf4 100644 --- a/go.sum +++ b/go.sum @@ -1635,8 +1635,8 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -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.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/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/go.work.sum b/go.work.sum index 36bd9f3510d..3017d8eb878 100644 --- a/go.work.sum +++ b/go.work.sum @@ -775,7 +775,6 @@ github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -2084,6 +2083,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go. 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= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= @@ -2114,6 +2114,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go. 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= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= From 8e11851bb085fac6ce2438d4b7dd5d340100ce48 Mon Sep 17 00:00:00 2001 From: Austin Pond Date: Sat, 6 Dec 2025 03:01:28 -0500 Subject: [PATCH 321/423] =?UTF-8?q?Dashboards:=20Use=20the=20OpenAPI=20gen?= =?UTF-8?q?erated=20by=20app-sdk=20in=20the=20manifest=20to=20=E2=80=A6=20?= =?UTF-8?q?(#114858)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dashboard/Makefile | 3 +- apps/dashboard/pkg/apis/dashboard_manifest.go | 24 + pkg/registry/apis/dashboard/register.go | 45 + .../dashboard.grafana.app-v2alpha1.json | 2061 ++++---- .../dashboard.grafana.app-v2beta1.json | 4591 +++++++++++++++++ pkg/tests/apis/openapi_test.go | 3 + 6 files changed, 5552 insertions(+), 1175 deletions(-) create mode 100644 pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index 3d5c7060199..7ff9b946dc9 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -11,8 +11,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati --tsgenpath=../../packages/grafana-schema/src/schema \ --grouping=group \ --defencoding=none \ - --genoperatorstate=false \ - --noschemasinmanifest + --genoperatorstate=false .PHONY: post-generate-cleanup post-generate-cleanup: ## Clean up the generated code diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 974062efcec..c4e35bd8f40 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -6,6 +6,7 @@ package apis import ( + "encoding/json" "fmt" "strings" @@ -21,6 +22,24 @@ import ( v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) +var ( + rawSchemaDashboardv0alpha1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv0alpha1, &versionSchemaDashboardv0alpha1) + rawSchemaSnapshotv0alpha1 = []byte(`{"Snapshot":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"dashboard":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"The raw dashboard (unstructured for now)","type":"object"},"expires":{"default":0,"description":"Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds)","type":"integer"},"external":{"default":false,"description":"When set to true, the snapshot exists in a remote server","type":"boolean"},"externalUrl":{"description":"The external URL where the snapshot can be seen","type":"string"},"originalUrl":{"description":"The URL that created the dashboard originally","type":"string"},"timestamp":{"description":"Snapshot creation timestamp","type":"string"},"title":{"description":"Snapshot title","type":"string"}},"type":"object"}}`) + versionSchemaSnapshotv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaSnapshotv0alpha1, &versionSchemaSnapshotv0alpha1) + rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv1beta1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv2alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv2beta1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) +) + var appManifestData = app.ManifestData{ AppName: "dashboard", Group: "dashboard.grafana.app", @@ -35,6 +54,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv0alpha1, }, { @@ -42,6 +62,7 @@ var appManifestData = app.ManifestData{ Plural: "Snapshots", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaSnapshotv0alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -60,6 +81,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv1beta1, }, }, Routes: app.ManifestVersionRoutes{ @@ -78,6 +100,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv2alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -96,6 +119,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv2beta1, }, }, Routes: app.ManifestVersionRoutes{ diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 16873ec2101..e9925f0a3b7 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -24,6 +24,7 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" + manifestdata "github.com/grafana/grafana/apps/dashboard/pkg/apis" internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" @@ -843,6 +844,50 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref)) maps.Copy(defs, dashv2alpha1.GetOpenAPIDefinitions(ref)) maps.Copy(defs, dashv2beta1.GetOpenAPIDefinitions(ref)) + md := manifestdata.LocalManifest().ManifestData + // Overwrite the OpenAPI generated from kubernetes (sourced from the go types) with the OpenAPI generated by grafana-app-sdk + // from the manifest CUE, as it correctly handles the CUE disjunctions in the dashboard spec. + // We don't touch any types which were not specified in the manifest CUE (such as custom route types). + for _, version := range md.Versions { + // We don't need to correct the v0 or v1 openAPI as the spec type is just `any` + if len(version.Name) > 1 && (version.Name[1] == '0' || version.Name[1] == '1') { + continue + } + for _, kind := range version.Kinds { + pkgPrefix := fmt.Sprintf("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/%s", version.Name) + oapi, err := kind.Schema.AsKubeOpenAPI(schema.GroupVersionKind{ + Group: md.Group, + Version: version.Name, + Kind: kind.Kind, + }, ref, pkgPrefix) + if err != nil { + logging.DefaultLogger.Error("unable to generate openAPI for kind %s: %w", kind.Kind, err) + continue + } + maps.Copy(defs, oapi) + } + } + + // Fix legacyOptions schema for v2alpha1 and v2beta1 to allow any value type + // The generated schema incorrectly restricts values to objects, but map[string]interface{} can hold any type + // This fix must be applied here so structured-merge-diff uses the correct schema + // For some reason this issue occurs with both the kubernetes-generated openAPI sourced from go, _and_ the OpenAPI from the AppManifest + // TODO: @IfSentient this should really be addressed in the app-sdk's generation, or work out what about this particular CUE value is broken + for _, defKey := range []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec", + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec", + } { + if def, ok := defs[defKey]; ok { + if legacyOptions, ok := def.Schema.Properties["legacyOptions"]; ok { + // Fix: Use additionalProperties: true to allow any value type (string, number, boolean, array, object, etc.) + // instead of restricting to objects only. This must match map[string]interface{} semantics. + legacyOptions.AdditionalProperties = &spec.SchemaOrBool{Allows: true} + def.Schema.Properties["legacyOptions"] = legacyOptions + defs[defKey] = def + } + } + } + return defs } } diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 567636ff7fc..2cad6213d04 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -968,9 +968,10 @@ "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.Dashboard": { "type": "object", "required": [ + "kind", + "apiVersion", "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -990,21 +991,10 @@ ] }, "spec": { - "description": "Spec is the spec of the Dashboard", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus" } }, "x-kubernetes-group-version-kind": [ @@ -1085,28 +1075,35 @@ "type": "boolean" }, "style": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1ActionStyle" + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + } + }, + "additionalProperties": false }, "title": { - "type": "string", - "default": "" + "type": "string" }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionType" }, "variables": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable" } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionType": { + "type": "string", + "enum": [ + "fetch", + "infinity" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable": { "type": "object", @@ -1117,18 +1114,20 @@ ], "properties": { "key": { - "type": "string", - "default": "" + "type": "string" }, "name": { - "type": "string", - "default": "" + "type": "string" }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariableType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariableType": { + "description": "Action variable type", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels": { "description": "Define the AdHocFilterWithLabels type", @@ -1147,38 +1146,34 @@ "type": "boolean" }, "key": { - "type": "string", - "default": "" + "type": "string" }, "keyLabel": { "type": "string" }, "operator": { - "type": "string", - "default": "" - }, - "origin": { "type": "string" }, + "origin": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFilterOrigin" + }, "value": { - "type": "string", - "default": "" + "type": "string" }, "valueLabels": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "values": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind": { "description": "Adhoc variable kind", @@ -1189,18 +1184,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec": { "description": "Adhoc variable specification", @@ -1217,17 +1207,12 @@ "properties": { "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "baseFilters": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" } }, "datasource": { @@ -1236,12 +1221,7 @@ "defaultKeys": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue" } }, "description": { @@ -1250,17 +1230,11 @@ "filters": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" } }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -1273,7 +1247,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping": { "description": "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.", @@ -1285,13 +1260,15 @@ }, "source": { "description": "Source type for the field value", - "type": "string" + "type": "string", + "default": "field" }, "value": { "description": "Constant value to use when source is \"text\"", "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationPanelFilter": { "type": "object", @@ -1301,18 +1278,18 @@ "properties": { "exclude": { "description": "Should the specified panels be included or excluded", - "type": "boolean" + "type": "boolean", + "default": false }, "ids": { "description": "Panel IDs that should be included or excluded", "type": "array", "items": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind": { "type": "object", @@ -1322,18 +1299,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec": { "type": "object", @@ -1345,53 +1317,44 @@ ], "properties": { "builtIn": { - "type": "boolean" + "type": "boolean", + "default": false }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" }, "enable": { - "type": "boolean", - "default": false + "type": "boolean" }, "filter": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationPanelFilter" }, "hide": { - "type": "boolean", - "default": false + "type": "boolean" }, "iconColor": { - "type": "string", - "default": "" + "type": "string" }, "legacyOptions": { "description": "Catch-all field for datasource-specific properties", "type": "object", - "additionalProperties": { - "type": "object" - } + "additionalProperties": true }, "mappings": { "description": "Mappings define how to convert data frame fields to annotation event fields.", "type": "object", "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping" } }, "name": { - "type": "string", - "default": "" + "type": "string" }, "query": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind": { "type": "object", @@ -1401,18 +1364,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec": { "type": "object", @@ -1424,17 +1382,13 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind" }, "element": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridRepeatOptions" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind": { "type": "object", @@ -1444,18 +1398,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec": { "type": "object", @@ -1466,40 +1415,47 @@ ], "properties": { "columnWidth": { - "type": "number", - "format": "double" + "type": "number" }, "columnWidthMode": { "type": "string", - "default": "" + "default": "standard", + "enum": [ + "narrow", + "standard", + "wide", + "custom" + ] }, "fillScreen": { - "type": "boolean" + "type": "boolean", + "default": false }, "items": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind" } }, "maxColumnCount": { "type": "number", - "format": "double" + "default": 3 }, "rowHeight": { - "type": "number", - "format": "double" + "type": "number" }, "rowHeightMode": { "type": "string", - "default": "" + "default": "standard", + "enum": [ + "short", + "standard", + "tall", + "custom" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridRepeatOptions": { "type": "object", @@ -1509,14 +1465,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind": { "type": "object", @@ -1526,18 +1481,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec": { "type": "object", @@ -1546,10 +1496,10 @@ ], "properties": { "value": { - "type": "boolean", - "default": false + "type": "boolean" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind": { "type": "object", @@ -1559,18 +1509,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec": { "type": "object", @@ -1582,19 +1527,36 @@ "properties": { "condition": { "type": "string", - "default": "" + "enum": [ + "and", + "or" + ] }, "items": { "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind" + } + ] } }, "visibility": { "type": "string", - "default": "" + "enum": [ + "show", + "hide" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": { "type": "object", @@ -1604,18 +1566,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": { "type": "object", @@ -1624,10 +1581,10 @@ ], "properties": { "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind": { "type": "object", @@ -1637,32 +1594,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": { - "type": "object", - "properties": { - "ConditionalRenderingDataKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind" - }, - "ConditionalRenderingTimeRangeSizeKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind" - }, - "ConditionalRenderingVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec": { "type": "object", @@ -1674,17 +1612,21 @@ "properties": { "operator": { "type": "string", - "default": "" + "enum": [ + "equals", + "notEquals", + "matches", + "notMatches" + ] }, "value": { - "type": "string", - "default": "" + "type": "string" }, "variable": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind": { "description": "Constant variable kind", @@ -1695,18 +1637,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec": { "description": "Constant variable specification", @@ -1720,19 +1657,13 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -1749,7 +1680,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus": { "description": "ConversionStatus is the status of the conversion of the dashboard.", @@ -1759,23 +1691,24 @@ ], "properties": { "error": { - "description": "The error message from the conversion. Empty if the conversion has not failed.", + "description": "The error message from the conversion.\nEmpty if the conversion has not failed.", "type": "string" }, "failed": { - "description": "Whether from another version has failed. If true, means that the dashboard is not valid, and the caller should instead fetch the stored version.", - "type": "boolean", - "default": false + "description": "Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.", + "type": "boolean" }, "source": { "description": "The original value map[string]any", - "type": "object" + "type": "object", + "additionalProperties": {} }, "storedVersion": { - "description": "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", + "description": "The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.", "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind": { "description": "Custom variable kind", @@ -1786,18 +1719,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec": { "description": "Custom variable specification", @@ -1819,22 +1747,16 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -1854,12 +1776,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "query": { @@ -1870,7 +1787,17 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardCursorSync": { + "description": "\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.", + "type": "string", + "enum": [ + "Crosshair", + "Tooltip", + "Off" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink": { "description": "Links with references to other dashboards or external resources", @@ -1894,8 +1821,7 @@ }, "icon": { "description": "Icon name to be displayed with the link", - "type": "string", - "default": "" + "type": "string" }, "includeVars": { "description": "If true, includes current template variables values in the link as query params", @@ -1908,15 +1834,13 @@ "default": false }, "placement": { - "description": "Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkPlacement" }, "tags": { "description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards", "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "targetBlank": { @@ -1926,24 +1850,33 @@ }, "title": { "description": "Title to display with the link", - "type": "string", - "default": "" + "type": "string" }, "tooltip": { "description": "Tooltip to display when the user hovers their mouse over it", - "type": "string", - "default": "" + "type": "string" }, "type": { - "description": "Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType`", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkType" }, "url": { "description": "Link URL. Only required/valid if the type is link", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkPlacement": { + "description": "Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkType": { + "description": "Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)", + "type": "string", + "enum": [ + "link", + "dashboards" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink": { "type": "object", @@ -1956,14 +1889,13 @@ "type": "boolean" }, "title": { - "type": "string", - "default": "" + "type": "string" }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind": { "type": "object", @@ -1974,16 +1906,17 @@ "properties": { "kind": { "description": "The kind of a DataQueryKind is the datasource type", - "type": "string", - "default": "" + "type": "string" }, "spec": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef": { "type": "object", @@ -1996,10 +1929,20 @@ "description": "Specific datasource instance", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTopic": { + "description": "A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.", + "type": "string", + "enum": [ + "series", + "annotations", + "alertStates" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig": { - "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization. Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, use the output of one transformation as the input to another transformation, etc.", + "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.", "type": "object", "required": [ "id", @@ -2011,27 +1954,22 @@ "type": "boolean" }, "filter": { - "description": "Optional frame matcher. When missing it will be applied to all results", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" }, "id": { "description": "Unique identifier of transformer", - "type": "string", - "default": "" + "type": "string" }, "options": { - "description": "Options to be passed to the transformer Valid options depend on the transformer id", - "type": "object" + "description": "Options to be passed to the transformer\nValid options depend on the transformer id", + "type": "object", + "additionalProperties": {} }, "topic": { - "description": "Where to pull DataFrames from as input to transformation", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTopic" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind": { "description": "Datasource variable kind", @@ -2042,18 +1980,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec": { "description": "Datasource variable specification", @@ -2077,22 +2010,16 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -2112,12 +2039,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "pluginId": { @@ -2125,8 +2047,7 @@ "default": "" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "regex": { "type": "string", @@ -2136,7 +2057,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue": { "type": "object", @@ -2149,9 +2071,22 @@ "default": "" }, "value": { - "type": "object" + "type": "object", + "additionalProperties": {} } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElement": { + "description": "Supported dashboard elements\n|* more element types in the future", + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind" + } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference": { "type": "object", @@ -2161,14 +2096,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "name": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFetchOptions": { "type": "object", @@ -2185,31 +2119,28 @@ "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "method": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod" }, "queryParams": { - "description": "These are 2D arrays of strings, each representing a key-value pair We are defining them this way because we can't generate a go struct that that would have exactly two strings in each sub-array", + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor": { "description": "Map a field to a color.", @@ -2223,51 +2154,74 @@ "type": "string" }, "mode": { - "description": "The main color scheme mode.", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorModeId" }, "seriesBy": { - "description": "Some visualizations need to know how to assign a series color from by value color schemes.", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorSeriesByMode" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorModeId": { + "description": "Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n`thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold\n`palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n`palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n`continuous-viridis`: Continuous Viridis palette mode\n`continuous-magma`: Continuous Magma palette mode\n`continuous-plasma`: Continuous Plasma palette mode\n`continuous-inferno`: Continuous Inferno palette mode\n`continuous-cividis`: Continuous Cividis palette mode\n`continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode\n`continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode\n`continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode\n`continuous-YlRd`: Continuous Yellow-Red palette mode\n`continuous-BlPu`: Continuous Blue-Purple palette mode\n`continuous-YlBl`: Continuous Yellow-Blue palette mode\n`continuous-blues`: Continuous Blue palette mode\n`continuous-reds`: Continuous Red palette mode\n`continuous-greens`: Continuous Green palette mode\n`continuous-purples`: Continuous Purple palette mode\n`shades`: Shades of a single color. Specify a single color, useful in an override rule.\n`fixed`: Fixed color mode. Specify a single color, useful in an override rule.", + "type": "string", + "enum": [ + "thresholds", + "palette-classic", + "palette-classic-by-name", + "continuous-viridis", + "continuous-magma", + "continuous-plasma", + "continuous-inferno", + "continuous-cividis", + "continuous-GrYlRd", + "continuous-RdYlGr", + "continuous-BlYlRd", + "continuous-YlRd", + "continuous-BlPu", + "continuous-YlBl", + "continuous-blues", + "continuous-reds", + "continuous-greens", + "continuous-purples", + "fixed", + "shades" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorSeriesByMode": { + "description": "Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.", + "type": "string", + "enum": [ + "min", + "max", + "last" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig": { - "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", "type": "object", "properties": { "actions": { "description": "Define interactive HTTP requests that can be triggered from data visualizations.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAction" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAction" } }, "color": { - "description": "Panel color configuration", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor" }, "custom": { - "description": "custom is specified by the FieldConfig field in panel plugin schemas.", + "description": "custom is specified by the FieldConfig field\nin panel plugin schemas.", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "decimals": { - "description": "Specify the number of decimals Grafana includes in the rendered value. If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. For example 1.1234 will display as 1.12 and 100.456 will display as 100. To display all decimals, set the unit to `String`.", - "type": "number", - "format": "double" + "description": "Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to `String`.", + "type": "number" }, "description": { "description": "Human readable field metadata", @@ -2278,7 +2232,7 @@ "type": "string" }, "displayNameFromDS": { - "description": "This can be used by data sources that return and explicit naming structure for values and labels When this property is configured, this value is used rather than the default naming strategy.", + "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", "type": "string" }, "filterable": { @@ -2289,54 +2243,49 @@ "description": "The behavior when clicking on a result", "type": "array", "items": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "mappings": { "description": "Convert input values into a display string", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapping" } }, "max": { "description": "The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", - "type": "number", - "format": "double" + "type": "number" }, "min": { "description": "The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", - "type": "number", - "format": "double" + "type": "number" }, "noValue": { "description": "Alternative to empty string", "type": "string" }, "path": { - "description": "An explicit path to the field in the datasource. When the frame meta includes a path, This will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and may be used to update the results", + "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", "type": "string" }, "thresholds": { - "description": "Map numeric values to states", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig" }, "unit": { - "description": "Unit a field should use. The unit you select is applied to all fields except time. You can use the units ID availables in Grafana or a custom unit. Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts As custom unit, you can use the following formats: `suffix:\u003csuffix\u003e` for custom unit that should go after value. `prefix:\u003cprefix\u003e` for custom unit that should go before value. `time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`. `si:\u003cbase scale\u003e\u003cunit characters\u003e` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. `count:\u003cunit\u003e` for a custom count unit. `currency:\u003cunit\u003e` for custom a currency unit.", + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", "type": "string" }, "writeable": { "description": "True if data source can write a value to the path. Auth/authz are supported separately", "type": "boolean" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource": { - "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", "type": "object", "required": [ "defaults", @@ -2344,27 +2293,41 @@ ], "properties": { "defaults": { - "description": "Defaults are the options applied to all fields.", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig" }, "overrides": { "description": "Overrides are the options applied to specific fields overriding the defaults.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides" + "type": "object", + "required": [ + "matcher", + "properties" + ], + "properties": { + "__systemRef": { + "description": "Describes config override rules created when interacting with Grafana.", + "type": "string" + }, + "matcher": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue" + } } - ] + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFilterOrigin": { + "description": "Determine the origin of the adhoc variable filter", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind": { "type": "object", @@ -2374,18 +2337,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec": { "type": "object", @@ -2398,38 +2356,25 @@ ], "properties": { "element": { - "description": "reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" }, "height": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatOptions" }, "width": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "x": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "y": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind": { "type": "object", @@ -2439,52 +2384,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": { - "type": "object", - "properties": { - "AutoGridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" - }, - "GridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" - }, - "RowsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" - }, - "TabsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": { - "type": "object", - "properties": { - "AutoGridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" - }, - "GridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" - }, - "RowsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" - }, - "TabsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec": { "type": "object", @@ -2495,15 +2401,11 @@ "items": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind": { "description": "Group variable kind", @@ -2514,18 +2416,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec": { "description": "GroupBy variable specification", @@ -2540,12 +2437,7 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" @@ -2557,8 +2449,7 @@ "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -2574,19 +2465,25 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "skipUrlSync": { "type": "boolean", "default": false } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "PATCH" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardInfinityOptions": { "type": "object", @@ -2600,39 +2497,35 @@ "type": "string" }, "datasourceUid": { - "type": "string", - "default": "" + "type": "string" }, "headers": { "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "method": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod" }, "queryParams": { - "description": "These are 2D arrays of strings, each representing a key-value pair We are defining them this way because we can't generate a go struct that that would have exactly two strings in each sub-array", + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind": { "description": "Interval variable kind", @@ -2643,18 +2536,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec": { "description": "Interval variable specification", @@ -2678,7 +2566,6 @@ }, "auto_count": { "type": "integer", - "format": "int64", "default": 0 }, "auto_min": { @@ -2686,19 +2573,13 @@ "default": "" }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -2710,12 +2591,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "query": { @@ -2723,14 +2599,14 @@ "default": "" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "skipUrlSync": { "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind": { "type": "object", @@ -2740,18 +2616,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec": { "type": "object", @@ -2763,27 +2634,20 @@ "properties": { "id": { "description": "Panel ID for the library panel in the dashboard", - "type": "number", - "format": "double", - "default": 0 + "type": "number" }, "libraryPanel": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef" }, "title": { "description": "Title for the library panel in the dashboard", - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef": { - "description": "A library panel is a reusable panel that you can use in any dashboard. When you make a change to a library panel, that change propagates to all instances of where the panel is used. Library panels streamline reuse of panels across multiple dashboards.", + "description": "A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.", "type": "object", "required": [ "name", @@ -2792,15 +2656,14 @@ "properties": { "name": { "description": "Library panel name", - "type": "string", - "default": "" + "type": "string" }, "uid": { "description": "Library panel uid", - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardList": { "type": "object", @@ -2845,8 +2708,18 @@ } ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType": { + "description": "Supported value mapping types\n`value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n`range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n`regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n`special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "type": "string", + "enum": [ + "value", + "range", + "regex", + "special" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig": { - "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", + "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", "type": "object", "required": [ "id" @@ -2859,9 +2732,11 @@ }, "options": { "description": "The matcher options. This is specific to the matcher implementation.", - "type": "object" + "type": "object", + "additionalProperties": {} } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue": { "description": "Define the MetricFindValue type", @@ -2877,13 +2752,20 @@ "type": "string" }, "text": { - "type": "string", - "default": "" + "type": "string" }, "value": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrFloat64" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind": { "type": "object", @@ -2893,29 +2775,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKindOrLibraryPanelKind": { - "type": "object", - "properties": { - "LibraryPanelKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind" - }, - "PanelKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind": { "type": "object", @@ -2925,18 +2791,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec": { "type": "object", @@ -2950,22 +2811,16 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" }, "hidden": { - "type": "boolean", - "default": false + "type": "boolean" }, "query": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" }, "refId": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec": { "type": "object", @@ -2979,49 +2834,31 @@ ], "properties": { "data": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind" }, "description": { - "type": "string", - "default": "" + "type": "string" }, "id": { - "type": "number", - "format": "double", - "default": 0 + "type": "number" }, "links": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink" } }, "title": { - "type": "string", - "default": "" + "type": "string" }, "transparent": { "type": "boolean" }, "vizConfig": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind": { "type": "object", @@ -3031,18 +2868,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec": { "type": "object", @@ -3055,34 +2887,20 @@ "queries": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind" } }, "queryOptions": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec" }, "transformations": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec": { "type": "object", @@ -3097,12 +2915,10 @@ "type": "string" }, "maxDataPoints": { - "type": "integer", - "format": "int64" + "type": "integer" }, "queryCachingTTL": { - "type": "integer", - "format": "int64" + "type": "integer" }, "timeFrom": { "type": "string" @@ -3110,7 +2926,8 @@ "timeShift": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind": { "description": "Query variable kind", @@ -3121,50 +2938,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": { - "type": "object", - "properties": { - "AdhocVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind" - }, - "ConstantVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind" - }, - "CustomVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind" - }, - "DatasourceVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind" - }, - "GroupByVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind" - }, - "IntervalVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind" - }, - "QueryVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" - }, - "SwitchVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" - }, - "TextVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec": { "description": "Query variable specification", @@ -3189,15 +2969,10 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" @@ -3209,8 +2984,7 @@ "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -3230,28 +3004,17 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "placeholder": { "type": "string" }, "query": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "regex": { "type": "string", @@ -3262,27 +3025,27 @@ "default": false }, "sort": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort" }, "staticOptions": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "staticOptionsOrder": { - "type": "string" + "type": "string", + "enum": [ + "before", + "after", + "sorted" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRangeMap": { - "description": "Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", + "description": "Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", "type": "object", "required": [ "type", @@ -3291,21 +3054,35 @@ "properties": { "options": { "description": "Range to match against and the result to apply when the value is within the range", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RangeMapOptions" + "type": "object", + "required": [ + "from", + "to", + "result" + ], + "properties": { + "from": { + "description": "Min value of the range. It can be null which means -Infinity", + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" + }, + "to": { + "description": "Max value of the range. It can be null which means +Infinity", + "type": "number" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRegexMap": { - "description": "Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", + "description": "Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", "type": "object", "required": [ "type", @@ -3314,18 +3091,31 @@ "properties": { "options": { "description": "Regular expression to match against and the result to apply when the value matches the regex", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RegexMapOptions" + "type": "object", + "required": [ + "pattern", + "result" + ], + "properties": { + "pattern": { + "description": "Regular expression to match against", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode": { + "description": "other repeat modes will be added in the future: label, frame", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatOptions": { "type": "object", @@ -3335,21 +3125,23 @@ ], "properties": { "direction": { - "type": "string" + "type": "string", + "enum": [ + "h", + "v" + ] }, "maxPerRow": { - "type": "integer", - "format": "int64" + "type": "integer" }, "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowRepeatOptions": { "type": "object", @@ -3359,14 +3151,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind": { "type": "object", @@ -3376,18 +3167,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind": { "type": "object", @@ -3397,18 +3183,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec": { "type": "object", @@ -3429,7 +3210,20 @@ "type": "boolean" }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + } + ] }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowRepeatOptions" @@ -3437,7 +3231,8 @@ "title": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec": { "type": "object", @@ -3448,15 +3243,11 @@ "rows": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec": { "type": "object", @@ -3476,18 +3267,11 @@ "annotations": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind" } }, "cursorSync": { - "description": "Configuration of dashboard cursor sync behavior. \"Off\" for no shared crosshair or tooltip (default). \"Crosshair\" for shared crosshair. \"Tooltip\" for shared crosshair AND shared tooltip.", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardCursorSync" }, "description": { "description": "Description of dashboard.", @@ -3495,31 +3279,40 @@ }, "editable": { "description": "Whether a dashboard is editable or not.", - "type": "boolean" + "type": "boolean", + "default": true }, "elements": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKindOrLibraryPanelKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElement" } }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + } + ] }, "links": { "description": "Links with references to other dashboards or external websites.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink" } }, "liveNow": { - "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width. This will keep data \"moving left\" regardless of the query refresh rate. This setting helps avoid dashboards presenting stale live data.", + "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.", "type": "boolean" }, "preload": { @@ -3528,42 +3321,35 @@ "default": false }, "revision": { - "description": "Plugins only. The version of the dashboard installed together with the plugin. This is used to determine if the dashboard should be updated when the plugin is updated.", - "type": "integer", - "format": "int32" + "description": "Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.", + "type": "integer" }, "tags": { "description": "Tags associated with dashboard.", "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "timeSettings": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec" }, "title": { "description": "Title of dashboard.", - "type": "string", - "default": "" + "type": "string" }, "variables": { "description": "Configured template variables.", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMap": { - "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.", "type": "object", "required": [ "type", @@ -3571,58 +3357,47 @@ ], "properties": { "options": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1SpecialValueMapOptions" + "type": "object", + "required": [ + "match", + "result" + ], + "properties": { + "match": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMatch" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMatch": { + "description": "Special value types supported by the `SpecialValueMap`", + "type": "string", + "enum": [ + "true", + "false", + "null", + "nan", + "null+nan", + "empty" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus": { "type": "object", "properties": { "conversion": { - "description": "Optional conversion status.", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString": { - "type": "object", - "properties": { - "ArrayOfString": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "String": { - "type": "string" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrFloat64": { - "type": "object", - "properties": { - "Float64": { - "type": "number", - "format": "double" - }, - "String": { - "type": "string" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind": { "type": "object", @@ -3632,18 +3407,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec": { "description": "Switch variable specification", @@ -3659,22 +3429,21 @@ "properties": { "current": { "type": "string", - "default": "" + "default": "false" }, "description": { "type": "string" }, "disabledValue": { "type": "string", - "default": "" + "default": "false" }, "enabledValue": { "type": "string", - "default": "" + "default": "true" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -3687,7 +3456,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { "type": "object", @@ -3697,14 +3467,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind": { "type": "object", @@ -3714,18 +3483,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec": { "type": "object", @@ -3736,15 +3500,11 @@ "tabs": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind": { "type": "object", @@ -3754,18 +3514,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec": { "type": "object", @@ -3777,7 +3532,20 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind" }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + } + ] }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions" @@ -3785,7 +3553,8 @@ "title": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind": { "description": "Text variable kind", @@ -3796,18 +3565,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec": { "description": "Text variable specification", @@ -3821,19 +3585,13 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -3850,7 +3608,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold": { "type": "object", @@ -3860,15 +3619,14 @@ ], "properties": { "color": { - "type": "string", - "default": "" + "type": "string" }, "value": { "description": "Value null means -Infinity", - "type": "number", - "format": "double" + "type": "number" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig": { "type": "object", @@ -3878,21 +3636,23 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsMode" }, "steps": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold" } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsMode": { + "type": "string", + "enum": [ + "absolute", + "percentage" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption": { "type": "object", @@ -3904,20 +3664,21 @@ "properties": { "display": { "type": "string", - "default": "" + "default": "Last 6 hours" }, "from": { "type": "string", - "default": "" + "default": "now-6h" }, "to": { "type": "string", - "default": "" + "default": "now" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec": { - "description": "Time configuration It defines the default time config for the time picker, the refresh picker for the specific dashboard.", + "description": "Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.", "type": "object", "required": [ "from", @@ -3929,64 +3690,76 @@ ], "properties": { "autoRefresh": { - "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\". v1: refresh", + "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh", "type": "string", "default": "" }, "autoRefreshIntervals": { - "description": "Interval options available in the refresh picker dropdown. v1: timepicker.refresh_intervals", + "description": "Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals", "type": "array", + "default": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], "items": { - "type": "string", - "default": "" + "type": "string" } }, "fiscalYearStartMonth": { "description": "The month that the fiscal year starts on. 0 = January, 11 = December", "type": "integer", - "format": "int64", "default": 0 }, "from": { - "description": "Start time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "description": "Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", "type": "string", - "default": "" + "default": "now-6h" }, "hideTimepicker": { - "description": "Whether timepicker is visible or not. v1: timepicker.hidden", + "description": "Whether timepicker is visible or not.\nv1: timepicker.hidden", "type": "boolean", "default": false }, "nowDelay": { - "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. v1: timepicker.nowDelay", + "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay", "type": "string" }, "quickRanges": { - "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. v1: timepicker.quick_ranges , not exposed in the UI", + "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption" } }, "timezone": { "description": "Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".", - "type": "string" + "type": "string", + "default": "browser" }, "to": { - "description": "End time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "description": "End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", "type": "string", - "default": "" + "default": "now" }, "weekStart": { "description": "Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".", - "type": "string" + "type": "string", + "enum": [ + "saturday", + "monday", + "sunday" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind": { "type": "object", @@ -3997,136 +3770,16 @@ "properties": { "kind": { "description": "The kind of a TransformationKind is the transformation ID", - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1ActionStyle": { - "type": "object", - "properties": { - "backgroundColor": { - "type": "string" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": { - "type": "object", - "required": [ - "matcher", - "properties" - ], - "properties": { - "__systemRef": { - "description": "Describes config override rules created when interacting with Grafana.", - "type": "string" - }, - "matcher": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" - } - ] - }, - "properties": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RangeMapOptions": { - "type": "object", - "required": [ - "from", - "to", - "result" - ], - "properties": { - "from": { - "description": "Min value of the range. It can be null which means -Infinity", - "type": "number", - "format": "double" - }, - "result": { - "description": "Config to apply when the value is within the range", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - }, - "to": { - "description": "Max value of the range. It can be null which means +Infinity", - "type": "number", - "format": "double" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RegexMapOptions": { - "type": "object", - "required": [ - "pattern", - "result" - ], - "properties": { - "pattern": { - "description": "Regular expression to match against", - "type": "string", - "default": "" - }, - "result": { - "description": "Config to apply when the value matches the regex", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1SpecialValueMapOptions": { - "type": "object", - "required": [ - "match", - "result" - ], - "properties": { - "match": { - "description": "Special value to match against", - "type": "string", - "default": "" - }, - "result": { - "description": "Config to apply when the value matches the special value", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap": { - "description": "Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", + "description": "Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", "type": "object", "required": [ "type", @@ -4134,39 +3787,33 @@ ], "properties": { "options": { - "description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", "type": "object", "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": { - "type": "object", - "properties": { - "RangeMap": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapping": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap" + }, + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRangeMap" }, - "RegexMap": { + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRegexMap" }, - "SpecialValueMap": { + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMap" - }, - "ValueMap": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap" } - } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult": { "description": "Result used as replacement with text and color when the value matches", @@ -4182,14 +3829,54 @@ }, "index": { "description": "Position in the mapping array. Only used internally.", - "type": "integer", - "format": "int32" + "type": "integer" }, "text": { "description": "Text to display when the value matches", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide": { + "description": "Determine if the variable shows on dashboard\nAccepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).", + "type": "string", + "enum": [ + "dontHide", + "hideLabel", + "hideVariable" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" + } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption": { "description": "Variable option specification", @@ -4205,21 +3892,58 @@ }, "text": { "description": "Text to be displayed for the option", - "allOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString" + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } } ] }, "value": { "description": "Value of the option", - "allOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString" + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } } ] } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh": { + "description": "Options to config when to refresh a variable\n`never`: Never refresh the variable\n`onDashboardLoad`: Queries the data source every time the dashboard loads.\n`onTimeRangeChanged`: Queries the data source when the dashboard time range changes.", + "type": "string", + "enum": [ + "never", + "onDashboardLoad", + "onTimeRangeChanged" + ] + }, + "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", + "enum": [ + "disabled", + "alphabeticalAsc", + "alphabeticalDesc", + "numericalAsc", + "numericalDesc", + "alphabeticalCaseInsensitiveAsc", + "alphabeticalCaseInsensitiveDesc", + "naturalAsc", + "naturalDesc" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind": { "type": "object", @@ -4230,18 +3954,13 @@ "properties": { "kind": { "description": "The kind of a VizConfigKind is the plugin ID", - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec": { "description": "--- Kinds ---", @@ -4253,24 +3972,20 @@ ], "properties": { "fieldConfig": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource" }, "options": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "pluginVersion": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardWithAccessInfo": { "description": "This is like the legacy DTO where access and metadata are all returned in a single call", @@ -4488,7 +4203,7 @@ } }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { - "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { @@ -4842,4 +4557,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json new file mode 100644 index 00000000000..198ad3aea25 --- /dev/null +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -0,0 +1,4591 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana dashboards as resources", + "title": "dashboard.grafana.app/v2beta1" + }, + "paths": { + "/apis/dashboard.grafana.app/v2beta1/": { + "get": { + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "list objects of kind Dashboard", + "operationId": "listDashboard", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "post": { + "tags": [ + "Dashboard" + ], + "description": "create a Dashboard", + "operationId": "createDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": [ + "Dashboard" + ], + "description": "delete collection of Dashboard", + "operationId": "deletecollectionDashboard", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards/{name}": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "read the specified Dashboard", + "operationId": "getDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "put": { + "tags": [ + "Dashboard" + ], + "description": "replace the specified Dashboard", + "operationId": "replaceDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": [ + "Dashboard" + ], + "description": "delete a Dashboard", + "operationId": "deleteDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "patch": { + "tags": [ + "Dashboard" + ], + "description": "partially update the specified Dashboard", + "operationId": "updateDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards/{name}/dto": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "connect GET requests to dto of Dashboard", + "operationId": "getDashboardDto", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardWithAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "DashboardWithAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DashboardWithAccessInfo", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions": { + "type": "object", + "required": [ + "canAdd", + "canEdit", + "canDelete" + ], + "properties": { + "canAdd": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationPermission": { + "type": "object", + "required": [ + "dashboard", + "organization" + ], + "properties": { + "dashboard": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions" + } + ] + }, + "organization": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard": { + "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "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", + "type": "string" + }, + "kind": { + "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", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec" + }, + "status": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "Dashboard", + "version": "v2beta1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAccess": { + "description": "Information about how the requesting user can use a given dashboard", + "type": "object", + "required": [ + "isPublic", + "canSave", + "canEdit", + "canAdmin", + "canStar", + "canDelete", + "annotationsPermissions" + ], + "properties": { + "annotationsPermissions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationPermission" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "description": "The permissions part", + "type": "boolean", + "default": false + }, + "canStar": { + "type": "boolean", + "default": false + }, + "isPublic": { + "type": "boolean", + "default": false + }, + "slug": { + "description": "Metadata fields", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAction": { + "type": "object", + "required": [ + "type", + "title" + ], + "properties": { + "confirmation": { + "type": "string" + }, + "fetch": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFetchOptions" + }, + "infinity": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardInfinityOptions" + }, + "oneClick": { + "type": "boolean" + }, + "style": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + } + }, + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionType" + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariable" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionType": { + "type": "string", + "enum": [ + "fetch", + "infinity" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariable": { + "type": "object", + "required": [ + "key", + "name", + "type" + ], + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariableType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariableType": { + "description": "Action variable type", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels": { + "description": "Define the AdHocFilterWithLabels type", + "type": "object", + "required": [ + "key", + "operator", + "value" + ], + "properties": { + "condition": { + "description": "@deprecated", + "type": "string" + }, + "forceEdit": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "keyLabel": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "origin": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFilterOrigin" + }, + "value": { + "type": "string" + }, + "valueLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableKind": { + "description": "Adhoc variable kind", + "type": "object", + "required": [ + "kind", + "group", + "spec" + ], + "properties": { + "datasource": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableSpec": { + "description": "Adhoc variable specification", + "type": "object", + "required": [ + "name", + "baseFilters", + "filters", + "defaultKeys", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "baseFilters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels" + } + }, + "defaultKeys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMetricFindValue" + } + }, + "description": { + "type": "string" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels" + } + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationEventFieldMapping": { + "description": "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.", + "type": "object", + "properties": { + "regex": { + "description": "Regular expression to apply to the field value", + "type": "string" + }, + "source": { + "description": "Source type for the field value", + "type": "string", + "default": "field" + }, + "value": { + "description": "Constant value to use when source is \"text\"", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationPanelFilter": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "exclude": { + "description": "Should the specified panels be included or excluded", + "type": "boolean", + "default": false + }, + "ids": { + "description": "Panel IDs that should be included or excluded", + "type": "array", + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQuerySpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryPlacement": { + "description": "Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQuerySpec": { + "type": "object", + "required": [ + "query", + "enable", + "hide", + "iconColor", + "name" + ], + "properties": { + "builtIn": { + "type": "boolean", + "default": false + }, + "enable": { + "type": "boolean" + }, + "filter": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationPanelFilter" + }, + "hide": { + "type": "boolean" + }, + "iconColor": { + "type": "string" + }, + "legacyOptions": { + "description": "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", + "type": "object", + "additionalProperties": true + }, + "mappings": { + "description": "Mappings define how to convert data frame fields to annotation event fields.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationEventFieldMapping" + } + }, + "name": { + "type": "string" + }, + "placement": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryPlacement" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemSpec": { + "type": "object", + "required": [ + "element" + ], + "properties": { + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "element": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference" + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridRepeatOptions" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutSpec": { + "type": "object", + "required": [ + "columnWidthMode", + "rowHeightMode", + "items" + ], + "properties": { + "columnWidth": { + "type": "number" + }, + "columnWidthMode": { + "type": "string", + "default": "standard", + "enum": [ + "narrow", + "standard", + "wide", + "custom" + ] + }, + "fillScreen": { + "type": "boolean", + "default": false + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemKind" + } + }, + "maxColumnCount": { + "type": "number", + "default": 3 + }, + "rowHeight": { + "type": "number" + }, + "rowHeightMode": { + "type": "string", + "default": "standard", + "enum": [ + "short", + "standard", + "tall", + "custom" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataSpec": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupSpec": { + "type": "object", + "required": [ + "visibility", + "condition", + "items" + ], + "properties": { + "condition": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeKind" + } + ] + } + }, + "visibility": { + "type": "string", + "enum": [ + "show", + "hide" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableSpec": { + "type": "object", + "required": [ + "variable", + "operator", + "value" + ], + "properties": { + "operator": { + "type": "string", + "enum": [ + "equals", + "notEquals", + "matches", + "notMatches" + ] + }, + "value": { + "type": "string" + }, + "variable": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableKind": { + "description": "Constant variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableSpec": { + "description": "Constant variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConversionStatus": { + "description": "ConversionStatus is the status of the conversion of the dashboard.", + "type": "object", + "required": [ + "failed" + ], + "properties": { + "error": { + "description": "The error message from the conversion.\nEmpty if the conversion has not failed.", + "type": "string" + }, + "failed": { + "description": "Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.", + "type": "boolean" + }, + "source": { + "description": "The original value map[string]any", + "type": "object", + "additionalProperties": {} + }, + "storedVersion": { + "description": "The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableKind": { + "description": "Custom variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableSpec": { + "description": "Custom variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "options", + "multi", + "includeAll", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardCursorSync": { + "description": "\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.", + "type": "string", + "enum": [ + "Crosshair", + "Tooltip", + "Off" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLink": { + "description": "Links with references to other dashboards or external resources", + "type": "object", + "required": [ + "title", + "type", + "icon", + "tooltip", + "tags", + "asDropdown", + "targetBlank", + "includeVars", + "keepTime" + ], + "properties": { + "asDropdown": { + "description": "If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards", + "type": "boolean", + "default": false + }, + "icon": { + "description": "Icon name to be displayed with the link", + "type": "string" + }, + "includeVars": { + "description": "If true, includes current template variables values in the link as query params", + "type": "boolean", + "default": false + }, + "keepTime": { + "description": "If true, includes current time range in the link as query params", + "type": "boolean", + "default": false + }, + "placement": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkPlacement" + }, + "tags": { + "description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards", + "type": "array", + "items": { + "type": "string" + } + }, + "targetBlank": { + "description": "If true, the link will be opened in a new tab", + "type": "boolean", + "default": false + }, + "title": { + "description": "Title to display with the link", + "type": "string" + }, + "tooltip": { + "description": "Tooltip to display when the user hovers their mouse over it", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkType" + }, + "url": { + "description": "Link URL. Only required/valid if the type is link", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkPlacement": { + "description": "Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkType": { + "description": "Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)", + "type": "string", + "enum": [ + "link", + "dashboards" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataLink": { + "type": "object", + "required": [ + "title", + "url" + ], + "properties": { + "targetBlank": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind": { + "type": "object", + "required": [ + "kind", + "group", + "version", + "spec" + ], + "properties": { + "datasource": { + "description": "New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.", + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "version": { + "type": "string", + "default": "v0" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTopic": { + "description": "A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.", + "type": "string", + "enum": [ + "series", + "annotations", + "alertStates" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTransformerConfig": { + "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.", + "type": "object", + "required": [ + "id", + "options" + ], + "properties": { + "disabled": { + "description": "Disabled transformations are skipped", + "type": "boolean" + }, + "filter": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig" + }, + "id": { + "description": "Unique identifier of transformer", + "type": "string" + }, + "options": { + "description": "Options to be passed to the transformer\nValid options depend on the transformer id", + "type": "object", + "additionalProperties": {} + }, + "topic": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTopic" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableKind": { + "description": "Datasource variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableSpec": { + "description": "Datasource variable specification", + "type": "object", + "required": [ + "name", + "pluginId", + "refresh", + "regex", + "current", + "options", + "multi", + "includeAll", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "pluginId": { + "type": "string", + "default": "" + }, + "refresh": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh" + }, + "regex": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDynamicConfigValue": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "default": "" + }, + "value": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElement": { + "description": "Supported dashboard elements\n|* more element types in the future", + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKind" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFetchOptions": { + "type": "object", + "required": [ + "method", + "url" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "method": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod" + }, + "queryParams": { + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColor": { + "description": "Map a field to a color.", + "type": "object", + "required": [ + "mode" + ], + "properties": { + "fixedColor": { + "description": "The fixed color value for fixed or shades color modes.", + "type": "string" + }, + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorModeId" + }, + "seriesBy": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorSeriesByMode" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorModeId": { + "description": "Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n`thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold\n`palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n`palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n`continuous-viridis`: Continuous Viridis palette mode\n`continuous-magma`: Continuous Magma palette mode\n`continuous-plasma`: Continuous Plasma palette mode\n`continuous-inferno`: Continuous Inferno palette mode\n`continuous-cividis`: Continuous Cividis palette mode\n`continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode\n`continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode\n`continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode\n`continuous-YlRd`: Continuous Yellow-Red palette mode\n`continuous-BlPu`: Continuous Blue-Purple palette mode\n`continuous-YlBl`: Continuous Yellow-Blue palette mode\n`continuous-blues`: Continuous Blue palette mode\n`continuous-reds`: Continuous Red palette mode\n`continuous-greens`: Continuous Green palette mode\n`continuous-purples`: Continuous Purple palette mode\n`shades`: Shades of a single color. Specify a single color, useful in an override rule.\n`fixed`: Fixed color mode. Specify a single color, useful in an override rule.", + "type": "string", + "enum": [ + "thresholds", + "palette-classic", + "palette-classic-by-name", + "continuous-viridis", + "continuous-magma", + "continuous-plasma", + "continuous-inferno", + "continuous-cividis", + "continuous-GrYlRd", + "continuous-RdYlGr", + "continuous-BlYlRd", + "continuous-YlRd", + "continuous-BlPu", + "continuous-YlBl", + "continuous-blues", + "continuous-reds", + "continuous-greens", + "continuous-purples", + "fixed", + "shades" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorSeriesByMode": { + "description": "Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.", + "type": "string", + "enum": [ + "min", + "max", + "last" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfig": { + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", + "type": "object", + "properties": { + "actions": { + "description": "Define interactive HTTP requests that can be triggered from data visualizations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAction" + } + }, + "color": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColor" + }, + "custom": { + "description": "custom is specified by the FieldConfig field\nin panel plugin schemas.", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "decimals": { + "description": "Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to `String`.", + "type": "number" + }, + "description": { + "description": "Human readable field metadata", + "type": "string" + }, + "displayName": { + "description": "The display value for this field. This supports template variables blank is auto", + "type": "string" + }, + "displayNameFromDS": { + "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", + "type": "string" + }, + "filterable": { + "description": "True if data source field supports ad-hoc filters", + "type": "boolean" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "mappings": { + "description": "Convert input values into a display string", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMapping" + } + }, + "max": { + "description": "The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + "type": "number" + }, + "min": { + "description": "The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + "type": "number" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string" + }, + "path": { + "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", + "type": "string" + }, + "thresholds": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig" + }, + "unit": { + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", + "type": "string" + }, + "writeable": { + "description": "True if data source can write a value to the path. Auth/authz are supported separately", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfigSource": { + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", + "type": "object", + "required": [ + "defaults", + "overrides" + ], + "properties": { + "defaults": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfig" + }, + "overrides": { + "description": "Overrides are the options applied to specific fields overriding the defaults.", + "type": "array", + "items": { + "type": "object", + "required": [ + "matcher", + "properties" + ], + "properties": { + "__systemRef": { + "description": "Describes config override rules created when interacting with Grafana.", + "type": "string" + }, + "matcher": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDynamicConfigValue" + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFilterOrigin": { + "description": "Determine the origin of the adhoc variable filter", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemSpec": { + "type": "object", + "required": [ + "x", + "y", + "width", + "height", + "element" + ], + "properties": { + "element": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference" + }, + "height": { + "type": "integer" + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatOptions" + }, + "width": { + "type": "integer" + }, + "x": { + "type": "integer" + }, + "y": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutSpec": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableKind": { + "description": "Group variable kind", + "type": "object", + "required": [ + "kind", + "group", + "spec" + ], + "properties": { + "datasource": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableSpec": { + "description": "GroupBy variable specification", + "type": "object", + "required": [ + "name", + "current", + "options", + "multi", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "defaultValue": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "PATCH" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardInfinityOptions": { + "type": "object", + "required": [ + "method", + "url", + "datasourceUid" + ], + "properties": { + "body": { + "type": "string" + }, + "datasourceUid": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "method": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod" + }, + "queryParams": { + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableKind": { + "description": "Interval variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableSpec": { + "description": "Interval variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "options", + "auto", + "auto_min", + "auto_count", + "refresh", + "hide", + "skipUrlSync" + ], + "properties": { + "auto": { + "type": "boolean", + "default": false + }, + "auto_count": { + "type": "integer", + "default": 0 + }, + "auto_min": { + "type": "string", + "default": "" + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "query": { + "type": "string", + "default": "" + }, + "refresh": { + "type": "string" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKindSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKindSpec": { + "type": "object", + "required": [ + "id", + "title", + "libraryPanel" + ], + "properties": { + "id": { + "description": "Panel ID for the library panel in the dashboard", + "type": "number" + }, + "libraryPanel": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelRef" + }, + "title": { + "description": "Title for the library panel in the dashboard", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelRef": { + "description": "A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.", + "type": "object", + "required": [ + "name", + "uid" + ], + "properties": { + "name": { + "description": "Library panel name", + "type": "string" + }, + "uid": { + "description": "Library panel uid", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "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", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + ] + } + }, + "kind": { + "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", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardList", + "version": "v2beta1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType": { + "description": "Supported value mapping types\n`value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n`range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n`regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n`special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "type": "string", + "enum": [ + "value", + "range", + "regex", + "special" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig": { + "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "The matcher id. This is used to find the matcher implementation from registry.", + "type": "string", + "default": "" + }, + "options": { + "description": "The matcher options. This is specific to the matcher implementation.", + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMetricFindValue": { + "description": "Define the MetricFindValue type", + "type": "object", + "required": [ + "text" + ], + "properties": { + "expandable": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "text": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQueryKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQuerySpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQuerySpec": { + "type": "object", + "required": [ + "query", + "refId", + "hidden" + ], + "properties": { + "hidden": { + "type": "boolean" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + }, + "refId": { + "type": "string", + "default": "A" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelSpec": { + "type": "object", + "required": [ + "id", + "title", + "description", + "links", + "data", + "vizConfig" + ], + "properties": { + "data": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupKind" + }, + "description": { + "type": "string" + }, + "id": { + "type": "number" + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataLink" + } + }, + "title": { + "type": "string" + }, + "transparent": { + "type": "boolean" + }, + "vizConfig": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigKind" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupSpec": { + "type": "object", + "required": [ + "queries", + "transformations", + "queryOptions" + ], + "properties": { + "queries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQueryKind" + } + }, + "queryOptions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryOptionsSpec" + }, + "transformations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTransformationKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryOptionsSpec": { + "type": "object", + "properties": { + "cacheTimeout": { + "type": "string" + }, + "hideTimeOverride": { + "type": "boolean" + }, + "interval": { + "type": "string" + }, + "maxDataPoints": { + "type": "integer" + }, + "queryCachingTTL": { + "type": "integer" + }, + "timeCompare": { + "type": "string" + }, + "timeFrom": { + "type": "string" + }, + "timeShift": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableKind": { + "description": "Query variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableSpec": { + "description": "Query variable specification", + "type": "object", + "required": [ + "name", + "current", + "hide", + "refresh", + "skipUrlSync", + "query", + "regex", + "sort", + "options", + "multi", + "includeAll", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "definition": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "placeholder": { + "type": "string" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + }, + "refresh": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh" + }, + "regex": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + }, + "sort": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort" + }, + "staticOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "staticOptionsOrder": { + "type": "string", + "enum": [ + "before", + "after", + "sorted" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRangeMap": { + "description": "Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Range to match against and the result to apply when the value is within the range", + "type": "object", + "required": [ + "from", + "to", + "result" + ], + "properties": { + "from": { + "description": "Min value of the range. It can be null which means -Infinity", + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + }, + "to": { + "description": "Max value of the range. It can be null which means +Infinity", + "type": "number" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRegexMap": { + "description": "Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Regular expression to match against and the result to apply when the value matches the regex", + "type": "object", + "required": [ + "pattern", + "result" + ], + "properties": { + "pattern": { + "description": "Regular expression to match against", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode": { + "description": "other repeat modes will be added in the future: label, frame", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "direction": { + "type": "string", + "enum": [ + "h", + "v" + ] + }, + "maxPerRow": { + "type": "integer" + }, + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowSpec": { + "type": "object", + "required": [ + "layout" + ], + "properties": { + "collapse": { + "type": "boolean" + }, + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "fillScreen": { + "type": "boolean" + }, + "hideHeader": { + "type": "boolean" + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + } + ] + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowRepeatOptions" + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutSpec": { + "type": "object", + "required": [ + "rows" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec": { + "type": "object", + "required": [ + "annotations", + "cursorSync", + "elements", + "layout", + "links", + "preload", + "tags", + "timeSettings", + "title", + "variables" + ], + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryKind" + } + }, + "cursorSync": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardCursorSync" + }, + "description": { + "description": "Description of dashboard.", + "type": "string" + }, + "editable": { + "description": "Whether a dashboard is editable or not.", + "type": "boolean", + "default": true + }, + "elements": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElement" + } + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + } + ] + }, + "links": { + "description": "Links with references to other dashboards or external websites.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLink" + } + }, + "liveNow": { + "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.", + "type": "boolean" + }, + "preload": { + "description": "When set to true, the dashboard will load all panels in the dashboard when it's loaded.", + "type": "boolean", + "default": false + }, + "revision": { + "description": "Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.", + "type": "integer" + }, + "tags": { + "description": "Tags associated with dashboard.", + "type": "array", + "items": { + "type": "string" + } + }, + "timeSettings": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeSettingsSpec" + }, + "title": { + "description": "Title of dashboard.", + "type": "string" + }, + "variables": { + "description": "Configured template variables.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMap": { + "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "type": "object", + "required": [ + "match", + "result" + ], + "properties": { + "match": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMatch" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMatch": { + "description": "Special value types supported by the `SpecialValueMap`", + "type": "string", + "enum": [ + "true", + "false", + "null", + "nan", + "null+nan", + "empty" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus": { + "type": "object", + "properties": { + "conversion": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConversionStatus" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableSpec": { + "type": "object", + "required": [ + "name", + "current", + "enabledValue", + "disabledValue", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "type": "string", + "default": "false" + }, + "description": { + "type": "string" + }, + "disabledValue": { + "type": "string", + "default": "false" + }, + "enabledValue": { + "type": "string", + "default": "true" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutSpec": { + "type": "object", + "required": [ + "tabs" + ], + "properties": { + "tabs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabSpec": { + "type": "object", + "required": [ + "layout" + ], + "properties": { + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + } + ] + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabRepeatOptions" + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableKind": { + "description": "Text variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableSpec": { + "description": "Text variable specification", + "type": "object", + "required": [ + "name", + "current", + "query", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThreshold": { + "type": "object", + "required": [ + "value", + "color" + ], + "properties": { + "color": { + "type": "string" + }, + "value": { + "description": "Value null means -Infinity", + "type": "number" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig": { + "type": "object", + "required": [ + "mode", + "steps" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsMode" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThreshold" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsMode": { + "type": "string", + "enum": [ + "absolute", + "percentage" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeRangeOption": { + "type": "object", + "required": [ + "display", + "from", + "to" + ], + "properties": { + "display": { + "type": "string", + "default": "Last 6 hours" + }, + "from": { + "type": "string", + "default": "now-6h" + }, + "to": { + "type": "string", + "default": "now" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeSettingsSpec": { + "description": "Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.", + "type": "object", + "required": [ + "from", + "to", + "autoRefresh", + "autoRefreshIntervals", + "hideTimepicker", + "fiscalYearStartMonth" + ], + "properties": { + "autoRefresh": { + "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh", + "type": "string", + "default": "" + }, + "autoRefreshIntervals": { + "description": "Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals", + "type": "array", + "default": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "items": { + "type": "string" + } + }, + "fiscalYearStartMonth": { + "description": "The month that the fiscal year starts on. 0 = January, 11 = December", + "type": "integer", + "default": 0 + }, + "from": { + "description": "Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "type": "string", + "default": "now-6h" + }, + "hideTimepicker": { + "description": "Whether timepicker is visible or not.\nv1: timepicker.hidden", + "type": "boolean", + "default": false + }, + "nowDelay": { + "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay", + "type": "string" + }, + "quickRanges": { + "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeRangeOption" + } + }, + "timezone": { + "description": "Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".", + "type": "string", + "default": "browser" + }, + "to": { + "description": "End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "type": "string", + "default": "now" + }, + "weekStart": { + "description": "Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".", + "type": "string", + "enum": [ + "saturday", + "monday", + "sunday" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTransformationKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "description": "The kind of a TransformationKind is the transformation ID", + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTransformerConfig" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMap": { + "description": "Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMapping": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRangeMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRegexMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMap" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult": { + "description": "Result used as replacement with text and color when the value matches", + "type": "object", + "properties": { + "color": { + "description": "Text to use when the value matches", + "type": "string" + }, + "icon": { + "description": "Icon to display when the value matches. Only specific visualizations.", + "type": "string" + }, + "index": { + "description": "Position in the mapping array. Only used internally.", + "type": "integer" + }, + "text": { + "description": "Text to display when the value matches", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide": { + "description": "Determine if the variable shows on dashboard\nAccepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).", + "type": "string", + "enum": [ + "dontHide", + "hideLabel", + "hideVariable", + "inControlsMenu" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableKind" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption": { + "description": "Variable option specification", + "type": "object", + "required": [ + "text", + "value" + ], + "properties": { + "selected": { + "description": "Whether the option is selected or not", + "type": "boolean" + }, + "text": { + "description": "Text to be displayed for the option", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "value": { + "description": "Value of the option", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh": { + "description": "Options to config when to refresh a variable\n`never`: Never refresh the variable\n`onDashboardLoad`: Queries the data source every time the dashboard loads.\n`onTimeRangeChanged`: Queries the data source when the dashboard time range changes.", + "type": "string", + "enum": [ + "never", + "onDashboardLoad", + "onTimeRangeChanged" + ] + }, + "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", + "enum": [ + "disabled", + "alphabeticalAsc", + "alphabeticalDesc", + "numericalAsc", + "numericalDesc", + "alphabeticalCaseInsensitiveAsc", + "alphabeticalCaseInsensitiveDesc", + "naturalAsc", + "naturalDesc" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigKind": { + "type": "object", + "required": [ + "kind", + "group", + "version", + "spec" + ], + "properties": { + "group": { + "description": "The group is the plugin ID", + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigSpec" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigSpec": { + "description": "--- Kinds ---", + "type": "object", + "required": [ + "options", + "fieldConfig" + ], + "properties": { + "fieldConfig": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfigSource" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardWithAccessInfo": { + "description": "This is like the legacy DTO where access and metadata are all returned in a single call", + "type": "object", + "required": [ + "metadata", + "spec", + "status", + "access" + ], + "properties": { + "access": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAccess" + } + ] + }, + "apiVersion": { + "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", + "type": "string" + }, + "kind": { + "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", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Dashboard", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardWithAccessInfo", + "version": "v2beta1" + } + ] + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": [ + "groupVersion", + "resources" + ], + "properties": { + "apiVersion": { + "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", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "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", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "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", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "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", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "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", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "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", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 3e02a985eb1..b9d0283e37a 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -78,6 +78,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "dashboard.grafana.app", Version: "v2alpha1", + }, { + Group: "dashboard.grafana.app", + Version: "v2beta1", }, { Group: "folder.grafana.app", Version: "v1beta1", From 592c599ca6d6e7c000ee98c6b0b1a93d30b02d53 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Sat, 6 Dec 2025 10:37:20 +0100 Subject: [PATCH 322/423] Alerting: Add configurable transport to historian app (#114935) --- .../historian/pkg/app/config/config.go | 10 ++++-- .../historian/pkg/app/config/config_test.go | 36 +++++++++++-------- .../pkg/app/notification/lokireader.go | 10 ++++-- .../apps/alerting/historian/register.go | 4 ++- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/alerting/historian/pkg/app/config/config.go b/apps/alerting/historian/pkg/app/config/config.go index 5d8027d933b..cb3b3caa711 100644 --- a/apps/alerting/historian/pkg/app/config/config.go +++ b/apps/alerting/historian/pkg/app/config/config.go @@ -1,6 +1,7 @@ package config import ( + "net/http" "net/url" "time" @@ -15,9 +16,14 @@ const ( lokiDefaultMaxQuerySize = 65536 // 64kb ) +type LokiConfig struct { + lokiclient.LokiConfig + Transport http.RoundTripper +} + type NotificationConfig struct { Enabled bool - Loki lokiclient.LokiConfig + Loki LokiConfig } type RuntimeConfig struct { @@ -27,7 +33,7 @@ type RuntimeConfig struct { func (n *NotificationConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) { flags.BoolVar(&n.Enabled, prefix+".enabled", false, "Enable notification query endpoints") - addLokiFlags(&n.Loki, prefix+".loki", flags) + addLokiFlags(&n.Loki.LokiConfig, prefix+".loki", flags) } func (r *RuntimeConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) { diff --git a/apps/alerting/historian/pkg/app/config/config_test.go b/apps/alerting/historian/pkg/app/config/config_test.go index 8234f4c945f..7f8ab623a7a 100644 --- a/apps/alerting/historian/pkg/app/config/config_test.go +++ b/apps/alerting/historian/pkg/app/config/config_test.go @@ -24,10 +24,12 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: false, - Loki: lokiclient.LokiConfig{ - ReadPathURL: nil, - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: nil, + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, @@ -38,10 +40,12 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: true, - Loki: lokiclient.LokiConfig{ - ReadPathURL: nil, - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: nil, + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, @@ -57,13 +61,15 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: false, - Loki: lokiclient.LokiConfig{ - ReadPathURL: lokiURL, - BasicAuthUser: "foo", - BasicAuthPassword: "bar", - TenantID: "baz", - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: lokiURL, + BasicAuthUser: "foo", + BasicAuthPassword: "bar", + TenantID: "baz", + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, diff --git a/apps/alerting/historian/pkg/app/notification/lokireader.go b/apps/alerting/historian/pkg/app/notification/lokireader.go index e8cea23dda7..c26519e59b4 100644 --- a/apps/alerting/historian/pkg/app/notification/lokireader.go +++ b/apps/alerting/historian/pkg/app/notification/lokireader.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "regexp" "sort" "strings" @@ -19,6 +20,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1" + "github.com/grafana/grafana/apps/alerting/historian/pkg/app/config" "github.com/grafana/grafana/apps/alerting/historian/pkg/app/logutil" ) @@ -47,7 +49,7 @@ type LokiReader struct { logger logging.Logger } -func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader { +func NewLokiReader(cfg config.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader { duration := instrument.NewHistogramCollector(promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ Namespace: Namespace, Subsystem: Subsystem, @@ -56,9 +58,13 @@ func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger Buckets: instrument.DefBuckets, }, instrument.HistogramCollectorBuckets)) + requester := &http.Client{ + Transport: cfg.Transport, + } + gkLogger := logutil.ToGoKitLogger(logger) return &LokiReader{ - client: lokiclient.NewLokiClient(cfg, lokiclient.NewRequester(), nil, duration, gkLogger, tracer, LokiClientSpanName), + client: lokiclient.NewLokiClient(cfg.LokiConfig, requester, nil, duration, gkLogger, tracer, LokiClientSpanName), logger: logger, } } diff --git a/pkg/registry/apps/alerting/historian/register.go b/pkg/registry/apps/alerting/historian/register.go index 7fc2176d758..68830dcd0ef 100644 --- a/pkg/registry/apps/alerting/historian/register.go +++ b/pkg/registry/apps/alerting/historian/register.go @@ -42,7 +42,9 @@ func RegisterAppInstaller( appSpecificConfig.Notification = historianAppConfig.NotificationConfig{ Enabled: nhCfg.Enabled, - Loki: lokiConfig, + Loki: historianAppConfig.LokiConfig{ + LokiConfig: lokiConfig, + }, } } } From 78b1ae4f27c0d8ae5473faf55523da791bfccbff Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Sat, 6 Dec 2025 16:45:18 +0300 Subject: [PATCH 323/423] Search: Fix field selector parsing (#114940) --- pkg/storage/unified/apistore/util.go | 2 +- pkg/storage/unified/apistore/util_test.go | 43 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/storage/unified/apistore/util.go b/pkg/storage/unified/apistore/util.go index d3763f652a6..6cb4c5b31f8 100644 --- a/pkg/storage/unified/apistore/util.go +++ b/pkg/storage/unified/apistore/util.go @@ -124,7 +124,7 @@ func toListRequest(k *resourcepb.ResourceKey, opts storage.ListOptions) (*resour if r.Value != "" { requirement.Values = append(requirement.Values, r.Value) } - req.Options.Labels = append(req.Options.Labels, requirement) + req.Options.Fields = append(req.Options.Fields, requirement) } } diff --git a/pkg/storage/unified/apistore/util_test.go b/pkg/storage/unified/apistore/util_test.go index 0bee8adf75e..68ca1ad462f 100644 --- a/pkg/storage/unified/apistore/util_test.go +++ b/pkg/storage/unified/apistore/util_test.go @@ -117,6 +117,49 @@ func TestToListRequest(t *testing.T) { }, wantErr: nil, }, + { + name: "with field selector", + key: &resourcepb.ResourceKey{ + Group: "test", + Resource: "test", + Namespace: "default", + }, + opts: storage.ListOptions{ + Predicate: storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"label": "A"}), + Field: fields.SelectorFromSet(fields.Set{"field": "B"}), + }, + }, + want: &resourcepb.ListRequest{ + VersionMatchV2: 1, + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Group: "test", + Resource: "test", + Namespace: "default", + }, + Labels: []*resourcepb.Requirement{ + { + Key: "label", + Operator: string(selection.Equals), + Values: []string{"A"}, + }, + }, + Fields: []*resourcepb.Requirement{ + { + Key: "field", + Operator: string(selection.Equals), + Values: []string{"B"}, + }, + }, + }, + }, + wantPredicate: storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"label": "A"}), + Field: fields.SelectorFromSet(fields.Set{"field": "B"}), + }, + wantErr: nil, + }, { name: "with trash label", key: &resourcepb.ResourceKey{ From d0977b524561d8bfcc20687f0d10cd5e2c5a0754 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Mon, 8 Dec 2025 09:22:28 +0100 Subject: [PATCH 324/423] `grafana-iam`: Add role apis to the standalone app (#114897) --- pkg/registry/apis/iam/authorizer.go | 2 +- pkg/registry/apis/iam/register.go | 41 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 0ec018d86de..05c8da97c2e 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -44,7 +44,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = iamauthorizer.NewCoreRoleAuthorizer(accessClient) resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer - resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled at storage layer + resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled by the backend wrapper resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = authorizer resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 786635fa19a..32e2c9fefef 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/prometheus/client_golang/prometheus" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -23,7 +24,6 @@ import ( "github.com/grafana/authlib/types" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" @@ -32,6 +32,7 @@ import ( iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer" "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -39,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/teambinding" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/accesscontrol" + gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -116,6 +118,8 @@ func RegisterAPIService( func NewAPIService( accessClient types.AccessClient, dbProvider legacysql.LegacyDatabaseProvider, + coreRoleStorage CoreRoleStorageBackend, + roleStorage RoleStorageBackend, features featuremgmt.FeatureToggles, zClient zanzana.Client, reg prometheus.Registerer, @@ -123,10 +127,17 @@ func NewAPIService( store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) registerMetrics(reg) + + resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) + coreRoleAuthorizer := iamauthorizer.NewCoreRoleAuthorizer(accessClient) + return &IdentityAccessManagementAPIBuilder{ store: store, display: user.NewLegacyDisplayREST(store), resourcePermissionsStorage: resourcePermissionsStorage, + rolesStorage: roleStorage, + coreRolesStorage: coreRoleStorage, + roleBindingsStorage: noopstorage.ProvideStorageBackend(), // TODO: add a proper storage backend logger: log.New("iam.apis"), features: features, accessClient: accessClient, @@ -135,20 +146,32 @@ func NewAPIService( reg: reg, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + user, ok := types.AuthInfoFrom(ctx) + if !ok { + return authorizer.DecisionDeny, "no identity found", apierrors.NewUnauthorized("no identity found in context") + } + + if a.GetResource() == "coreroles" { + if user.GetIdentityType() != types.TypeAccessPolicy { + return authorizer.DecisionDeny, "only access policy identities have access for now", nil + } + return coreRoleAuthorizer.Authorize(ctx, a) + } + // For now only authorize resourcepermissions resource if a.GetResource() == "resourcepermissions" { - // Authorization is handled at the storage layer + // Authorization is handled by the backend wrapper return authorizer.DecisionAllow, "", nil } - user, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "no identity found", err + if a.GetResource() == "roles" { + if user.GetIdentityType() != types.TypeAccessPolicy { + return authorizer.DecisionDeny, "only access policy identities have access for now", nil + } + return resourceAuthorizer.Authorize(ctx, a) } - if user.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "only grafana admins have access for now", nil + + return authorizer.DecisionDeny, "access denied", nil }), } } From 8bf3ac97108cd826c76a845d1544541a6e632622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 8 Dec 2025 10:13:56 +0100 Subject: [PATCH 325/423] SelectBase: Use standard portal container (#114844) * SelectBase: Use standard portal container * Fixed positioning issue --- packages/grafana-ui/src/components/Select/SelectBase.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 8609c307c25..cad0eb10e20 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -16,6 +16,7 @@ import { t, Trans } from '@grafana/i18n'; import { useTheme2 } from '../../themes/ThemeContext'; import { Icon } from '../Icon/Icon'; +import { getPortalContainer } from '../Portal/Portal'; import { CustomInput } from './CustomInput'; import { DropdownIndicator } from './DropdownIndicator'; @@ -123,7 +124,7 @@ export function SelectBase({ minMenuHeight, maxVisibleValues, menuPlacement = 'auto', - menuPosition, + menuPosition = 'fixed', menuShouldPortal = true, noOptionsMessage = t('grafana-ui.select.no-options-label', 'No options found'), onBlur, @@ -255,9 +256,9 @@ export function SelectBase({ maxVisibleValues, menuIsOpen: isOpen, menuPlacement: menuPlacement === 'auto' && closeToBottom ? 'top' : menuPlacement, - menuPosition, + menuPosition: menuShouldPortal ? 'fixed' : menuPosition, menuShouldBlockScroll: true, - menuPortalTarget: menuShouldPortal && typeof document !== 'undefined' ? document.body : undefined, + menuPortalTarget: menuShouldPortal && getPortalContainer(), menuShouldScrollIntoView: false, onBlur, onChange: onChangeWithEmpty, From 3490c3b0fdc8cab4fc40ccce89b4be9d82aec6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 8 Dec 2025 10:19:44 +0100 Subject: [PATCH 326/423] e2e: add tests for translations (#114390) e2e: add tests for translations --- devenv/plugins.yaml | 4 + .../components/App/App.tsx | 3 +- .../grafana-extensionstest-app/constants.ts | 1 + .../i18next.config.ts | 13 +++ .../en-US/grafana-extensionstest-app.json | 7 ++ .../es-ES/grafana-extensionstest-app.json | 7 ++ .../sv-SE/grafana-extensionstest-app.json | 7 ++ .../grafana-extensionstest-app/module.tsx | 3 + .../grafana-extensionstest-app/package.json | 10 ++- .../pages/Config.tsx | 17 ++++ .../pages/index.tsx | 1 + .../grafana-extensionstest-app/plugin.json | 5 +- .../tests/translations/french.spec.ts | 12 +++ .../tests/translations/swedish.spec.ts | 12 +++ .../webpack.config.ts | 1 + .../components/ConfigEditor.tsx | 27 ++++-- .../grafana-test-datasource/i18next.config.ts | 13 +++ .../en-US/grafana-e2etest-datasource.json | 23 +++++ .../es-ES/grafana-e2etest-datasource.json | 23 +++++ .../sv-SE/grafana-e2etest-datasource.json | 23 +++++ .../grafana-test-datasource/module.ts | 4 + .../grafana-test-datasource/package.json | 9 +- .../grafana-test-datasource/plugin.json | 5 +- .../tests/translations/french.spec.ts | 11 +++ .../tests/translations/swedish.spec.ts | 11 +++ .../grafana-test-datasource/webpack.config.ts | 1 + .../grafana-test-panel/CHANGELOG.md | 1 + .../test-plugins/grafana-test-panel/README.md | 0 .../components/SimplePanel.tsx | 83 +++++++++++++++++++ .../grafana-test-panel/i18next.config.ts | 13 +++ .../grafana-test-panel/img/logo.svg | 1 + .../locales/en-US/grafana-e2etest-panel.json | 30 +++++++ .../locales/es-ES/grafana-e2etest-panel.json | 30 +++++++ .../locales/sv-SE/grafana-e2etest-panel.json | 30 +++++++ .../test-plugins/grafana-test-panel/module.ts | 46 ++++++++++ .../grafana-test-panel/package.json | 50 +++++++++++ .../grafana-test-panel/plugin.json | 26 ++++++ .../tests/translations/french.spec.ts | 13 +++ .../tests/translations/swedish.spec.ts | 13 +++ .../grafana-test-panel/tsconfig.json | 8 ++ .../test-plugins/grafana-test-panel/types.ts | 7 ++ .../grafana-test-panel/webpack.config.ts | 45 ++++++++++ pkg/build/e2e-playwright/main.go | 5 ++ playwright.config.ts | 4 + scripts/grafana-server/custom.ini | 2 +- yarn.lock | 38 +++++++++ 46 files changed, 678 insertions(+), 20 deletions(-) create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/en-US/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/es-ES/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/sv-SE/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/CHANGELOG.md create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/README.md create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/components/SimplePanel.tsx create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/img/logo.svg create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/en-US/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/es-ES/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/sv-SE/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/module.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/package.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/plugin.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tsconfig.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/types.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/webpack.config.ts diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml index 554a4828cff..0f292e324f1 100644 --- a/devenv/plugins.yaml +++ b/devenv/plugins.yaml @@ -21,3 +21,7 @@ apps: org_id: 1 org_name: Main Org. disabled: false +panels: + - type: grafana-e2etest-panel + org_id: 1 + org_name: Main Org. diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx index d57a1476e19..26d7b201466 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx @@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import { AppRootProps } from '@grafana/data'; import { ROUTES } from '../../constants'; -import { AddedComponents, AddedLinks, ExposedComponents } from '../../pages'; +import { AddedComponents, AddedLinks, Config, ExposedComponents } from '../../pages'; import { testIds } from '../../testIds'; export function App(props: AppRootProps) { @@ -13,6 +13,7 @@ export function App(props: AppRootProps) { } /> } /> } /> + } /> } /> diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts index 120eb5d8191..c208781acf3 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts @@ -8,4 +8,5 @@ export enum ROUTES { ExposedComponents = 'exposed-components', AddedComponents = 'added-components', AddedLinks = 'added-links', + Config = 'config', } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts new file mode 100644 index 00000000000..ba1645d38c9 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'i18next-cli'; +import pluginJson from './plugin.json'; + +export default defineConfig({ + locales: pluginJson.languages, + extract: { + input: ['**/*.{tsx,ts}'], + output: 'locales/{{language}}/{{namespace}}.json', + defaultNS: pluginJson.id, + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json new file mode 100644 index 00000000000..2fc28958864 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "Is this translated" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json new file mode 100644 index 00000000000..2c2f51a239d --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "¿Está traducido?" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json new file mode 100644 index 00000000000..8bae86f58aa --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "Det här är översatt" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx index 89ed1af12c4..9f585c0d5f7 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx @@ -3,6 +3,9 @@ import { App } from './components/App'; import { QueryModal } from './components/QueryModal'; import { selectQuery } from './utils/utils'; import pluginJson from './plugin.json'; +import { initPluginTranslations } from '@grafana/i18n'; + +await initPluginTranslations(pluginJson.id); export const plugin = new AppPlugin<{}>() .setRootPage(App) diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index f89721aaac9..7f094c82b9b 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -6,7 +6,8 @@ "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", - "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." + "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .", + "i18n-extract": "i18next-cli extract --sync-primary" }, "author": "Grafana Labs", "license": "Apache-2.0", @@ -20,17 +21,19 @@ "@types/semver": "7.5.8", "@types/uuid": "9.0.8", "glob": "10.5.0", + "i18next-cli": "^1.24.22", "ts-node": "10.9.2", "typescript": "5.5.4", "webpack": "5.95.0", "webpack-merge": "5.10.0" }, "engines": { - "node": ">=20" + "node": ">= 22 <25" }, "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", + "@grafana/i18n": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", @@ -42,5 +45,6 @@ }, "peerDependencies": { "@grafana/runtime": "*" - } + }, + "packageManager": "yarn@4.11.0" } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx new file mode 100644 index 00000000000..8b31e490242 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx @@ -0,0 +1,17 @@ +import { Trans } from '@grafana/i18n'; +import { PluginPage } from '@grafana/runtime'; +import { Stack } from '@grafana/ui'; + +export function Config() { + return ( + + +
+

+ Is this translated +

+
+
+
+ ); +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx index 1326d3c7bdf..84ddfc8e6ea 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx @@ -1,3 +1,4 @@ export { ExposedComponents } from './ExposedComponents'; export { AddedComponents } from './AddedComponents'; export { AddedLinks } from './AddedLinks'; +export { Config } from './Config'; diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json index 3f5adfa215a..c5ce29f2dc4 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json @@ -82,10 +82,11 @@ ] }, "dependencies": { - "grafanaDependency": ">=10.4.0", + "grafanaDependency": ">=12.0.0", "plugins": [], "extensions": { "exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1", "grafana/add-to-dashboard-form/v1"] } - } + }, + "languages": ["en-US", "es-ES", "sv-SE"] } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts new file mode 100644 index 00000000000..a991453b0cc --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts @@ -0,0 +1,12 @@ +import { FRENCH_FRANCE } from '@grafana/i18n'; +import { expect, test } from '@grafana/plugin-e2e'; +import pluginJson from '../../plugin.json'; +import { ROUTES } from '../../constants'; + +test.use({ userPreferences: { language: FRENCH_FRANCE } }); + +test('should display default translation (en-US)', async ({ gotoAppPage }) => { + const configPage = await gotoAppPage({ pluginId: pluginJson.id, path: ROUTES.Config }); + + await expect(configPage.ctx.page.getByText('Is this translated')).toBeVisible(); +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts new file mode 100644 index 00000000000..404f5053948 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts @@ -0,0 +1,12 @@ +import { SWEDISH_SWEDEN } from '@grafana/i18n'; +import { expect, test } from '@grafana/plugin-e2e'; +import pluginJson from '../../plugin.json'; +import { ROUTES } from '../../constants'; + +test.use({ userPreferences: { language: SWEDISH_SWEDEN } }); + +test('should display correct translation', async ({ gotoAppPage }) => { + const configPage = await gotoAppPage({ pluginId: pluginJson.id, path: ROUTES.Config }); + + await expect(configPage.ctx.page.getByText('Det här är översatt')).toBeVisible(); +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts index 564555396a5..ceff913dbba 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts @@ -34,6 +34,7 @@ const config = async (env: Env): Promise => { ], }), ], + externals: [...(baseConfig.externals as any), 'i18next'], }; return mergeWithCustomize({ diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx index 2c46992a5d9..06101e6e050 100644 --- a/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx +++ b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx @@ -1,6 +1,7 @@ import { ChangeEvent } from 'react'; import { Checkbox, InlineField, InlineSwitch, Input, SecretInput, Select } from '@grafana/ui'; import { DataSourcePluginOptionsEditorProps, SelectableValue, toOption } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { MyDataSourceOptions, MySecureJsonData } from '../types'; interface Props extends DataSourcePluginOptionsEditorProps {} @@ -45,36 +46,46 @@ export function ConfigEditor(props: Props) { return ( <> - + ) => onJsonDataChange('path', e.target.value)} value={jsonData.path} - placeholder="Enter the path, e.g. /api/v1" + placeholder={t('config-editor.path.placeholder', 'Enter the path, e.g. /api/v1')} width={40} /> - + ) => onSecureJsonDataChange('path', e.target.value)} /> - + ) => onJsonDataChange('switchEnabled', e.target.checked)} /> - + ) => onJsonDataChange('checkboxEnabled', e.target.checked)} /> - + - + + render={({ field: { ref, value, onChange, ...field } }) => ( + {inputs.dataSources && inputs.dataSources.map((input: DataSourceInput) => { if (input.pluginId === ExpressionDatasourceRef.type) { @@ -102,6 +100,7 @@ export const ImportDashboardFormV2 = ({ key={input.pluginId} invalid={!!errors[dataSourceOption]} error={errors[dataSourceOption] ? 'Please select a data source' : undefined} + noMargin > name={dataSourceOption} @@ -133,7 +132,7 @@ export const ImportDashboardFormV2 = ({ ); })} - + - + ); }; diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx index 9f87a602d3f..e71b419b8e4 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import { locationUtil } from '@grafana/data'; import { locationService, reportInteraction } from '@grafana/runtime'; import { @@ -20,7 +18,6 @@ const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported'; type FormData = SaveDashboardCommand & { [key: `datasource-${string}`]: string }; export function ImportDashboardOverviewV2() { - const [uidReset, setUidReset] = useState(false); const dispatch = useDispatch(); // Get state from Redux store @@ -29,10 +26,6 @@ export function ImportDashboardOverviewV2() { const inputs = useSelector((state: StoreState) => state.importDashboard.inputs); const folder = searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' }; - function onUidReset() { - setUidReset(true); - } - function onCancel() { dispatch(clearLoadedDashboard()); } @@ -180,7 +173,7 @@ export function ImportDashboardOverviewV2() { <> onSubmit={onSubmit} - defaultValues={{ dashboard, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} + defaultValues={{ dashboard, folderUid: folder.uid, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} validateOnMount validateOn="onChange" > @@ -191,9 +184,7 @@ export function ImportDashboardOverviewV2() { errors={errors} control={control} getValues={getValues} - uidReset={uidReset} onCancel={onCancel} - onUidReset={onUidReset} onSubmit={onSubmit} watch={watch} /> From 83311049adbd6c0e3ba936897ec1252a699f849f Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:16:33 -0500 Subject: [PATCH 359/423] fix: create dashboard in legacy storage within transaction (#114808) fix: create dashboard within transaction --- .../apis/dashboard/legacy/sql_dashboards.go | 23 +++++++++- pkg/registry/apis/dashboard/legacy/storage.go | 11 ++++- .../api/dashboards/api_dashboards_test.go | 44 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 24ec135e785..fbb0e825cd1 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -140,11 +140,30 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, } func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query string, args ...any) (*sql.Rows, error) { - // Use transaction if available in context. + var tx *sql.Tx + // After this function runs, the `tx` variable will only be set if + // this function was called in the context of a transaction set up by a + // caller upstream. In that case, we reuse the transaction. + _ = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + coreTx, err := sess.Tx() + if err != nil { + return nil + } + + tx = coreTx.Tx + return nil + }) + + // Use transaction from unified storage if available in the context. // This allows us to run migrations in a transaction which is specifically required for SQLite. - if tx := resource.TransactionFromContext(ctx); tx != nil { + if tx == nil { + tx = resource.TransactionFromContext(ctx) + } + + if tx != nil { return tx.QueryContext(ctx, query, args...) } + return helper.DB.GetSqlxSession().Query(ctx, query, args...) } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index d8c948c9664..1521021c424 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -132,10 +132,19 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ } } else { failOnExisting := event.Type == resourcepb.WatchEvent_ADDED - after, _, err := a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + sql, err := a.sql(ctx) if err != nil { return 0, err } + + var after *dashboard.Dashboard + if err := sql.DB.InTransaction(ctx, func(ctx context.Context) error { + var err error + after, _, err = a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + return err + }); err != nil { + return 0, err + } if after != nil { meta, err := utils.MetaAccessor(after) if err != nil { diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 50ab7d939ea..fcfb09cb5ba 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -233,6 +233,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) { resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ "dashboard": map[string]interface{}{ @@ -266,6 +267,34 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + + t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) { + originalCount := getDashboardCount(t, grafanaListedAddr, "admin", "admin") + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Bad dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "gridPos": map[string]int{"h": 0, "w": 0, "x": 0, "y": 0}, + "libraryPanel": map[string]string{ + "name": "Bad panel", + "uid": "invalid-uid", + }, + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "library element could not be found") + err = resp.Body.Close() + require.NoError(t, err) + + // A new dashboard is not created in this situation. + require.Equal(t, originalCount, getDashboardCount(t, grafanaListedAddr, "admin", "admin")) + }) } func TestIntegrationDashboardQuota(t *testing.T) { @@ -982,6 +1011,21 @@ func postDashboard(t *testing.T, grafanaListedAddr, user, password string, paylo return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec } +func getDashboardCount(t *testing.T, grafanaListenAddr, user, password string) int { + endpoint := fmt.Sprintf("http://%s:%s@%s/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search", user, password, grafanaListenAddr) + resp, err := http.Get(endpoint) //nolint:gosec + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + var payload map[string]any + require.NoError(t, json.Unmarshal(body, &payload)) + + return int(payload["totalHits"].(float64)) +} + func TestIntegrationDashboardServicePermissions(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) From 8602ec7924cd6e800135b461ec74d718eb85df3d Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Tue, 9 Dec 2025 17:31:38 +0200 Subject: [PATCH 360/423] IAM: Add integration tests for team search (#114996) add integration tests for team search --- .../apis/iam/team_search_integration_test.go | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 pkg/tests/apis/iam/team_search_integration_test.go diff --git a/pkg/tests/apis/iam/team_search_integration_test.go b/pkg/tests/apis/iam/team_search_integration_test.go new file mode 100644 index 00000000000..c01ca9a641a --- /dev/null +++ b/pkg/tests/apis/iam/team_search_integration_test.go @@ -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") + }) +} From 297e886e1be0e5a6eaa975ed60296b784797d83c Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Tue, 9 Dec 2025 16:33:43 +0100 Subject: [PATCH 361/423] fix: remove dsIndexProvider from Convert_V2alpha1_to_V0 (#115017) --- .../pkg/migration/conversion/conversion.go | 4 +- apps/dashboard/pkg/migration/conversion/v2.go | 10 +- .../pkg/migration/conversion/v2_test.go | 2 +- .../conversion/v2alpha1_to_v1beta1.go | 146 +++++++++--------- 4 files changed, 77 insertions(+), 85 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index d0f90e1ba98..54edf869f84 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -62,13 +62,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 } diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index fee798d3ec5..fa8a49e91b4 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -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), diff --git a/apps/dashboard/pkg/migration/conversion/v2_test.go b/apps/dashboard/pkg/migration/conversion/v2_test.go index 18e0713fa84..cbacde6746e 100644 --- a/apps/dashboard/pkg/migration/conversion/v2_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2_test.go @@ -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") diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 180a8d603bb..fb2854845ce 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -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, @@ -1336,7 +1328,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 +1478,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 +1501,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 +1509,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 +1528,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 +1655,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 +1678,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 } From a3daf0e39dab576ae860badffe4e10fcdb023f87 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 9 Dec 2025 09:40:34 -0600 Subject: [PATCH 362/423] Unified storage: Add quotas app to apiserver (#114425) * initial generation * went through doc to add new resource * added dummy kind so grafana will run * added dummy handler and custom route * fix app name * gets custom route working - still a dummy route * adds groupOverride to manifest * adds quotas to grpc client and server * WIP - trying to get api recognized - not working * Gets route working * fixes group and resource vars * expects group and resource as separate params * set content-type header on response * removes Quotas kind and regens * Update grafana-app-sdk to v0.48.5 * Update codegen * updates manifest * formatting * updates grafana-app-sdk version to 0.48.5 * regen ResourceClient mocks * adds tests * remove commented code * uncomment go mod tidy * fix tests and make update workspace * adds quotas app to codeowners * formatting * make gen-apps * deletes temp file * fix generated folder code * make gofmt * make gen-go * make update-workspace * add COPY apps/quotas to Dockerfile * fix test mock * fixes undefined NewFolderStatus() * make gen-apps, and add func for NewFolderStatus * make gen-apps again * make update-workspace * regen folder_object_gen.go * gofmt * fix linting * apps/folder make update-workspace * make gen-apps * make gen-apps * fixes enterprise_imports.go * go get testcontainers * adds feature toggle * make update-workspace * fix go mod * fix another client mock --------- Co-authored-by: Steve Simpson --- .github/CODEOWNERS | 1 + Dockerfile | 1 + apps/alerting/historian/go.sum | 2 + apps/example/kinds/manifest.cue | 10 +- apps/quotas/Makefile | 9 + apps/quotas/go.mod | 92 +++++ apps/quotas/go.sum | 252 ++++++++++++ apps/quotas/kinds/cue.mod/module.cue | 2 + apps/quotas/kinds/manifest.cue | 92 +++++ .../getusage_request_params_object_gen.go | 33 ++ .../getusage_request_params_types_gen.go | 13 + .../getusage_response_body_types_gen.go | 17 + .../getusage_response_object_types_gen.go | 37 ++ apps/quotas/pkg/apis/quotas_manifest.go | 213 ++++++++++ apps/quotas/pkg/app/app.go | 123 ++++++ apps/quotas/pkg/app/app_test.go | 71 ++++ .../quota/v0alpha1/quota_object_gen.ts | 49 +++ .../quota/v0alpha1/types.metadata.gen.ts | 30 ++ .../quota/v0alpha1/types.spec.gen.ts | 14 + .../quota/v0alpha1/types.status.gen.ts | 30 ++ go.mod | 4 +- go.sum | 10 +- go.work | 1 + go.work.sum | 20 +- .../src/types/featureToggles.gen.ts | 4 + pkg/extensions/enterprise_imports.go | 3 +- pkg/registry/apis/dashboard/legacy/client.go | 4 + pkg/registry/apis/dashboard/search_test.go | 4 + pkg/registry/apis/iam/team_search_test.go | 3 + pkg/registry/apps/apps.go | 7 + pkg/registry/apps/apps_test.go | 4 +- pkg/registry/apps/quotas/register.go | 50 +++ pkg/registry/apps/wireset.go | 2 + pkg/server/wire_gen.go | 13 +- pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 + pkg/storage/unified/apistore/store_test.go | 1 + pkg/storage/unified/proto/resource.proto | 21 + pkg/storage/unified/resource/client.go | 4 + pkg/storage/unified/resource/client_mock.go | 74 ++++ pkg/storage/unified/resource/server.go | 33 ++ pkg/storage/unified/resource/server_test.go | 70 ++++ pkg/storage/unified/resourcepb/resource.pb.go | 367 ++++++++++++------ .../unified/resourcepb/resource_grpc.pb.go | 91 +++++ pkg/storage/unified/sql/service.go | 1 + 47 files changed, 1758 insertions(+), 149 deletions(-) create mode 100644 apps/quotas/Makefile create mode 100644 apps/quotas/go.mod create mode 100644 apps/quotas/go.sum create mode 100644 apps/quotas/kinds/cue.mod/module.cue create mode 100644 apps/quotas/kinds/manifest.cue create mode 100644 apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go create mode 100644 apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go create mode 100644 apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go create mode 100644 apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go create mode 100644 apps/quotas/pkg/apis/quotas_manifest.go create mode 100644 apps/quotas/pkg/app/app.go create mode 100644 apps/quotas/pkg/app/app_test.go create mode 100644 apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts create mode 100644 apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts create mode 100644 apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts create mode 100644 apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts create mode 100644 pkg/registry/apps/quotas/register.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30bba5db0ff..d8a1e4104bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/Dockerfile b/Dockerfile index d3f63dd0544..558672951e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index b4d2d1dc2e2..9c00f19a029 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -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= diff --git a/apps/example/kinds/manifest.cue b/apps/example/kinds/manifest.cue index 934d419623d..65947eaa151 100644 --- a/apps/example/kinds/manifest.cue +++ b/apps/example/kinds/manifest.cue @@ -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 '/namespaces//'. 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 '/'. cluster: { "/other": { @@ -113,4 +113,4 @@ v1alpha1: { enabled: true } } -} \ No newline at end of file +} diff --git a/apps/quotas/Makefile b/apps/quotas/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/quotas/Makefile @@ -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 \ No newline at end of file diff --git a/apps/quotas/go.mod b/apps/quotas/go.mod new file mode 100644 index 00000000000..1ab18fe8876 --- /dev/null +++ b/apps/quotas/go.mod @@ -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 +) diff --git a/apps/quotas/go.sum b/apps/quotas/go.sum new file mode 100644 index 00000000000..5787fa55023 --- /dev/null +++ b/apps/quotas/go.sum @@ -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= diff --git a/apps/quotas/kinds/cue.mod/module.cue b/apps/quotas/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..6e70f424d2e --- /dev/null +++ b/apps/quotas/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/quotas/kinds" +language: version: "v0.8.2" diff --git a/apps/quotas/kinds/manifest.cue b/apps/quotas/kinds/manifest.cue new file mode 100644 index 00000000000..8f799ff070c --- /dev/null +++ b/apps/quotas/kinds/manifest.cue @@ -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" or + // "v(alpha|beta)". 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 '/namespaces//'. + 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, 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 + } + } +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go new file mode 100644 index 00000000000..b19b40d5e02 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go @@ -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() diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go new file mode 100644 index 00000000000..45394a7f20f --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go @@ -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{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go new file mode 100644 index 00000000000..eb87d022edd --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go @@ -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{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go new file mode 100644 index 00000000000..87d6be2e587 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go @@ -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() diff --git a/apps/quotas/pkg/apis/quotas_manifest.go b/apps/quotas/pkg/apis/quotas_manifest.go new file mode 100644 index 00000000000..e72524d59f3 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas_manifest.go @@ -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||/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 "/" 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) +} diff --git a/apps/quotas/pkg/app/app.go b/apps/quotas/pkg/app/app.go new file mode 100644 index 00000000000..d4862661b60 --- /dev/null +++ b/apps/quotas/pkg/app/app.go @@ -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{} +} diff --git a/apps/quotas/pkg/app/app_test.go b/apps/quotas/pkg/app/app_test.go new file mode 100644 index 00000000000..252c2c4e240 --- /dev/null +++ b/apps/quotas/pkg/app/app_test.go @@ -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) + }) +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts new file mode 100644 index 00000000000..70f306a1b08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts @@ -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; + annotations?: Record; + 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; +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts @@ -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; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..9209753fa64 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts @@ -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: "", +}); + diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts @@ -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; +} + +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; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/go.mod b/go.mod index e589e5a18a5..af798993f31 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 91104bfeaf4..b08b87734c7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/go.work b/go.work index ecfab4e96ce..884f393673e 100644 --- a/go.work +++ b/go.work @@ -23,6 +23,7 @@ use ( ./apps/plugins ./apps/preferences ./apps/provisioning + ./apps/quotas ./apps/scope ./apps/secret ./apps/shorturl diff --git a/go.work.sum b/go.work.sum index 3017d8eb878..eaa5da46cf0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index eba76d2c198..9b03299aab9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -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; diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 472652cc103..fbff17523fc 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -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" ) diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index 913dd02d14d..258d0d1a519 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -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") +} diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index ab7f05e7d00..406494b9d36 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -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 +} diff --git a/pkg/registry/apis/iam/team_search_test.go b/pkg/registry/apis/iam/team_search_test.go index ccc1abbf18c..76efed1c067 100644 --- a/pkg/registry/apis/iam/team_search_test.go +++ b/pkg/registry/apis/iam/team_search_test.go @@ -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 +} diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index 9ea31109495..a1ec8aafd65 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -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) diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 6a6f0c9a2aa..737e7c8aa9a 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -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 := "as.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 { diff --git a/pkg/registry/apps/quotas/register.go b/pkg/registry/apps/quotas/register.go new file mode 100644 index 00000000000..b81d4fef5cf --- /dev/null +++ b/pkg/registry/apps/quotas/register.go @@ -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 := "asapp.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 +} diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index c57fe84d018..a91fa138309 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -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, ) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index d3cf6da6b5a..8500130aeea 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -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 { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 680680791a5..c8ac36bc585 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 15d15a986a5..fda591b9fbd 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index afc599d4eb8..6e106fd9950 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 96ebe6dc5de..9ac920199e8 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 495efc32175..5d7c5a372a0 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -166,6 +166,7 @@ type resourceClientMock struct { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } // always return GRPC Unauthenticated code diff --git a/pkg/storage/unified/proto/resource.proto b/pkg/storage/unified/proto/resource.proto index 91a9288b194..ca91d9efd21 100644 --- a/pkg/storage/unified/proto/resource.proto +++ b/pkg/storage/unified/proto/resource.proto @@ -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); +} diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index b2742f71dc9..e51b7ec4876 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -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( diff --git a/pkg/storage/unified/resource/client_mock.go b/pkg/storage/unified/resource/client_mock.go index fcc7392880e..059421b487a 100644 --- a/pkg/storage/unified/resource/client_mock.go +++ b/pkg/storage/unified/resource/client_mock.go @@ -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)) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 951b1be5b9c..bdfb2e8c7ca 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -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 diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 504c49616f9..b4ab0cdff2a 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -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) + }) +} diff --git a/pkg/storage/unified/resourcepb/resource.pb.go b/pkg/storage/unified/resourcepb/resource.pb.go index 8c6fb47b491..8046b354ccd 100644 --- a/pkg/storage/unified/resourcepb/resource.pb.go +++ b/pkg/storage/unified/resourcepb/resource.pb.go @@ -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, diff --git a/pkg/storage/unified/resourcepb/resource_grpc.pb.go b/pkg/storage/unified/resourcepb/resource_grpc.pb.go index c90aa0f26a6..a37ab264d76 100644 --- a/pkg/storage/unified/resourcepb/resource_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/resource_grpc.pb.go @@ -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 "asClient{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", +} diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 22ef4f5daf5..75b3e80fcb0 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -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 From 45e679eebadd9dea42335620bd93281262f3e2f0 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Tue, 9 Dec 2025 16:54:54 +0100 Subject: [PATCH 363/423] fix: use dsIndexProvider cache on schema migrations (#115018) * fix: use dsIndexProvider cache on migrations * chore: use same comment as before --- apps/dashboard/pkg/migration/conversion/conversion.go | 7 ------- apps/dashboard/pkg/migration/migrate.go | 10 +++++++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index 54edf869f84..ad65dc84c85 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -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 { diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 9c87a45b2db..87940e943a3 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -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) }) } From 533ee1f078738fcef4884aec35708522415294c3 Mon Sep 17 00:00:00 2001 From: Kristina Demeshchik Date: Tue, 9 Dec 2025 10:55:51 -0500 Subject: [PATCH 364/423] Dashboard : Allow applying variable regex to display text (#114426) * Ability to apply regex to display text * Frontend tests * scenes-react version * lock file * adjust tests input * adjust inputs * unused variable * change data type * unit tests * bump scenes * bump scenes * Update docs * V2->V1 conversion * re-generate files * update openai snapshots --- .../kinds/v2alpha1/dashboard_spec.cue | 5 + .../kinds/v2beta1/dashboard_spec.cue | 5 + .../dashboard/v0alpha1/dashboard_kind.cue | 6 ++ .../apis/dashboard/v1beta1/dashboard_kind.cue | 6 ++ .../dashboard/v2alpha1/dashboard_spec.cue | 5 + .../dashboard/v2alpha1/dashboard_spec_gen.go | 12 +++ .../v2alpha1/zz_generated.openapi.go | 6 ++ .../apis/dashboard/v2beta1/dashboard_spec.cue | 5 + .../dashboard/v2beta1/dashboard_spec_gen.go | 12 +++ .../dashboard/v2beta1/zz_generated.openapi.go | 6 ++ apps/dashboard/pkg/apis/dashboard_manifest.go | 4 +- .../input/v1beta1.variable-conversions.json | 8 +- ...v1beta1.variable-conversions.v0alpha1.json | 2 + ...v1beta1.variable-conversions.v2alpha1.json | 2 + .../v1beta1.variable-conversions.v2beta1.json | 2 + .../conversion/v1beta1_to_v2alpha1.go | 11 +++ .../conversion/v2alpha1_to_v1beta1.go | 3 + .../conversion/v2alpha1_to_v2beta1.go | 1 + .../conversion/v2beta1_to_v2alpha1.go | 1 + .../variables/add-template-variables/index.md | 1 + .../new-query-variable.spec.ts | 10 ++ eslint-suppressions.json | 5 - kinds/dashboard/dashboard_kind.cue | 6 ++ package.json | 4 +- packages/grafana-data/src/index.ts | 1 + .../grafana-data/src/types/templateVars.ts | 3 + .../src/selectors/pages.ts | 3 + packages/grafana-schema/src/index.gen.ts | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 10 ++ .../src/schema/dashboard/v2_examples.ts | 1 + .../dashboard/v2alpha1/types.spec.gen.ts | 8 ++ .../dashboard/v2beta1/types.spec.gen.ts | 8 ++ pkg/kinds/dashboard/dashboard_spec_gen.go | 11 +++ .../dashboard.grafana.app-v2alpha1.json | 11 +++ .../dashboard.grafana.app-v2beta1.json | 11 +++ .../transformSceneToSaveModel.test.ts.snap | 3 + ...sformSceneToSaveModelSchemaV2.test.ts.snap | 1 + .../sceneVariablesSetToVariables.test.ts | 3 + .../sceneVariablesSetToVariables.ts | 2 + .../transformSaveModelSchemaV2ToScene.ts | 1 + .../transformSceneToSaveModelSchemaV2.test.ts | 1 + .../components/QueryVariableForm.test.tsx | 23 +++++ .../components/QueryVariableForm.tsx | 41 +++----- .../QueryVariableRegexForm.test.tsx | 96 +++++++++++++++++++ .../components/QueryVariableRegexForm.tsx | 91 ++++++++++++++++++ .../components/VariableTextAreaField.tsx | 9 +- .../editors/QueryVariableEditor.test.tsx | 1 + .../variables/editors/QueryVariableEditor.tsx | 49 ++++------ .../dashboard-scene/utils/variables.test.ts | 2 + .../dashboard-scene/utils/variables.ts | 2 +- .../dashboard/api/ResponseTransformers.ts | 2 + public/locales/en-US/grafana.json | 8 ++ yarn.lock | 22 ++--- 53 files changed, 465 insertions(+), 88 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.test.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 3fe93d7305f..c13eb866c80 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -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] | *[] diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index ef4a27fd6b7..bb833795354 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -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] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index d2a65bbbf24..4a255a57ba6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -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 diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index d2a65bbbf24..4a255a57ba6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -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 diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index d822ad9f38a..ec8d7eead87 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -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] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 625c6fe17c0..883c5399663 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -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 diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index ff7429d7c2b..d697aa4f8b9 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -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: "", diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 0c061075e6d..12d7bec351b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -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] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index a6e63aa0bcc..a8ec1537e38 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -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 diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 3a129374192..2b1fe573336 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -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: "", diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index c4e35bd8f40..c815e815e08 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -32,10 +32,10 @@ var ( rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv1beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) - rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"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)","enum":["value","text"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) - rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"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)","enum":["value","text"],"type":"string"},"VariableSort":{"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","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) ) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json index ae1ef7cd04e..d6010e5100e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -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": [] } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index 4e12e6982ef..725e658dfc2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -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": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index c7c6c646a94..c51582691b9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -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": [], diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index 7b1a899d7fa..441258438a9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -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": [], diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 236a4337efb..4d6fd791fa9 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -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), }, diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index fb2854845ce..fed2691db27 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -1312,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 diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 452f0140047..5b3a0d115b8 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -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) diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 95dfcf76c9d..aa7e8e5f36d 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -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) diff --git a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md index 69dbf4a2547..aef32f9b569 100644 --- a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md @@ -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. diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts index ed92c79ee36..852261dbaf5 100644 --- a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -79,6 +79,16 @@ test.describe( await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); 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 ); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index ba2f59ffe7c..ee6e1a1ba31 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 346edbb4753..454d02f270f 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -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 diff --git a/package.json b/package.json index e9ce0640360..683ed2373de 100644 --- a/package.json +++ b/package.json @@ -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:*", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 5084c577176..63c36639e25 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -521,6 +521,7 @@ export { VariableRefresh, VariableSort, VariableHide, + type VariableRegexApplyTo, type VariableType, type VariableModel, type TypedVariableModel, diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index e6feea4dd3f..8b6ed69463b 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -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'; diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 1fa640a2563..b88dac9099c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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', }, diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 038e62a70a2..140cd138539 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -12,6 +12,7 @@ export type { AnnotationTarget, AnnotationPanelFilter, VariableOption, + VariableRegexApplyTo, DashboardLink, DashboardLinkType, DashboardLinkPlacement, diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index c6a722cbae7..34aa9ac015b 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -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: diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 7c542121a7b..e0f10d0f770 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -293,6 +293,7 @@ export const handyTestingSchema: Spec = { }, refresh: 'onDashboardLoad', regex: 'regex1', + regexApplyTo: 'value', skipUrlSync: false, sort: 'disabled', allowCustomValue: true, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 67d24915bf1..78068b9412d 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -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 diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 315fa4768a4..6a05bed3f1c 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -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 diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index fd8dbae7b77..bd73e527534 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -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. diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 2cad6213d04..b0d21a3a60c 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -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", diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json index 198ad3aea25..f4ecc6c4599 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -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", diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 2a7f5bc424e..512ec28dd77 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -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", }, { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 603f84311fb..b0133a8eb92 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -173,6 +173,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, "refresh": "onDashboardLoad", "regex": "regex1", + "regexApplyTo": "value", "skipUrlSync": false, "sort": "alphabeticalDesc", }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index cc49cfadc77..f45193f2ad8 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -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": [ diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 87f6d650da7..466d659ab4b 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -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, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 23a2b125aac..96781fe9fc2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index f136ddc1998..6d0c450add3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -283,6 +283,7 @@ describe('transformSceneToSaveModelSchemaV2', () => { sort: VariableSortV1.alphabeticalDesc, refresh: VariableRefresh.onDashboardLoad, regex: 'regex1', + regexApplyTo: 'value', allValue: '*', includeAll: true, isMulti: true, diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index d3d23396974..89848a84127 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -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 }, diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 030de016a5f..b20f325d885 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -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) => void; + regexApplyTo?: VariableRegexApplyTo; + onRegexApplyToChange?: (event: VariableRegexApplyTo) => void; sort: VariableSort; onSortChange: (option: SelectableValue) => 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({ /> )} - - - Optional, 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 ( - - see examples - - ). - -
- } - // eslint-disable-next-line @grafana/i18n/no-untranslated-strings - placeholder="/.*-(?.*)-(?.*)-.*/" - onBlur={onRegExChange} - testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2} - width={52} + { + const onRegExChange = jest.fn(); + const onRegexApplyToChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the form fields correctly', () => { + const { getByTestId, getByRole } = render( + + ); + + 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( + + ); + + 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( + + ); + + expect(getByRole('radio', { name: 'Variable value' })).toBeChecked(); + }); + + it('should call onRegExChange when regex input is blurred', () => { + const { getByTestId } = render( + + ); + + 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( + + ); + + const displayTextOption = getByRole('radio', { name: 'Display text' }); + fireEvent.click(displayTextOption); + + expect(onRegexApplyToChange).toHaveBeenCalledTimes(1); + expect(onRegexApplyToChange).toHaveBeenCalledWith('text'); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx new file mode 100644 index 00000000000..dc72927c6fd --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx @@ -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) => void; + regexApplyTo?: VariableRegexApplyTo; + onRegexApplyToChange?: (option: VariableRegexApplyTo) => void; +} + +export function QueryVariableRegexForm({ regex, regexApplyTo, onRegExChange, onRegexApplyToChange }: Props) { + const APPLY_REGEX_TO_OPTIONS: Array> = 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 ( + + + + + Optional, 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 ( + + see examples + + ). + +
+ } + // eslint-disable-next-line @grafana/i18n/no-untranslated-strings + placeholder="/.*-(?.*)-(?.*)-.*/" + onBlur={onRegExChange} + testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2} + width={52} + noMargin + /> + + {onRegexApplyToChange && ( + + + + )} + + + ); +} diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx index ae3e82ac525..260346c8d70 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx @@ -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) => void; - description?: React.ReactNode; + description?: ReactNode; + noMargin?: boolean; } export function VariableTextAreaField({ @@ -31,13 +31,14 @@ export function VariableTextAreaField({ ariaLabel, required, width, + noMargin, testId, }: PropsWithChildren): ReactElement { const styles = useStyles2(getStyles); const id = useId(); return ( - +