Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d68451b70e | ||
|
|
2be78f8e63 | ||
|
|
076d378adc | ||
|
|
6b854bc57d |
@@ -66,6 +66,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
|
||||
}
|
||||
|
||||
func formatErrorPath(path []string) string {
|
||||
if len(path) <= 4 {
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
// omitting the "lineage.schemas[0].schema.spec" prefix here.
|
||||
return strings.Join(path[4:], ".")
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
|
||||
}
|
||||
|
||||
func formatErrorPath(path []string) string {
|
||||
if len(path) <= 4 {
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
// omitting the "lineage.schemas[0].schema.spec" prefix here.
|
||||
return strings.Join(path[4:], ".")
|
||||
}
|
||||
|
||||
+11
-12
@@ -29,7 +29,7 @@ refs:
|
||||
- pattern: /docs/grafana/
|
||||
destination: docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#create-access-policies
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions
|
||||
rbac-role-definitions:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/
|
||||
@@ -66,17 +66,16 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
|
||||
|
||||
The following list contains app plugins that have fine-grained RBAC support.
|
||||
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
|
||||
### Revoke fine-grained access from app plugins
|
||||
|
||||
|
||||
@@ -2136,6 +2136,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/sharing/ShareExportTab.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
@@ -2937,6 +2942,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/manage-dashboards/state/actions.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
||||
@@ -313,7 +313,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(org.RoleEditor)},
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
// Keeping the name to avoid breaking changes (for users who have assigned this role to grant permissions on organization annotations)
|
||||
annotationsReaderRole = ac.RoleRegistration{
|
||||
@@ -620,7 +619,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole,
|
||||
snapshotsCreatorRole, snapshotsDeleterRole, snapshotsReaderRole}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
allAnnotationsReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
|
||||
@@ -126,7 +126,6 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon
|
||||
}
|
||||
|
||||
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardUID); err != nil || !canSave {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
return dashboardGuardianResponse(err)
|
||||
} else if err != nil {
|
||||
@@ -272,7 +271,6 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp
|
||||
return resp
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
@@ -331,7 +329,6 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo
|
||||
return resp
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
@@ -442,7 +439,6 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response
|
||||
|
||||
canSave, err := hs.canMassDeleteAnnotations(c, dashboardUID)
|
||||
if err != nil || !canSave {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
return dashboardGuardianResponse(err)
|
||||
} else if err != nil {
|
||||
@@ -504,7 +500,6 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.
|
||||
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser)
|
||||
if resp != nil {
|
||||
@@ -615,7 +610,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
tempUser = &user.SignedInUser{
|
||||
OrgID: orgID,
|
||||
@@ -632,7 +626,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
|
||||
return nil, errors.New("could not resolve annotation type")
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
switch annotation.GetType() {
|
||||
case annotations.Organization:
|
||||
@@ -669,7 +662,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
if dashboardUID != "" {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
|
||||
@@ -694,7 +686,6 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardU
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
if dashboardUID == "" {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||
|
||||
@@ -166,7 +166,6 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
}
|
||||
|
||||
annotationPermissions := &dashboardsV1.AnnotationPermission{}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID))
|
||||
} else {
|
||||
|
||||
@@ -150,7 +150,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
continue
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if panel.ID == "datagrid" && !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagEnableDatagridEditing) {
|
||||
continue
|
||||
}
|
||||
@@ -191,7 +190,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
|
||||
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
|
||||
trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for"))
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget
|
||||
featureToggles := hs.Features.GetEnabled(c.Req.Context())
|
||||
// this is needed for backwards compatibility with external plugins
|
||||
@@ -408,7 +406,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
|
||||
|
||||
@@ -447,7 +444,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
frontendSettings.Namespace = hs.namespacer(c.OrgID)
|
||||
|
||||
// experimental scope features
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagScopeFilters) {
|
||||
frontendSettings.ListScopesEndpoint = hs.Cfg.ScopesListScopesURL
|
||||
frontendSettings.ListDashboardScopesEndpoint = hs.Cfg.ScopesListDashboardsURL
|
||||
|
||||
@@ -62,7 +62,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
|
||||
if !prefs.Cookies("analytics") {
|
||||
settings.GoogleAnalytics4Id = ""
|
||||
@@ -95,7 +94,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
}
|
||||
|
||||
var regionalFormat string
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) {
|
||||
regionalFormat = "en"
|
||||
|
||||
|
||||
@@ -361,7 +361,6 @@ func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err
|
||||
|
||||
func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string {
|
||||
setCookie := true
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
|
||||
var userID int64
|
||||
if c.SignedInUser != nil && !c.IsNil() {
|
||||
|
||||
@@ -81,7 +81,6 @@ func (proxy *PluginProxy) HandleRequest() {
|
||||
hasSlash := strings.HasSuffix(proxy.proxyPath, "/")
|
||||
proxy.proxyPath = path
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hasSlash && !strings.HasSuffix(path, "/") && proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagPluginProxyPreserveTrailingSlash) {
|
||||
proxy.proxyPath += "/"
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
|
||||
AngularDetected: pluginDef.Angular.Detected,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
|
||||
listItem.IAM = pluginDef.IAM
|
||||
}
|
||||
@@ -491,7 +490,6 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
|
||||
// This is a non-blocking function that verifies that the installer has
|
||||
// the permissions that the plugin requests to have on Grafana.
|
||||
|
||||
@@ -29,7 +29,6 @@ func (c *ResultConverter) Convert(ctx context.Context,
|
||||
}
|
||||
|
||||
var dt data.FrameType
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
dt, useDataplane, _ := shouldUseDataplane(frames, logger, c.Features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane))
|
||||
if useDataplane {
|
||||
logger.Debug("Handling SSE data source query through dataplane", "datatype", dt)
|
||||
|
||||
@@ -70,7 +70,6 @@ func handleDataplaneFrames(ctx context.Context, tracer tracing.Tracer, features
|
||||
case data.KindTimeSeries:
|
||||
return handleDataplaneTimeseries(frames)
|
||||
case data.KindNumeric:
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
sortMetrics := !features.IsEnabled(ctx, featuremgmt.FlagDisableNumericMetricsSortingInExpressions)
|
||||
return handleDataplaneNumeric(frames, sortMetrics)
|
||||
default:
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ type DataPipeline []Node
|
||||
// map of the refId of the of each command
|
||||
func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (mathexp.Vars, error) {
|
||||
vars := make(mathexp.Vars)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
|
||||
groupByDSFlag := s.features.IsEnabled(c, featuremgmt.FlagSseGroupByDatasource)
|
||||
// Execute datasource nodes first, and grouped by datasource.
|
||||
if groupByDSFlag {
|
||||
|
||||
@@ -15,7 +15,6 @@ 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"
|
||||
@@ -31,7 +30,6 @@ 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"
|
||||
@@ -56,7 +54,9 @@ 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"
|
||||
_ "github.com/testcontainers/testcontainers-go"
|
||||
)
|
||||
|
||||
@@ -64,7 +64,6 @@ func (l *loggerImpl) Middleware() web.Middleware {
|
||||
// put the start time on context so we can measure it later.
|
||||
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) {
|
||||
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
|
||||
}
|
||||
|
||||
@@ -114,7 +114,6 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
|
||||
handler = "notfound"
|
||||
} else {
|
||||
// log requests where we could not identify handler so we can register them.
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(r.Context(), featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
|
||||
log.Warn("request instrumented as unknown", "path", r.URL.Path, "status_code", status)
|
||||
}
|
||||
|
||||
@@ -90,8 +90,3 @@ func (d *directResourceClient) Watch(ctx context.Context, in *resourcepb.WatchRe
|
||||
func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) {
|
||||
return nil, fmt.Errorf("BulkProcess not supported with direct resource client")
|
||||
}
|
||||
|
||||
// RebuildIndexes implements resource.ResourceClient.
|
||||
func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
@@ -109,14 +109,13 @@ func ProvideMigratorDashboardAccessor(
|
||||
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,
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -133,15 +132,14 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider,
|
||||
) *dashboardSqlAccess {
|
||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
return &dashboardSqlAccess{
|
||||
sql: sql,
|
||||
namespacer: namespacer,
|
||||
dashStore: dashStore,
|
||||
provisioning: provisioning,
|
||||
dashboardSearchClient: *dashboardSearchClient,
|
||||
dashboardPermissionSvc: dashboardPermissionSvc,
|
||||
libraryPanelSvc: libraryPanelSvc,
|
||||
accessControl: accessControl,
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
sql: sql,
|
||||
namespacer: namespacer,
|
||||
dashStore: dashStore,
|
||||
provisioning: provisioning,
|
||||
dashboardSearchClient: *dashboardSearchClient,
|
||||
dashboardPermissionSvc: dashboardPermissionSvc,
|
||||
libraryPanelSvc: libraryPanelSvc,
|
||||
accessControl: accessControl,
|
||||
invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled),
|
||||
}
|
||||
}
|
||||
@@ -1048,7 +1046,3 @@ func parseLibraryPanelRow(p panel) (dashboardV0.LibraryPanel, error) {
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (b *dashboardSqlAccess) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
@@ -33,11 +33,9 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
|
||||
case *v0.Dashboard:
|
||||
errorOnSchemaMismatches = false // Never error for v0
|
||||
case *v1.Dashboard:
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1)
|
||||
case *v2alpha1.Dashboard:
|
||||
case *v2beta1.Dashboard:
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid dashboard type: %T", obj)
|
||||
@@ -47,7 +45,6 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
|
||||
return nil, apierrors.NewBadRequest("Not supported: FieldValidationMode: Warn")
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
alwaysLogSchemaValidationErrors := b.features.IsEnabled(ctx, featuremgmt.FlagDashboardSchemaValidationLogging)
|
||||
|
||||
var errors field.ErrorList
|
||||
|
||||
@@ -581,8 +581,3 @@ func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.Resource
|
||||
func (m *mockSearchClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
|
||||
return m.search, m.searchErr
|
||||
}
|
||||
|
||||
// RebuildIndexes implements resourcepb.ResourceIndexClient.
|
||||
func (m *mockSearchClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -176,28 +175,6 @@ func TestIntegrationDistributor(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RebuildIndexes", func(t *testing.T) {
|
||||
instanceResponseCount := make(map[string]int)
|
||||
|
||||
// simulate RebuildIndexes for a single namespace
|
||||
testNamespace := testNamespaces[0]
|
||||
|
||||
req := &resourcepb.RebuildIndexesRequest{
|
||||
Namespace: testNamespace,
|
||||
Keys: []*resourcepb.ResourceKey{{
|
||||
Namespace: testNamespace,
|
||||
Group: "folder.grafana.app",
|
||||
Resource: "folders",
|
||||
}},
|
||||
}
|
||||
distributorRes := getDistributorResponse(t, req, distributorServer.resourceClient.RebuildIndexes, instanceResponseCount)
|
||||
require.Nil(t, distributorRes.Error)
|
||||
|
||||
// assert all instances got the response by looking at the merged details
|
||||
count := strings.Count(distributorRes.Details, "{instance:")
|
||||
require.Equal(t, len(testServers), count)
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, testServer := range testServers {
|
||||
wg.Add(1)
|
||||
|
||||
Generated
+2
-2
@@ -666,7 +666,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1309,7 +1309,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -741,7 +741,6 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole")
|
||||
defer span.End()
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
|
||||
s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.")
|
||||
return nil
|
||||
@@ -758,7 +757,6 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole")
|
||||
defer span.End()
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
|
||||
s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.")
|
||||
return nil
|
||||
|
||||
@@ -28,7 +28,6 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti
|
||||
var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...)
|
||||
|
||||
func getDashboardViewActions(features featuremgmt.FeatureToggles) []string {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead)
|
||||
}
|
||||
@@ -36,7 +35,6 @@ func getDashboardViewActions(features featuremgmt.FeatureToggles) []string {
|
||||
}
|
||||
|
||||
func getDashboardEditActions(features featuremgmt.FeatureToggles) []string {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...)
|
||||
}
|
||||
@@ -44,7 +42,6 @@ func getDashboardEditActions(features featuremgmt.FeatureToggles) []string {
|
||||
}
|
||||
|
||||
func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQ
|
||||
scopeTypes := annotationScopeTypes(scopes)
|
||||
_, canAccessOrgAnnotations := scopeTypes[annotations.Organization.String()]
|
||||
_, canAccessDashAnnotations := scopeTypes[annotations.Dashboard.String()]
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
canAccessDashAnnotations = true
|
||||
}
|
||||
@@ -123,7 +122,6 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context,
|
||||
}
|
||||
|
||||
filterType := searchstore.TypeDashboard
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
filterType = searchstore.TypeAnnotation
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ func ProvideRegistration(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP)
|
||||
if !hasEnabledProviders {
|
||||
|
||||
@@ -210,7 +210,6 @@ func (c *CachingServiceClient) WithQueryDataCaching(ctx context.Context, req *ba
|
||||
// Update the query cache with the result for this metrics request
|
||||
if err == nil && cr.UpdateCacheFn != nil {
|
||||
// If AWS async caching is not enabled, use the old code path
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.features == nil || !c.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) {
|
||||
cr.UpdateCacheFn(ctx, resp)
|
||||
} else if reqCtx != nil {
|
||||
|
||||
@@ -2009,7 +2009,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex
|
||||
request.Limit = query.Limit
|
||||
request.Page = query.Page
|
||||
request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+
|
||||
request.Fields = dashboardsearch.IncludeFields
|
||||
request.Fields = append(
|
||||
dashboardsearch.IncludeFields,
|
||||
// Include the dashboard legacy ID in the results, as it is needed when
|
||||
// determining whether a provisioned dashboard exists or not, see
|
||||
// `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`.
|
||||
resource.SEARCH_FIELD_LEGACY_ID,
|
||||
resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID,
|
||||
)
|
||||
|
||||
namespace := dr.k8sclient.GetNamespace(query.OrgId)
|
||||
var err error
|
||||
|
||||
@@ -2016,6 +2016,26 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) {
|
||||
_, err := service.searchDashboardsThroughK8s(ctx, query)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("search will request legacy dashboard ID", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
k8sCliMock := new(client.MockK8sHandler)
|
||||
service := &DashboardServiceImpl{k8sclient: k8sCliMock}
|
||||
query := &dashboards.FindPersistedDashboardsQuery{
|
||||
ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck
|
||||
OrgId: 1,
|
||||
}
|
||||
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
|
||||
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
|
||||
return slices.Contains(req.Fields, "grafana.app/deprecatedInternalID") &&
|
||||
slices.Contains(req.Fields, "labels.grafana.app/deprecatedInternalID")
|
||||
})).Return(&resourcepb.ResourceSearchResponse{
|
||||
Results: &resourcepb.ResourceTable{},
|
||||
TotalHits: 0,
|
||||
}, nil)
|
||||
_, err := service.searchDashboardsThroughK8s(ctx, query)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
type FeatureToggles interface {
|
||||
// IsEnabled checks if a feature is enabled for a given context.
|
||||
// The settings may be per user, tenant, or globally set in the cloud
|
||||
//
|
||||
// Deprecated: FeatureToggles.IsEnabled is deprecated and will be removed in a future release.
|
||||
// Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client]), for example:
|
||||
// openfeature.NewDefaultClient().Boolean(ctx, "your-flag", false, openfeature.TransactionContext(ctx))
|
||||
IsEnabled(ctx context.Context, flag string) bool
|
||||
|
||||
// IsEnabledGlobally checks if a flag is configured globally. For now, this is the same
|
||||
@@ -23,9 +19,7 @@ type FeatureToggles interface {
|
||||
// a full server restart for a change to take place.
|
||||
//
|
||||
// Deprecated: FeatureToggles.IsEnabledGlobally is deprecated and will be removed in a future release.
|
||||
// Toggles that must be reliably evaluated at the service startup should be
|
||||
// changed to settings (see setting.StartupSettings), and/or removed entirely.
|
||||
// For app registration please use `grafana-apiserver.runtime_config` in settings.ini
|
||||
// Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client])
|
||||
IsEnabledGlobally(flag string) bool
|
||||
|
||||
// Get the enabled flags -- this *may* also include disabled flags (with value false)
|
||||
|
||||
@@ -148,7 +148,6 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagKubernetesLibraryPanels) {
|
||||
l.k8sHandler.getK8sLibraryElement(c)
|
||||
return nil // already handled in the k8s handler
|
||||
|
||||
@@ -44,7 +44,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
|
||||
})
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hasAccess(cloudmigration.MigrationAssistantAccess) && s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) {
|
||||
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
|
||||
Text: "Migrate to Grafana Cloud",
|
||||
@@ -100,7 +99,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if (s.cfg.Env == setting.Dev) || s.features.IsEnabled(ctx, featuremgmt.FlagEnableExtensionsAdminPage) && hasAccess(pluginaccesscontrol.AdminAccessEvaluator) {
|
||||
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
|
||||
Text: "Extensions",
|
||||
@@ -149,7 +147,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.license.FeatureEnabled("groupsync") &&
|
||||
s.features.IsEnabled(ctx, featuremgmt.FlagGroupAttributeSync) &&
|
||||
hasAccess(ac.EvalAny(
|
||||
|
||||
@@ -407,7 +407,6 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && (c.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
@@ -436,7 +435,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
var alertChildNavs []*navtree.NavLink
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) {
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
@@ -494,7 +492,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts with active notifications", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
@@ -506,7 +503,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
})
|
||||
}
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
|
||||
@@ -77,7 +77,6 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body api
|
||||
return response.Error(http.StatusBadRequest, "Invalid alertmanager choice specified", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
disableExternal := srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingDisableSendAlertsExternal)
|
||||
if disableExternal && sendAlertsTo != ngmodels.InternalAlertmanager {
|
||||
return response.Error(http.StatusBadRequest, "Sending alerts to external alertmanagers is disallowed on this instance", err)
|
||||
|
||||
@@ -79,7 +79,6 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to authorize access to rule group", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) {
|
||||
if _, err := store.OptimizeAlertQueries(rule.Data); err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "Failed to optimize query")
|
||||
@@ -179,7 +178,6 @@ func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimod
|
||||
}
|
||||
|
||||
var optimizations []store.Optimization
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) {
|
||||
var err error
|
||||
optimizations, err = store.OptimizeAlertQueries(cond.Data)
|
||||
@@ -225,7 +223,6 @@ func addOptimizedQueryWarnings(evalResults *backend.QueryDataResponse, optimizat
|
||||
}
|
||||
|
||||
func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingBacktesting) {
|
||||
return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -27,9 +26,6 @@ import (
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
type RuleStoreReader interface {
|
||||
@@ -58,9 +54,6 @@ type PrometheusSrv struct {
|
||||
provenanceStore ProvenanceStore
|
||||
}
|
||||
|
||||
// Package-level OpenTelemetry tracer per Grafana instrumentation conventions.
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus")
|
||||
|
||||
func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv {
|
||||
return &PrometheusSrv{
|
||||
log,
|
||||
@@ -226,14 +219,6 @@ func GetStatesFromQuery(v url.Values) (map[eval.State]struct{}, error) {
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func MapStateSetToStrings(stateSet map[eval.State]struct{}) []string {
|
||||
states := make([]string, 0, len(stateSet))
|
||||
for state := range stateSet {
|
||||
states = append(states, state.String())
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func GetHealthFromQuery(v url.Values) (map[string]struct{}, error) {
|
||||
health := make(map[string]struct{})
|
||||
for _, s := range v["health"] {
|
||||
@@ -267,13 +252,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
|
||||
c.Query("")
|
||||
|
||||
ctx, span := tracer.Start(c.Req.Context(), "api.prometheus.RouteGetRuleStatuses")
|
||||
defer span.End()
|
||||
// Propagate the new context so child spans can attach to it.
|
||||
c.Req = c.Req.WithContext(ctx)
|
||||
orgID := c.GetOrgID()
|
||||
span.SetAttributes(attribute.Int64("org_id", orgID))
|
||||
|
||||
ruleResponse := apimodels.RuleResponse{
|
||||
DiscoveryBase: apimodels.DiscoveryBase{
|
||||
Status: "success",
|
||||
@@ -283,14 +261,13 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
},
|
||||
}
|
||||
|
||||
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), orgID, c.SignedInUser)
|
||||
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
ruleResponse.Status = "error"
|
||||
ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error())
|
||||
ruleResponse.ErrorType = apiv1.ErrServer
|
||||
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
|
||||
}
|
||||
span.AddEvent("User visible namespaces retrieved")
|
||||
|
||||
allowedNamespaces := map[string]string{}
|
||||
for namespaceUID, folder := range namespaceMap {
|
||||
@@ -306,8 +283,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
allowedNamespaces[namespaceUID] = folder.Fullpath
|
||||
}
|
||||
}
|
||||
span.AddEvent("User permissions checked")
|
||||
span.SetAttributes(attribute.Int("allowedNamespaces", len(allowedNamespaces)))
|
||||
|
||||
provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.GetOrgID(), (&ngmodels.AlertRule{}).ResourceType())
|
||||
if err != nil {
|
||||
@@ -322,7 +297,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
||||
srv.store,
|
||||
RuleGroupStatusesOptions{
|
||||
Ctx: c.Req.Context(),
|
||||
OrgID: orgID,
|
||||
OrgID: c.OrgID,
|
||||
Query: c.Req.Form,
|
||||
AllowedNamespaces: allowedNamespaces,
|
||||
},
|
||||
@@ -430,10 +405,6 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler
|
||||
}
|
||||
|
||||
func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse {
|
||||
ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2")
|
||||
defer span.End()
|
||||
opts.Ctx = ctx
|
||||
|
||||
ruleResponse := apimodels.RuleResponse{
|
||||
DiscoveryBase: apimodels.DiscoveryBase{
|
||||
Status: "success",
|
||||
@@ -457,17 +428,9 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.String("dashboard_uid", dashboardUID),
|
||||
attribute.Int64("panel_id", panelID),
|
||||
)
|
||||
|
||||
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
|
||||
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
|
||||
span.SetAttributes(
|
||||
attribute.Int64("limit_rules", limitRulesPerGroup),
|
||||
attribute.Int64("limit_alerts", limitAlertsPerRule),
|
||||
)
|
||||
matchers, err := getMatchersFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
ruleResponse.Status = "error"
|
||||
@@ -475,8 +438,6 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
span.SetAttributes(attribute.Int("matcher_count", len(matchers)))
|
||||
|
||||
stateFilterSet, err := GetStatesFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
ruleResponse.Status = "error"
|
||||
@@ -484,10 +445,6 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Int("state_filter_count", len(stateFilterSet)),
|
||||
attribute.StringSlice("state_filter", MapStateSetToStrings(stateFilterSet)),
|
||||
)
|
||||
|
||||
healthFilterSet, err := GetHealthFromQuery(opts.Query)
|
||||
if err != nil {
|
||||
@@ -496,18 +453,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Int("health_filter_count", len(healthFilterSet)),
|
||||
attribute.StringSlice("health_filter", slices.Collect(maps.Keys(healthFilterSet))),
|
||||
)
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Bool("include_internal_labels", len(labelOptions) == 0),
|
||||
)
|
||||
|
||||
if len(opts.AllowedNamespaces) == 0 {
|
||||
log.Debug("User does not have access to any namespaces")
|
||||
@@ -526,36 +476,19 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
}
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Bool("folder_uid_set", folderUID != ""),
|
||||
attribute.Int("namespace_count", len(namespaceUIDs)),
|
||||
)
|
||||
|
||||
ruleGroups := opts.Query["rule_group"]
|
||||
ruleUIDs := opts.Query["rule_uid"]
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int("rule_group_count", len(ruleGroups)),
|
||||
attribute.Int("rule_uid_count", len(ruleUIDs)),
|
||||
)
|
||||
|
||||
receiverName := opts.Query.Get("receiver_name")
|
||||
span.SetAttributes(attribute.Bool("receiver_name_set", receiverName != ""))
|
||||
|
||||
title := opts.Query.Get("search.rule_name")
|
||||
span.SetAttributes(attribute.Bool("search_rule_name_set", title != ""))
|
||||
|
||||
searchRuleGroup := opts.Query.Get("search.rule_group")
|
||||
span.SetAttributes(attribute.Bool("search_rule_group_set", searchRuleGroup != ""))
|
||||
|
||||
var ruleType ngmodels.RuleTypeFilter
|
||||
switch ngmodels.RuleType(opts.Query.Get("rule_type")) {
|
||||
case ngmodels.RuleTypeAlerting:
|
||||
ruleType = ngmodels.RuleTypeFilterAlerting
|
||||
span.SetAttributes(attribute.Bool("alerting_only", true))
|
||||
case ngmodels.RuleTypeRecording:
|
||||
ruleType = ngmodels.RuleTypeFilterRecording
|
||||
span.SetAttributes(attribute.Bool("recording_only", true))
|
||||
default:
|
||||
ruleType = ngmodels.RuleTypeFilterAll
|
||||
}
|
||||
@@ -574,23 +507,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1)
|
||||
maxRules := getInt64WithDefault(opts.Query, "rule_limit", -1)
|
||||
nextToken := opts.Query.Get("group_next_token")
|
||||
span.SetAttributes(
|
||||
attribute.Int64("group_limit", maxGroups),
|
||||
attribute.Int64("rule_limit", maxRules),
|
||||
attribute.Bool("group_next_token_set", nextToken != ""),
|
||||
)
|
||||
|
||||
if maxGroups == 0 || maxRules == 0 {
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
ruleNames := opts.Query["rule_name"]
|
||||
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
|
||||
for _, rn := range ruleNames {
|
||||
ruleNamesSet[rn] = struct{}{}
|
||||
}
|
||||
span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet)))
|
||||
|
||||
byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{
|
||||
ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{
|
||||
OrgID: opts.OrgID,
|
||||
@@ -615,11 +536,12 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.ErrorType = apiv1.ErrServer
|
||||
return ruleResponse
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Int("store_rule_list_len", len(ruleList)),
|
||||
attribute.Bool("store_continue_token_set", continueToken != ""),
|
||||
)
|
||||
span.AddEvent("Alert rules retrieved from store")
|
||||
|
||||
ruleNames := opts.Query["rule_name"]
|
||||
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
|
||||
for _, rn := range ruleNames {
|
||||
ruleNamesSet[rn] = struct{}{}
|
||||
}
|
||||
|
||||
groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.AllowedNamespaces)
|
||||
rulesTotals := make(map[string]int64, len(groupedRules))
|
||||
|
||||
@@ -194,11 +194,8 @@ func (ng *AlertNG) init() error {
|
||||
var opts []notifier.Option
|
||||
moaLogger := log.New("ngalert.multiorg.alertmanager")
|
||||
crypto := notifier.NewCrypto(ng.SecretsService, ng.store, moaLogger)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
remotePrimary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemotePrimary)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
remoteSecondaryWithRemoteState := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondaryWithRemoteState)
|
||||
if remotePrimary || remoteSecondary || remoteSecondaryWithRemoteState {
|
||||
m := ng.Metrics.GetRemoteAlertmanagerMetrics()
|
||||
@@ -720,7 +717,6 @@ func configureNotificationHistorian(
|
||||
l log.Logger,
|
||||
tracer tracing.Tracer,
|
||||
) (nfstatus.NotificationHistorian, error) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !featureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNotificationHistory) || !cfg.Enabled {
|
||||
met.Info.Set(0)
|
||||
return nil, nil
|
||||
|
||||
@@ -89,7 +89,6 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error
|
||||
|
||||
d.logger.Debug("Attempting to sync admin configs", "count", len(cfgs))
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
disableExternal := d.featureManager.IsEnabled(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal)
|
||||
orgsFound := make(map[int64]struct{}, len(cfgs))
|
||||
|
||||
|
||||
@@ -1097,7 +1097,6 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
|
||||
continue
|
||||
}
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingQueryOptimization) {
|
||||
if optimizations, err := OptimizeAlertQueries(converted.Data); err != nil {
|
||||
st.Logger.Error("Could not migrate rule from range to instant query", "rule", rule.UID, "err", err)
|
||||
|
||||
@@ -87,12 +87,10 @@ func ProvideSyncer(
|
||||
}
|
||||
|
||||
func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync) {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading) {
|
||||
logging.DefaultLogger.Warn("pluginInstallAPISync is enabled, but pluginStoreServiceLoading is disabled. skipping plugin sync.")
|
||||
return nil
|
||||
|
||||
@@ -61,7 +61,6 @@ func UpdatePreferencesFor(ctx context.Context,
|
||||
Navbar: dtoCmd.Navbar,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) {
|
||||
saveCmd.RegionalFormat = dtoCmd.RegionalFormat
|
||||
}
|
||||
@@ -102,7 +101,6 @@ func GetPreferencesFor(ctx context.Context,
|
||||
dto.Language = &preference.JSONData.Language
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) {
|
||||
if preference.JSONData.RegionalFormat != "" {
|
||||
dto.RegionalFormat = &preference.JSONData.RegionalFormat
|
||||
|
||||
@@ -43,7 +43,6 @@ func ProvidePromTypeMigrationProvider(
|
||||
}
|
||||
|
||||
func (s *PromTypeMigrationProviderImpl) Run(ctx context.Context) error {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagPrometheusTypeMigration) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
@@ -9,10 +10,12 @@ import (
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
@@ -28,7 +31,7 @@ type DashboardProvisioner interface {
|
||||
}
|
||||
|
||||
// DashboardProvisionerFactory creates DashboardProvisioners based on input
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error)
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (DashboardProvisioner, error)
|
||||
|
||||
// Provisioner is responsible for syncing dashboard from disk to Grafana's database.
|
||||
type Provisioner struct {
|
||||
@@ -38,6 +41,8 @@ type Provisioner struct {
|
||||
duplicateValidator duplicateValidator
|
||||
provisioner dashboards.DashboardProvisioningService
|
||||
dual dualwrite.Service
|
||||
serverLock *serverlock.ServerLockService
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (provider *Provisioner) HasDashboardSources() bool {
|
||||
@@ -45,7 +50,7 @@ func (provider *Provisioner) HasDashboardSources() bool {
|
||||
}
|
||||
|
||||
// New returns a new DashboardProvisioner
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) {
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, cfg *setting.Cfg, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service, serverLockService *serverlock.ServerLockService) (DashboardProvisioner, error) {
|
||||
logger := log.New("provisioning.dashboard")
|
||||
cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)}
|
||||
configs, err := cfgReader.readConfig(ctx)
|
||||
@@ -78,6 +83,8 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
|
||||
duplicateValidator: newDuplicateValidator(logger, fileReaders),
|
||||
provisioner: provisioner,
|
||||
dual: dual,
|
||||
serverLock: serverLockService,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
return d, nil
|
||||
@@ -95,23 +102,53 @@ func (provider *Provisioner) Provision(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
provider.log.Info("starting to provision dashboards")
|
||||
var errProvisioning error
|
||||
|
||||
for _, reader := range provider.fileReaders {
|
||||
if err := reader.walkDisk(ctx); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
|
||||
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
|
||||
// retry obtaining the lock for 20 attempts
|
||||
retryOpt := func(attempts int) error {
|
||||
if attempts < 20 {
|
||||
return nil
|
||||
}
|
||||
return errors.New("retries exhausted")
|
||||
}
|
||||
|
||||
provider.duplicateValidator.validate()
|
||||
provider.log.Info("finished to provision dashboards")
|
||||
return nil
|
||||
lockTimeConfig := serverlock.LockTimeConfig{
|
||||
// if a replica crashes while holding the lock, other replicas can obtain the
|
||||
// lock after this duration (15s default value, might be configured via config file)
|
||||
MaxInterval: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds) * time.Second,
|
||||
|
||||
// wait beetween 100ms and 1s before retrying to obtain the lock (default values, might be configured via config file)
|
||||
MinWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMinWaitMs) * time.Millisecond,
|
||||
MaxWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs) * time.Millisecond,
|
||||
}
|
||||
|
||||
// this means that if we fail to obtain the lock after ~10 seconds, we return an error
|
||||
lockErr := provider.serverLock.LockExecuteAndReleaseWithRetries(ctx, "provisioning_dashboards", lockTimeConfig, func(ctx context.Context) {
|
||||
provider.log.Info("starting to provision dashboards")
|
||||
|
||||
for _, reader := range provider.fileReaders {
|
||||
if err := reader.walkDisk(ctx); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
|
||||
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
errProvisioning = fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
provider.duplicateValidator.validate()
|
||||
provider.log.Info("finished to provision dashboards")
|
||||
}, retryOpt)
|
||||
|
||||
if lockErr != nil {
|
||||
provider.log.Error("Failed to obtain dashboard provisioning lock", "error", lockErr)
|
||||
return lockErr
|
||||
}
|
||||
|
||||
return errProvisioning
|
||||
}
|
||||
|
||||
// CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/dskit/services"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -64,6 +65,7 @@ func ProvideService(
|
||||
tracer tracing.Tracer,
|
||||
dual dualwrite.Service,
|
||||
promTypeMigrationProvider promtypemigration.PromTypeMigrationProvider,
|
||||
serverLockService *serverlock.ServerLockService,
|
||||
) (*ProvisioningServiceImpl, error) {
|
||||
s := &ProvisioningServiceImpl{
|
||||
Cfg: cfg,
|
||||
@@ -92,6 +94,7 @@ func ProvideService(
|
||||
tracer: tracer,
|
||||
migratePrometheusType: promTypeMigrationProvider.Run,
|
||||
dual: dual,
|
||||
serverLock: serverLockService,
|
||||
}
|
||||
|
||||
s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName)
|
||||
@@ -166,7 +169,7 @@ func (ps *ProvisioningServiceImpl) running(ctx context.Context) error {
|
||||
|
||||
func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error {
|
||||
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual)
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.Cfg, ps.orgService, ps.dashboardService, ps.folderService, ps.dual, ps.serverLock)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
|
||||
}
|
||||
@@ -242,6 +245,7 @@ type ProvisioningServiceImpl struct {
|
||||
resourcePermissions accesscontrol.ReceiverPermissionsService
|
||||
tracer tracing.Tracer
|
||||
dual dualwrite.Service
|
||||
serverLock *serverlock.ServerLockService
|
||||
migratePrometheusType func(context.Context) error
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
@@ -160,7 +162,7 @@ func setup(t *testing.T) *serviceTestStruct {
|
||||
searchStub := searchV2.NewStubSearchService()
|
||||
|
||||
service, err := newProvisioningServiceImpl(
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) {
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (dashboards.DashboardProvisioner, error) {
|
||||
serviceTest.dashboardProvisionerInstantiations++
|
||||
return serviceTest.mock, nil
|
||||
},
|
||||
|
||||
@@ -37,7 +37,6 @@ func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*Ren
|
||||
|
||||
var renderUser *RenderUser
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if looksLikeJWT(key) && rs.features.IsEnabled(ctx, featuremgmt.FlagRenderAuthJWT) {
|
||||
from = "jwt"
|
||||
renderUser = rs.getRenderUserFromJWT(key)
|
||||
|
||||
@@ -122,7 +122,6 @@ func (s *SecretsService) registerUsageMetrics() {
|
||||
|
||||
// Enabled / disabled
|
||||
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
|
||||
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
|
||||
}
|
||||
@@ -168,7 +167,6 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret
|
||||
defer span.End()
|
||||
|
||||
// Use legacy encryption service if featuremgmt.FlagDisableEnvelopeEncryption toggle is on
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
|
||||
return s.enc.Encrypt(ctx, payload, s.cfg.SecretKey)
|
||||
}
|
||||
@@ -345,7 +343,6 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
|
||||
|
||||
// If encrypted with envelope encryption, the feature is disabled and
|
||||
// no provider is initialized, then we throw an error.
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.encryptedWithEnvelopeEncryption(payload) &&
|
||||
s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) &&
|
||||
!s.providersInitialized() {
|
||||
@@ -483,7 +480,6 @@ func (s *SecretsService) RotateDataKeys(ctx context.Context) error {
|
||||
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
|
||||
s.log.Info("Data keys re-encryption triggered")
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
|
||||
s.log.Info("Envelope encryption is not enabled but trying to init providers anyway...")
|
||||
|
||||
|
||||
@@ -150,6 +150,11 @@ type Cfg struct {
|
||||
PluginsPath string
|
||||
EnterpriseLicensePath string
|
||||
|
||||
// Classic Provisioning settings
|
||||
ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64
|
||||
ClassicProvisioningDashboardsServerLockMinWaitMs int64
|
||||
ClassicProvisioningDashboardsServerLockMaxWaitMs int64
|
||||
|
||||
// SMTP email settings
|
||||
Smtp SmtpSettings
|
||||
|
||||
@@ -1221,6 +1226,8 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.readClassicProvisioningSettings(iniFile)
|
||||
|
||||
// read dashboard settings
|
||||
dashboards := iniFile.Section("dashboards")
|
||||
cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
|
||||
@@ -2107,6 +2114,12 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) {
|
||||
cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100)
|
||||
cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000)
|
||||
cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15)
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error {
|
||||
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
|
||||
cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath)
|
||||
|
||||
@@ -13,8 +13,6 @@ service ResourceIndex {
|
||||
|
||||
// Get the resource stats
|
||||
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
|
||||
|
||||
rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse);
|
||||
}
|
||||
|
||||
// Get statistics across multiple resources
|
||||
@@ -140,23 +138,3 @@ message ResourceSearchResponse {
|
||||
// Facet results
|
||||
map<string,Facet> facet = 7;
|
||||
}
|
||||
|
||||
message RebuildIndexesRequest {
|
||||
// Namespace (tenant) must be the same as all keys' namespace
|
||||
string namespace = 1;
|
||||
|
||||
// List of ResourceKeys (Namespace + Group + Resource)
|
||||
repeated ResourceKey keys = 2;
|
||||
}
|
||||
|
||||
message RebuildIndexesResponse {
|
||||
// Total count of rebuilt indexes
|
||||
int64 rebuildCount = 1;
|
||||
|
||||
// Result message
|
||||
string details = 2;
|
||||
|
||||
// Error details
|
||||
ErrorResult error = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package resource
|
||||
|
||||
@@ -838,80 +838,6 @@ func (_c *MockResourceClient_Read_Call) RunAndReturn(run func(context.Context, *
|
||||
return _c
|
||||
}
|
||||
|
||||
// RebuildIndexes provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockResourceClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, 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 RebuildIndexes")
|
||||
}
|
||||
|
||||
var r0 *resourcepb.RebuildIndexesResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) *resourcepb.RebuildIndexesResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*resourcepb.RebuildIndexesResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockResourceClient_RebuildIndexes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RebuildIndexes'
|
||||
type MockResourceClient_RebuildIndexes_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RebuildIndexes is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *resourcepb.RebuildIndexesRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *MockResourceClient_Expecter) RebuildIndexes(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_RebuildIndexes_Call {
|
||||
return &MockResourceClient_RebuildIndexes_Call{Call: _e.mock.On("RebuildIndexes",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_RebuildIndexes_Call) Run(run func(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption)) *MockResourceClient_RebuildIndexes_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.RebuildIndexesRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_RebuildIndexes_Call) Return(_a0 *resourcepb.RebuildIndexesResponse, _a1 error) *MockResourceClient_RebuildIndexes_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_RebuildIndexes_Call) RunAndReturn(run func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)) *MockResourceClient_RebuildIndexes_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Search provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockResourceClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
|
||||
@@ -234,9 +234,6 @@ func combineRebuildRequests(a, b rebuildRequest) (c rebuildRequest, ok bool) {
|
||||
ret.lastImportTime = b.lastImportTime
|
||||
}
|
||||
|
||||
// Combine complete channels
|
||||
ret.completeChannels = append(a.completeChannels, b.completeChannels...)
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
@@ -515,52 +512,6 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (s *searchSupport) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "resource.searchSupport.RebuildIndexes")
|
||||
defer span.End()
|
||||
|
||||
filterKeys := make([]NamespacedResource, len(req.Keys))
|
||||
for _, key := range req.Keys {
|
||||
if req.Namespace != key.Namespace {
|
||||
return &resourcepb.RebuildIndexesResponse{
|
||||
Error: NewBadRequestError("key namespace does not match request namespace"),
|
||||
}, nil
|
||||
}
|
||||
filterKeys = append(filterKeys, NamespacedResource{
|
||||
Namespace: key.Namespace,
|
||||
Group: key.Group,
|
||||
Resource: key.Resource,
|
||||
})
|
||||
}
|
||||
|
||||
importTimes, err := s.getLastImportTimes(ctx)
|
||||
if err != nil {
|
||||
return &resourcepb.RebuildIndexesResponse{
|
||||
Error: AsErrorResult(err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
completeChs := s.findIndexesToRebuild(importTimes, filterKeys, time.Now())
|
||||
rebuildCount := len(completeChs)
|
||||
for _, ch := range completeChs {
|
||||
select {
|
||||
case <-ch:
|
||||
continue
|
||||
case <-ctx.Done(): // request was done before all indexes rebuilt
|
||||
return &resourcepb.RebuildIndexesResponse{
|
||||
RebuildCount: int64(rebuildCount),
|
||||
Details: fmt.Sprintf("returning before all index rebuilds completed for %d indexes", rebuildCount),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// All rebuilds completed successfully
|
||||
return &resourcepb.RebuildIndexesResponse{
|
||||
RebuildCount: int64(rebuildCount),
|
||||
Details: fmt.Sprintf("completed %d index rebuilds", rebuildCount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *searchSupport) buildIndexes(ctx context.Context) (int, error) {
|
||||
totalBatchesIndexed := 0
|
||||
group := errgroup.Group{}
|
||||
@@ -651,23 +602,16 @@ func (s *searchSupport) runPeriodicScanForIndexesToRebuild(ctx context.Context)
|
||||
if err != nil {
|
||||
s.log.Error("failed to get import times", "error", err)
|
||||
}
|
||||
s.findIndexesToRebuild(importTimes, nil, time.Now())
|
||||
s.findIndexesToRebuild(importTimes, time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, filterKeys []NamespacedResource, now time.Time) []chan struct{} {
|
||||
func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, now time.Time) {
|
||||
// Check all open indexes and see if any of them need to be rebuilt.
|
||||
// This is done periodically to make sure that the indexes are up to date.
|
||||
|
||||
var keys []NamespacedResource
|
||||
if filterKeys != nil {
|
||||
keys = filterKeys
|
||||
} else {
|
||||
keys = s.search.GetOpenIndexes()
|
||||
}
|
||||
|
||||
var completeChs []chan struct{}
|
||||
keys := s.search.GetOpenIndexes()
|
||||
for _, key := range keys {
|
||||
idx := s.search.GetIndex(key)
|
||||
if idx == nil {
|
||||
@@ -694,17 +638,18 @@ func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResou
|
||||
}
|
||||
|
||||
if shouldRebuildIndex(bi, s.minBuildVersion, minBuildTime, lastImportTime, nil) {
|
||||
completeCh := make(chan struct{})
|
||||
completeChs = append(completeChs, completeCh)
|
||||
rebuildReq := newRebuildRequest(key, minBuildTime, lastImportTime, s.minBuildVersion, completeCh)
|
||||
s.rebuildQueue.Add(rebuildReq)
|
||||
s.rebuildQueue.Add(rebuildRequest{
|
||||
NamespacedResource: key,
|
||||
minBuildTime: minBuildTime,
|
||||
minBuildVersion: s.minBuildVersion,
|
||||
lastImportTime: lastImportTime,
|
||||
})
|
||||
|
||||
if s.indexMetrics != nil {
|
||||
s.indexMetrics.RebuildQueueLength.Set(float64(s.rebuildQueue.Len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return completeChs
|
||||
}
|
||||
|
||||
func (s *searchSupport) getLastImportTimes(ctx context.Context) (map[NamespacedResource]time.Time, error) {
|
||||
@@ -745,12 +690,6 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) {
|
||||
|
||||
l := s.log.New("namespace", req.Namespace, "group", req.Group, "resource", req.Resource)
|
||||
|
||||
defer func() {
|
||||
for _, ch := range req.completeChannels {
|
||||
close(ch)
|
||||
}
|
||||
}()
|
||||
|
||||
idx := s.search.GetIndex(req.NamespacedResource)
|
||||
if idx == nil {
|
||||
span.AddEvent("index not found")
|
||||
@@ -843,22 +782,6 @@ type rebuildRequest struct {
|
||||
minBuildTime time.Time // if not zero, rebuild index if it has been built before this timestamp
|
||||
lastImportTime time.Time // if not zero, rebuild index if it has been built before this timestamp.
|
||||
minBuildVersion *semver.Version // if not nil, rebuild index with build version older than this.
|
||||
|
||||
completeChannels []chan<- struct{} // signal rebuild index is complete
|
||||
}
|
||||
|
||||
func newRebuildRequest(key NamespacedResource, minBuildTime, lastImportTime time.Time, minBuildVersion *semver.Version, completeCh chan<- struct{}) rebuildRequest {
|
||||
var completeChannels []chan<- struct{} // setup a list as requests can be combined
|
||||
if completeCh != nil {
|
||||
completeChannels = []chan<- struct{}{completeCh}
|
||||
}
|
||||
return rebuildRequest{
|
||||
NamespacedResource: key,
|
||||
minBuildTime: minBuildTime,
|
||||
minBuildVersion: minBuildVersion,
|
||||
lastImportTime: lastImportTime,
|
||||
completeChannels: completeChannels,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats, key NamespacedResource, reason string) (ResourceIndex, error) {
|
||||
|
||||
@@ -211,11 +211,6 @@ func (s *searchWrapper) Search(ctx context.Context, in *resourcepb.ResourceSearc
|
||||
return client.Search(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (s *searchWrapper) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest,
|
||||
opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
return s.unifiedClient.RebuildIndexes(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// compareSearchResults compares legacy and unified search results and logs/metrics the outcome
|
||||
func (s *searchWrapper) compareSearchResults(legacyResponse, unifiedResponse *resourcepb.ResourceSearchResponse, requestKey *resourcepb.ResourceKey) {
|
||||
if legacyResponse == nil || unifiedResponse == nil {
|
||||
|
||||
@@ -112,11 +112,6 @@ func (m *MockResourceIndexClient) GetStats(ctx context.Context, in *resourcepb.R
|
||||
return args.Get(0).(*resourcepb.ResourceStatsResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockResourceIndexClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
args := m.Called(ctx, in, opts)
|
||||
return args.Get(0).(*resourcepb.RebuildIndexesResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func setupTestSearchClient(t *testing.T) (schema.GroupResource, *MockResourceIndexClient, *MockResourceIndexClient, featuremgmt.FeatureToggles) {
|
||||
t.Helper()
|
||||
gr := schema.GroupResource{Group: "test", Resource: "items"}
|
||||
|
||||
@@ -2,12 +2,8 @@ package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/dskit/ring"
|
||||
@@ -118,100 +114,6 @@ func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.Resourc
|
||||
return client.GetStats(ctx, r)
|
||||
}
|
||||
|
||||
func (ds *distributorServer) RebuildIndexes(ctx context.Context, r *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
ctx, span := ds.tracing.Start(ctx, "distributor.RebuildIndexes")
|
||||
defer span.End()
|
||||
|
||||
// validate input
|
||||
for _, key := range r.Keys {
|
||||
if r.Namespace != key.Namespace {
|
||||
return &resourcepb.RebuildIndexesResponse{
|
||||
Error: NewBadRequestError("key namespace does not match request namespace"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// distribute the request to all search pods to minimize risk of stale index
|
||||
// it will not rebuild on those which don't have the index open
|
||||
rs, err := ds.ring.GetAllHealthy(searchRingRead)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get all healthy instances from the ring")
|
||||
}
|
||||
|
||||
err = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", "all"))
|
||||
if err != nil {
|
||||
ds.log.Debug("error setting grpc header", "err", err)
|
||||
}
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
md = make(metadata.MD)
|
||||
}
|
||||
rCtx := userutils.InjectOrgID(metadata.NewOutgoingContext(ctx, md), r.Namespace)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var totalRebuildCount atomic.Int64
|
||||
detailsCh := make(chan string, len(rs.Instances))
|
||||
errorCh := make(chan error, len(rs.Instances))
|
||||
|
||||
for _, inst := range rs.Instances {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
client, err := ds.clientPool.GetClientForInstance(inst)
|
||||
if err != nil {
|
||||
errorCh <- fmt.Errorf("instance %s: failed to get client, %w", inst.Id, err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp, err := client.(*RingClient).Client.RebuildIndexes(rCtx, r)
|
||||
if err != nil {
|
||||
errorCh <- fmt.Errorf("instance %s: failed to distribute rebuild index request, %w", inst.Id, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rsp.Error != nil {
|
||||
errorCh <- fmt.Errorf("instance %s: rebuild index request returned the error %s", inst.Id, rsp.Error.Message)
|
||||
return
|
||||
}
|
||||
|
||||
if rsp.Details != "" {
|
||||
detailsCh <- fmt.Sprintf("{instance: %s, details: %s}", inst.Id, rsp.Details)
|
||||
}
|
||||
|
||||
totalRebuildCount.Add(rsp.RebuildCount)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorCh)
|
||||
close(detailsCh)
|
||||
|
||||
errs := make([]error, 0, len(errorCh))
|
||||
for err := range errorCh {
|
||||
ds.log.Error("rebuild indexes call failed with %w", err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
var details string
|
||||
for d := range detailsCh {
|
||||
if len(details) > 0 {
|
||||
details += ", "
|
||||
}
|
||||
details += d
|
||||
}
|
||||
|
||||
response := &resourcepb.RebuildIndexesResponse{
|
||||
RebuildCount: totalRebuildCount.Load(),
|
||||
Details: details,
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
response.Error = AsErrorResult(errors.Join(errs...))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
|
||||
ctx, span := ds.tracing.Start(ctx, "distributor.CountManagedObjects")
|
||||
defer span.End()
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -87,8 +85,7 @@ func (m *MockDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb
|
||||
|
||||
// mockStorageBackend implements StorageBackend for testing
|
||||
type mockStorageBackend struct {
|
||||
resourceStats []ResourceStats
|
||||
lastImportTimes []ResourceLastImportTime
|
||||
resourceStats []ResourceStats
|
||||
}
|
||||
|
||||
func (m *mockStorageBackend) GetResourceStats(ctx context.Context, nsr NamespacedResource, minCount int) ([]ResourceStats, error) {
|
||||
@@ -130,11 +127,7 @@ func (m *mockStorageBackend) ListModifiedSince(ctx context.Context, key Namespac
|
||||
|
||||
func (m *mockStorageBackend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[ResourceLastImportTime, error] {
|
||||
return func(yield func(ResourceLastImportTime, error) bool) {
|
||||
for _, ti := range m.lastImportTimes {
|
||||
if !yield(ti, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
yield(ResourceLastImportTime{}, errors.New("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,14 +605,14 @@ func TestFindIndexesForRebuild(t *testing.T) {
|
||||
{Namespace: "resource-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: lastImportTime,
|
||||
}
|
||||
|
||||
support.findIndexesToRebuild(importTimes, nil, now)
|
||||
support.findIndexesToRebuild(importTimes, now)
|
||||
require.Equal(t, 7, support.rebuildQueue.Len())
|
||||
|
||||
now5m := now.Add(5 * time.Minute)
|
||||
|
||||
// Running findIndexesToRebuild again should not add any new indexes to the rebuild queue, and all existing
|
||||
// ones should be "combined" with new ones (this will "bump" minBuildTime)
|
||||
support.findIndexesToRebuild(importTimes, nil, now5m)
|
||||
support.findIndexesToRebuild(importTimes, now5m)
|
||||
require.Equal(t, 7, support.rebuildQueue.Len())
|
||||
|
||||
// Values that we expect to find in rebuild requests.
|
||||
@@ -628,7 +621,7 @@ func TestFindIndexesForRebuild(t *testing.T) {
|
||||
minBuildTimeDashboard := now5m.Add(-1 * time.Hour)
|
||||
|
||||
vals := support.rebuildQueue.Elements()
|
||||
expected := []rebuildRequest{
|
||||
require.ElementsMatch(t, vals, []rebuildRequest{
|
||||
{NamespacedResource: NamespacedResource{Namespace: "resource-2h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
|
||||
{NamespacedResource: NamespacedResource{Namespace: "resource-10h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
|
||||
{NamespacedResource: NamespacedResource{Namespace: "resource-10h-v6", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
|
||||
@@ -638,10 +631,7 @@ func TestFindIndexesForRebuild(t *testing.T) {
|
||||
{NamespacedResource: NamespacedResource{Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard},
|
||||
|
||||
{NamespacedResource: NamespacedResource{Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard, lastImportTime: lastImportTime},
|
||||
}
|
||||
if diff := cmp.Diff(expected, vals, cmpopts.IgnoreFields(rebuildRequest{}, "completeChannels"), cmp.AllowUnexported(rebuildRequest{})); diff != "" {
|
||||
t.Errorf("rebuildQueue mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRebuildIndexes(t *testing.T) {
|
||||
@@ -758,81 +748,3 @@ func checkRebuildIndex(t *testing.T, support *searchSupport, req rebuildRequest,
|
||||
require.Nil(t, idxAfter, "index should not exist after rebuildIndex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebuildIndexesForResource(t *testing.T) {
|
||||
key := NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}
|
||||
|
||||
storage := &mockStorageBackend{
|
||||
resourceStats: []ResourceStats{
|
||||
{NamespacedResource: key, Count: 50, ResourceVersion: 11111111},
|
||||
},
|
||||
lastImportTimes: []ResourceLastImportTime{{
|
||||
NamespacedResource: key,
|
||||
LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
}},
|
||||
}
|
||||
|
||||
search := &mockSearchBackend{}
|
||||
supplier := &TestDocumentBuilderSupplier{
|
||||
GroupsResources: map[string]string{
|
||||
"group": "resource",
|
||||
},
|
||||
}
|
||||
|
||||
opts := SearchOptions{
|
||||
Backend: search,
|
||||
Resources: supplier,
|
||||
InitMinCount: 1,
|
||||
}
|
||||
|
||||
support, err := newSearchSupport(opts, storage, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, support)
|
||||
|
||||
err = support.init(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, support.rebuildQueue.Len())
|
||||
|
||||
// invalid request
|
||||
rebuildReq := &resourcepb.RebuildIndexesRequest{
|
||||
Namespace: "some-other-namespace",
|
||||
Keys: []*resourcepb.ResourceKey{{
|
||||
Namespace: key.Namespace,
|
||||
Group: key.Group,
|
||||
Resource: key.Resource,
|
||||
}}}
|
||||
rsp, err := support.RebuildIndexes(t.Context(), rebuildReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "key namespace does not match request namespace", rsp.Error.Message)
|
||||
|
||||
rebuildReq.Namespace = key.Namespace
|
||||
|
||||
// cached index info
|
||||
search.cache[key] = &MockResourceIndex{
|
||||
buildInfo: IndexBuildInfo{BuildVersion: semver.MustParse("5.0.0"), BuildTime: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)},
|
||||
}
|
||||
|
||||
// old import time will not be rebuilt
|
||||
storage.lastImportTimes = []ResourceLastImportTime{{
|
||||
NamespacedResource: key,
|
||||
LastImportTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
}}
|
||||
rsp, err = support.RebuildIndexes(t.Context(), rebuildReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), rsp.RebuildCount)
|
||||
require.Equal(t, 0, support.rebuildQueue.Len())
|
||||
|
||||
// recent import time gets added to rebuild queue and processed
|
||||
storage.lastImportTimes = []ResourceLastImportTime{{
|
||||
NamespacedResource: key,
|
||||
LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
}}
|
||||
|
||||
rsp, err = support.RebuildIndexes(t.Context(), rebuildReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), rsp.RebuildCount)
|
||||
|
||||
// rebuild waited for rebuild queue to process
|
||||
require.Equal(t, 0, support.rebuildQueue.Len())
|
||||
}
|
||||
|
||||
@@ -1541,11 +1541,3 @@ func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func(
|
||||
return queueCtx.Err() // Timed out or canceled while waiting for execution.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
if s.search == nil {
|
||||
return nil, fmt.Errorf("search index not configured")
|
||||
}
|
||||
|
||||
return s.search.RebuildIndexes(ctx, req)
|
||||
}
|
||||
|
||||
@@ -386,123 +386,6 @@ func (x *ResourceSearchResponse) GetFacet() map[string]*ResourceSearchResponse_F
|
||||
return nil
|
||||
}
|
||||
|
||||
type RebuildIndexesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Namespace (tenant)
|
||||
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
// List of ResourceKeys (Namespace + Group + Resource)
|
||||
Keys []*ResourceKey `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesRequest) Reset() {
|
||||
*x = RebuildIndexesRequest{}
|
||||
mi := &file_search_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RebuildIndexesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RebuildIndexesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[4]
|
||||
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 RebuildIndexesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RebuildIndexesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_search_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesRequest) GetKeys() []*ResourceKey {
|
||||
if x != nil {
|
||||
return x.Keys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RebuildIndexesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Total count of rebuilt indexes
|
||||
RebuildCount int64 `protobuf:"varint,1,opt,name=rebuildCount,proto3" json:"rebuildCount,omitempty"`
|
||||
// Result message
|
||||
Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"`
|
||||
// Error details
|
||||
Error *ErrorResult `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesResponse) Reset() {
|
||||
*x = RebuildIndexesResponse{}
|
||||
mi := &file_search_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RebuildIndexesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RebuildIndexesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[5]
|
||||
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 RebuildIndexesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RebuildIndexesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_search_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesResponse) GetRebuildCount() int64 {
|
||||
if x != nil {
|
||||
return x.RebuildCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesResponse) GetDetails() string {
|
||||
if x != nil {
|
||||
return x.Details
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RebuildIndexesResponse) GetError() *ErrorResult {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResourceStatsResponse_Stats struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Resource group
|
||||
@@ -517,7 +400,7 @@ type ResourceStatsResponse_Stats struct {
|
||||
|
||||
func (x *ResourceStatsResponse_Stats) Reset() {
|
||||
*x = ResourceStatsResponse_Stats{}
|
||||
mi := &file_search_proto_msgTypes[6]
|
||||
mi := &file_search_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -529,7 +412,7 @@ func (x *ResourceStatsResponse_Stats) String() string {
|
||||
func (*ResourceStatsResponse_Stats) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[6]
|
||||
mi := &file_search_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -576,7 +459,7 @@ type ResourceSearchRequest_Sort struct {
|
||||
|
||||
func (x *ResourceSearchRequest_Sort) Reset() {
|
||||
*x = ResourceSearchRequest_Sort{}
|
||||
mi := &file_search_proto_msgTypes[7]
|
||||
mi := &file_search_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -588,7 +471,7 @@ func (x *ResourceSearchRequest_Sort) String() string {
|
||||
func (*ResourceSearchRequest_Sort) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[7]
|
||||
mi := &file_search_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -628,7 +511,7 @@ type ResourceSearchRequest_Facet struct {
|
||||
|
||||
func (x *ResourceSearchRequest_Facet) Reset() {
|
||||
*x = ResourceSearchRequest_Facet{}
|
||||
mi := &file_search_proto_msgTypes[8]
|
||||
mi := &file_search_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -640,7 +523,7 @@ func (x *ResourceSearchRequest_Facet) String() string {
|
||||
func (*ResourceSearchRequest_Facet) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[8]
|
||||
mi := &file_search_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -685,7 +568,7 @@ type ResourceSearchResponse_Facet struct {
|
||||
|
||||
func (x *ResourceSearchResponse_Facet) Reset() {
|
||||
*x = ResourceSearchResponse_Facet{}
|
||||
mi := &file_search_proto_msgTypes[10]
|
||||
mi := &file_search_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -697,7 +580,7 @@ func (x *ResourceSearchResponse_Facet) String() string {
|
||||
func (*ResourceSearchResponse_Facet) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[10]
|
||||
mi := &file_search_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -751,7 +634,7 @@ type ResourceSearchResponse_TermFacet struct {
|
||||
|
||||
func (x *ResourceSearchResponse_TermFacet) Reset() {
|
||||
*x = ResourceSearchResponse_TermFacet{}
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
mi := &file_search_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -763,7 +646,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string {
|
||||
func (*ResourceSearchResponse_TermFacet) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
mi := &file_search_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -897,42 +780,22 @@ var file_search_proto_rawDesc = string([]byte{
|
||||
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46,
|
||||
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22,
|
||||
0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65,
|
||||
0x73, 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, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02,
|
||||
0x20, 0x03, 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, 0x04, 0x6b, 0x65, 0x79,
|
||||
0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64,
|
||||
0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c,
|
||||
0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72,
|
||||
0x72, 0x6f, 0x72, 0x18, 0x03, 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, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e,
|
||||
0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
|
||||
0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73,
|
||||
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,
|
||||
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
|
||||
0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65,
|
||||
0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b,
|
||||
0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74,
|
||||
0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74,
|
||||
0x61, 0x74, 0x73, 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 (
|
||||
@@ -947,53 +810,47 @@ func file_search_proto_rawDescGZIP() []byte {
|
||||
return file_search_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_search_proto_goTypes = []any{
|
||||
(*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest
|
||||
(*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse
|
||||
(*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest
|
||||
(*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse
|
||||
(*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest
|
||||
(*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse
|
||||
(*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats
|
||||
(*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort
|
||||
(*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet
|
||||
nil, // 9: resource.ResourceSearchRequest.FacetEntry
|
||||
(*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet
|
||||
(*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet
|
||||
nil, // 12: resource.ResourceSearchResponse.FacetEntry
|
||||
(*ErrorResult)(nil), // 13: resource.ErrorResult
|
||||
(*ListOptions)(nil), // 14: resource.ListOptions
|
||||
(*ResourceKey)(nil), // 15: resource.ResourceKey
|
||||
(*ResourceTable)(nil), // 16: resource.ResourceTable
|
||||
(*ResourceStatsResponse_Stats)(nil), // 4: resource.ResourceStatsResponse.Stats
|
||||
(*ResourceSearchRequest_Sort)(nil), // 5: resource.ResourceSearchRequest.Sort
|
||||
(*ResourceSearchRequest_Facet)(nil), // 6: resource.ResourceSearchRequest.Facet
|
||||
nil, // 7: resource.ResourceSearchRequest.FacetEntry
|
||||
(*ResourceSearchResponse_Facet)(nil), // 8: resource.ResourceSearchResponse.Facet
|
||||
(*ResourceSearchResponse_TermFacet)(nil), // 9: resource.ResourceSearchResponse.TermFacet
|
||||
nil, // 10: resource.ResourceSearchResponse.FacetEntry
|
||||
(*ErrorResult)(nil), // 11: resource.ErrorResult
|
||||
(*ListOptions)(nil), // 12: resource.ListOptions
|
||||
(*ResourceKey)(nil), // 13: resource.ResourceKey
|
||||
(*ResourceTable)(nil), // 14: resource.ResourceTable
|
||||
}
|
||||
var file_search_proto_depIdxs = []int32{
|
||||
13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
||||
6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
||||
14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
||||
15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
||||
7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
||||
9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
||||
13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
||||
15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
||||
16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
||||
12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
||||
15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
|
||||
13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
|
||||
8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
||||
11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
||||
10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
||||
2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
||||
0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
||||
4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
|
||||
3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
||||
1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
||||
5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
|
||||
18, // [18:21] is the sub-list for method output_type
|
||||
15, // [15:18] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
11, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
||||
4, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
||||
12, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
||||
13, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
||||
5, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
||||
7, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
||||
11, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
||||
13, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
||||
14, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
||||
10, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
||||
6, // 10: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
||||
9, // 11: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
||||
8, // 12: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
||||
2, // 13: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
||||
0, // 14: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
||||
3, // 15: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
||||
1, // 16: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
||||
15, // [15:17] is the sub-list for method output_type
|
||||
13, // [13:15] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_search_proto_init() }
|
||||
@@ -1008,7 +865,7 @@ func file_search_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 13,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -19,9 +19,8 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion8
|
||||
|
||||
const (
|
||||
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
|
||||
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
|
||||
ResourceIndex_RebuildIndexes_FullMethodName = "/resource.ResourceIndex/RebuildIndexes"
|
||||
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
|
||||
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
|
||||
)
|
||||
|
||||
// ResourceIndexClient is the client API for ResourceIndex service.
|
||||
@@ -34,7 +33,6 @@ type ResourceIndexClient interface {
|
||||
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
|
||||
RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error)
|
||||
}
|
||||
|
||||
type resourceIndexClient struct {
|
||||
@@ -65,16 +63,6 @@ func (c *resourceIndexClient) GetStats(ctx context.Context, in *ResourceStatsReq
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RebuildIndexesResponse)
|
||||
err := c.cc.Invoke(ctx, ResourceIndex_RebuildIndexes_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ResourceIndexServer is the server API for ResourceIndex service.
|
||||
// All implementations should embed UnimplementedResourceIndexServer
|
||||
// for forward compatibility
|
||||
@@ -85,7 +73,6 @@ type ResourceIndexServer interface {
|
||||
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
|
||||
RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations.
|
||||
@@ -98,9 +85,6 @@ func (UnimplementedResourceIndexServer) Search(context.Context, *ResourceSearchR
|
||||
func (UnimplementedResourceIndexServer) GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
|
||||
}
|
||||
func (UnimplementedResourceIndexServer) RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RebuildIndexes not implemented")
|
||||
}
|
||||
|
||||
// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ResourceIndexServer will
|
||||
@@ -149,24 +133,6 @@ func _ResourceIndex_GetStats_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ResourceIndex_RebuildIndexes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RebuildIndexesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ResourceIndexServer).RebuildIndexes(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ResourceIndex_RebuildIndexes_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ResourceIndexServer).RebuildIndexes(ctx, req.(*RebuildIndexesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -182,10 +148,6 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetStats",
|
||||
Handler: _ResourceIndex_GetStats_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RebuildIndexes",
|
||||
Handler: _ResourceIndex_RebuildIndexes_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "search.proto",
|
||||
|
||||
@@ -994,9 +994,7 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso
|
||||
b.lastImportTimeDeletionTime.Store(now)
|
||||
}
|
||||
|
||||
rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{
|
||||
SQLTemplate: sqltemplate.New(b.dialect),
|
||||
})
|
||||
rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{SQLTemplate: sqltemplate.New(b.dialect)})
|
||||
if err != nil {
|
||||
return func(yield func(resource.ResourceLastImportTime, error) bool) {
|
||||
yield(resource.ResourceLastImportTime{}, err)
|
||||
@@ -1039,7 +1037,3 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
|
||||
return nil, fmt.Errorf("rebuild indexes not supported by unistore sql backend")
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
SceneQueryRunner,
|
||||
sceneGraph,
|
||||
sceneUtils,
|
||||
VizPanel,
|
||||
isSceneObject,
|
||||
@@ -22,7 +21,6 @@ import { Panel } from '@grafana/schema';
|
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
||||
import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions';
|
||||
|
||||
import { DashboardEditActionEvent } from '../edit-pane/shared';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
@@ -31,7 +29,6 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||
import {
|
||||
activateSceneObjectAndParentTree,
|
||||
getDashboardSceneFor,
|
||||
@@ -87,11 +84,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
|
||||
if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
|
||||
if (config.featureToggles.newVizSuggestions) {
|
||||
this._autoSelectVisualization(panel);
|
||||
} else {
|
||||
panel.changePluginType('timeseries');
|
||||
}
|
||||
panel.changePluginType('timeseries');
|
||||
}
|
||||
|
||||
this._subs.add(
|
||||
@@ -124,31 +117,6 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
};
|
||||
}
|
||||
|
||||
private async _autoSelectVisualization(panel: VizPanel) {
|
||||
const dataObject = sceneGraph.getData(panel);
|
||||
|
||||
this._subs.add(
|
||||
dataObject.subscribeToState(async () => {
|
||||
const { data } = dataObject.state;
|
||||
const hasData = data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0);
|
||||
|
||||
if (hasData && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
|
||||
const panelModel = new PanelModelCompatibilityWrapper(panel);
|
||||
const suggestions = await getAllSuggestions(data, panelModel);
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
const defaultFirstSuggestion = suggestions[0];
|
||||
await panel.changePluginType(
|
||||
defaultFirstSuggestion.pluginId,
|
||||
defaultFirstSuggestion.options,
|
||||
defaultFirstSuggestion.fieldConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private commitChanges() {
|
||||
if (!this.state.isDirty && !this._changesHaveBeenMade) {
|
||||
// Nothing to commit
|
||||
@@ -260,17 +228,12 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
})
|
||||
);
|
||||
|
||||
const isUnconfigured = Boolean(
|
||||
config.featureToggles.newVizSuggestions && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID
|
||||
);
|
||||
|
||||
// Setup options pane
|
||||
this.setState({
|
||||
optionsPane: new PanelOptionsPane({
|
||||
panelRef: this.state.panelRef,
|
||||
searchQuery: '',
|
||||
listMode: OptionFilter.All,
|
||||
isVizPickerOpen: isUnconfigured,
|
||||
}),
|
||||
isInitializing: false,
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { CoreApp, GrafanaTheme2, PanelPlugin, PanelProps } from '@grafana/data';
|
||||
import { PanelPlugin, PanelProps } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { sceneUtils } from '@grafana/scenes';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Icon, Menu, Stack, Text, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Menu, Stack } from '@grafana/ui';
|
||||
|
||||
import { NEW_PANEL_TITLE } from '../../dashboard/utils/dashboard';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
@@ -18,8 +16,6 @@ const UnconfiguredPanel = new PanelPlugin(UnconfiguredPanelComp);
|
||||
|
||||
function UnconfiguredPanelComp(props: PanelProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const panelContext = usePanelContext();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onMenuClick = useCallback((isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
@@ -30,15 +26,14 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
||||
DashboardInteractions.panelActionClicked('configure', props.id, 'panel');
|
||||
};
|
||||
|
||||
const dashboard = window.__grafanaSceneContext;
|
||||
const panel =
|
||||
dashboard instanceof DashboardScene ? findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id)) : null;
|
||||
|
||||
const onUseLibraryPanel = () => {
|
||||
if (!dashboard || !(dashboard instanceof DashboardScene)) {
|
||||
const dashboard = window.__grafanaSceneContext;
|
||||
|
||||
if (!(dashboard instanceof DashboardScene)) {
|
||||
throw new Error('DashboardScene not found');
|
||||
}
|
||||
|
||||
const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id));
|
||||
if (!panel) {
|
||||
throw new Error('Panel not found');
|
||||
}
|
||||
@@ -46,18 +41,6 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
||||
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!panel || !config.featureToggles.newVizSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panelContext.app === CoreApp.PanelEditor) {
|
||||
panel.setState({ title: '' });
|
||||
} else if (!panel.state.title) {
|
||||
panel.setState({ title: NEW_PANEL_TITLE });
|
||||
}
|
||||
}, [panel, panelContext.app]);
|
||||
|
||||
const MenuActions = () => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
@@ -73,25 +56,6 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const showEmptyState = config.featureToggles.newVizSuggestions && panelContext.app === CoreApp.PanelEditor;
|
||||
|
||||
if (showEmptyState) {
|
||||
const defaultContent = (
|
||||
<Trans i18nKey="dashboard.new-panel.empty-state-message">
|
||||
Run a query to visualize it here or go to all visualizations to add other panel types
|
||||
</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.emptyStateWrapper}>
|
||||
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
{defaultContent}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
|
||||
<Box paddingBottom={2}>
|
||||
@@ -115,20 +79,3 @@ sceneUtils.registerRuntimePanelPlugin({
|
||||
pluginId: UNCONFIGURED_PANEL_PLUGIN_ID,
|
||||
plugin: UnconfiguredPanel,
|
||||
});
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
emptyStateWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
emptyStateIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Props {
|
||||
dashboardJson: AsyncState<{
|
||||
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
||||
hasLibraryPanels?: boolean;
|
||||
initialSaveModelVersion: 'v1' | 'v2';
|
||||
}>;
|
||||
isSharingExternally: boolean;
|
||||
exportMode: ExportMode;
|
||||
@@ -40,27 +41,49 @@ export function ResourceExport({
|
||||
onViewYAML,
|
||||
}: Props) {
|
||||
const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels;
|
||||
const initialSaveModelVersion = dashboardJson.value?.initialSaveModelVersion;
|
||||
const isV2Dashboard =
|
||||
dashboardJson.value?.json && 'spec' in dashboardJson.value.json && 'elements' in dashboardJson.value.json.spec;
|
||||
const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels;
|
||||
|
||||
const switchExportLabel = isV2Dashboard
|
||||
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
|
||||
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
|
||||
const switchExportLabel =
|
||||
exportMode === ExportMode.V2Resource
|
||||
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
|
||||
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
|
||||
const switchExportModeLabel = t('export.json.export-mode', 'Model');
|
||||
const switchExportFormatLabel = t('export.json.export-format', 'Format');
|
||||
|
||||
return (
|
||||
<Stack gap={2} direction="column">
|
||||
<Stack gap={1} direction="column">
|
||||
{!isV2Dashboard && (
|
||||
{initialSaveModelVersion === 'v1' && (
|
||||
<Stack alignItems="center">
|
||||
<Label>{switchExportModeLabel}</Label>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
||||
value: ExportMode.V1Resource,
|
||||
},
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
|
||||
value: ExportMode.V2Resource,
|
||||
},
|
||||
]}
|
||||
value={exportMode}
|
||||
onChange={(value) => onExportModeChange(value)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{initialSaveModelVersion === 'v2' && (
|
||||
<Stack alignItems="center">
|
||||
<Label>{switchExportModeLabel}</Label>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.classic', 'Classic'),
|
||||
value: ExportMode.Classic,
|
||||
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
|
||||
value: ExportMode.V2Resource,
|
||||
},
|
||||
{
|
||||
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
|
||||
@@ -85,7 +108,9 @@ export function ResourceExport({
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{exportMode !== ExportMode.V1Resource && (
|
||||
{(isV2Dashboard ||
|
||||
exportMode === ExportMode.Classic ||
|
||||
(initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && (
|
||||
<Stack gap={1} alignItems="start">
|
||||
<Label>{switchExportLabel}</Label>
|
||||
<Switch
|
||||
|
||||
@@ -113,6 +113,9 @@ describe('ShareExportTab', () => {
|
||||
// Should call transformSceneToV1 (not transform V2→V1)
|
||||
expect(transformSceneToV1Spy).toHaveBeenCalled();
|
||||
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
|
||||
|
||||
// Should report correct initial version
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
|
||||
// If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion
|
||||
@@ -133,6 +136,9 @@ describe('ShareExportTab', () => {
|
||||
// Should auto-transform V2→V1
|
||||
expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first
|
||||
expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1
|
||||
|
||||
// Should report correct initial version
|
||||
expect(result.initialSaveModelVersion).toBe('v2');
|
||||
});
|
||||
|
||||
// If V2 dashboard → V1 Resource with external sharing should transform and apply external sharing
|
||||
@@ -158,6 +164,9 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should call makeExportableV1 for external sharing
|
||||
expect(makeExportableV1Spy).toHaveBeenCalled();
|
||||
|
||||
// Should report correct initial version
|
||||
expect(result.initialSaveModelVersion).toBe('v2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,6 +187,9 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should not call V2→V1 transformation since source is already V2
|
||||
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
|
||||
|
||||
// Should report correct initial version
|
||||
expect(result.initialSaveModelVersion).toBe('v2');
|
||||
});
|
||||
|
||||
// If V1 dashboard → V2 Resource should detect library panels correctly
|
||||
@@ -189,6 +201,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should detect library panels from V1 dashboard
|
||||
expect(result.hasLibraryPanels).toBe(true);
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
|
||||
// If V1 dashboard with dashboardNewLayouts disabled → V2 Resource should detect library panels correctly
|
||||
@@ -200,6 +213,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should detect library panels from V1 dashboard (first branch of the logic)
|
||||
expect(result.hasLibraryPanels).toBe(true);
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
|
||||
// If V1 dashboard without library panels → V2 Resource should return false
|
||||
@@ -211,6 +225,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should not detect library panels
|
||||
expect(result.hasLibraryPanels).toBe(false);
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -232,6 +247,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should detect library panels from V2 dashboard elements (second branch of the logic)
|
||||
expect(result.hasLibraryPanels).toBe(true);
|
||||
expect(result.initialSaveModelVersion).toBe('v2');
|
||||
});
|
||||
|
||||
// Test the second branch: V2 dashboard with V1 initial save model
|
||||
@@ -243,6 +259,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should detect library panels from V2 dashboard elements (second branch of the logic)
|
||||
expect(result.hasLibraryPanels).toBe(true);
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
|
||||
// If V2 dashboard without library panels → V2 Resource should return false
|
||||
@@ -254,6 +271,7 @@ describe('ShareExportTab', () => {
|
||||
|
||||
// Should not detect library panels
|
||||
expect(result.hasLibraryPanels).toBe(false);
|
||||
expect(result.initialSaveModelVersion).toBe('v2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,6 +294,9 @@ describe('ShareExportTab', () => {
|
||||
expect(result.json).not.toHaveProperty('apiVersion');
|
||||
expect(result.json).not.toHaveProperty('kind');
|
||||
expect(result.json).not.toHaveProperty('status');
|
||||
|
||||
// Should report correct initial version
|
||||
expect(result.initialSaveModelVersion).toBe('v1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -102,16 +102,28 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
public getExportableDashboardJson = async (): Promise<{
|
||||
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
|
||||
hasLibraryPanels?: boolean;
|
||||
initialSaveModelVersion: 'v1' | 'v2';
|
||||
}> => {
|
||||
const { isSharingExternally, exportMode } = this.state;
|
||||
|
||||
const scene = getDashboardSceneFor(this);
|
||||
const exportableDashboard = await scene.serializer.makeExportableExternally(scene);
|
||||
const initialSaveModel = scene.getInitialSaveModel();
|
||||
const initialSaveModelVersion = initialSaveModel && isDashboardV2Spec(initialSaveModel) ? 'v2' : 'v1';
|
||||
const origDashboard = scene.serializer.getSaveModel(scene);
|
||||
const exportable = isSharingExternally ? exportableDashboard : origDashboard;
|
||||
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
||||
|
||||
if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && exportMode !== ExportMode.V1Resource) {
|
||||
if (
|
||||
isDashboardV2Spec(origDashboard) &&
|
||||
'elements' in exportable &&
|
||||
initialSaveModelVersion === 'v2' &&
|
||||
exportMode !== ExportMode.V1Resource
|
||||
) {
|
||||
this.setState({
|
||||
exportMode: ExportMode.V2Resource,
|
||||
});
|
||||
|
||||
// For automatic V2 path, also process library panels when sharing externally
|
||||
let finalSpec = exportable;
|
||||
if (isSharingExternally && isDashboardV2Spec(exportable)) {
|
||||
@@ -120,6 +132,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
if ('error' in result) {
|
||||
return {
|
||||
json: { error: result.error },
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
||||
};
|
||||
}
|
||||
@@ -134,13 +147,14 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
spec: finalSpec,
|
||||
status: {},
|
||||
},
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
|
||||
};
|
||||
}
|
||||
|
||||
if (exportMode === ExportMode.V1Resource) {
|
||||
// Check if source is V2 and auto-transform to V1
|
||||
if (isDashboardV2Spec(origDashboard)) {
|
||||
if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') {
|
||||
try {
|
||||
const spec = transformSceneToSaveModelSchemaV2(scene);
|
||||
const metadata = getMetadata(scene, Boolean(isSharingExternally));
|
||||
@@ -171,6 +185,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
spec: exportableV1,
|
||||
status: {},
|
||||
},
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1),
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -178,6 +193,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
json: {
|
||||
error: `Failed to convert dashboard to v1. ${err}`,
|
||||
},
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels: undefined,
|
||||
};
|
||||
}
|
||||
@@ -193,6 +209,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
spec,
|
||||
status: {},
|
||||
},
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec),
|
||||
};
|
||||
}
|
||||
@@ -206,7 +223,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
// Check if dashboard contains library panels based on dashboard version
|
||||
let hasLibraryPanels = false;
|
||||
// Case: V1 dashboard loaded (with kubernetesDashboards enabled and dashboardNewLayouts disabled), and user explicitly selected V2Resource export mode
|
||||
if (!isDashboardV2Spec(origDashboard)) {
|
||||
if (initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)) {
|
||||
hasLibraryPanels = hasLibraryPanelsInV1Dashboard(origDashboard);
|
||||
} else if (isDashboardV2Spec(origDashboard)) {
|
||||
// Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource
|
||||
@@ -222,10 +239,35 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
spec: exportableV2,
|
||||
status: {},
|
||||
},
|
||||
initialSaveModelVersion,
|
||||
hasLibraryPanels,
|
||||
};
|
||||
}
|
||||
|
||||
// Classic mode
|
||||
// This handles a case when:
|
||||
// 1. dashboardNewLayouts feature toggle is enabled
|
||||
// 2. v1 dashboard is loaded
|
||||
// 3. dashboard hasn't been edited yet - if it was edited, user would be forced to save it in v2 version
|
||||
if (
|
||||
initialSaveModelVersion === 'v1' &&
|
||||
isDashboardV2Spec(origDashboard) &&
|
||||
initialSaveModel &&
|
||||
'panels' in initialSaveModel
|
||||
) {
|
||||
const oldModel = new DashboardModel(initialSaveModel, undefined, {
|
||||
getVariablesFromState: () => {
|
||||
return getVariablesCompatibility(window.__grafanaSceneContext);
|
||||
},
|
||||
});
|
||||
const exportableV1 = isSharingExternally ? await makeExportableV1(oldModel) : initialSaveModel;
|
||||
return {
|
||||
json: exportableV1,
|
||||
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(initialSaveModel),
|
||||
initialSaveModelVersion,
|
||||
};
|
||||
}
|
||||
|
||||
// legacy mode or classic mode when dashboardNewLayouts is disabled
|
||||
// At this point we know that dashboard should be V1 or could have produced an error
|
||||
return {
|
||||
@@ -234,6 +276,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
'error' in exportable || !isV1ClassicDashboard(origDashboard)
|
||||
? false
|
||||
: hasLibraryPanelsInV1Dashboard(origDashboard),
|
||||
initialSaveModelVersion,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -254,11 +297,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
const extension = isViewingYAML ? 'yaml' : 'json';
|
||||
saveAs(blob, `${title}-${time}.${extension}`);
|
||||
|
||||
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
|
||||
|
||||
DashboardInteractions.exportDownloadJsonClicked({
|
||||
externally: isSharingExternally,
|
||||
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
|
||||
dashboard_schema_version: dashboard.initialSaveModelVersion,
|
||||
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
||||
format: isViewingYAML ? 'yaml' : 'json',
|
||||
action: 'download',
|
||||
@@ -269,11 +310,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
|
||||
const dashboard = await this.getExportableDashboardJson();
|
||||
const { isSharingExternally, isViewingYAML, exportMode } = this.state;
|
||||
|
||||
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
|
||||
|
||||
DashboardInteractions.exportCopyJsonClicked({
|
||||
externally: isSharingExternally,
|
||||
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
|
||||
dashboard_schema_version: dashboard.initialSaveModelVersion,
|
||||
has_library_panels: Boolean(dashboard.hasLibraryPanels),
|
||||
export_mode: exportMode || 'classic',
|
||||
format: isViewingYAML ? 'yaml' : 'json',
|
||||
@@ -363,7 +402,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
|
||||
/>
|
||||
) : (
|
||||
<Stack gap={2} direction="column">
|
||||
<Field noMargin label={exportExternallyTranslation}>
|
||||
<Field label={exportExternallyTranslation}>
|
||||
<Switch
|
||||
id="share-externally-toggle"
|
||||
value={isSharingExternally}
|
||||
|
||||
@@ -96,7 +96,6 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||
if (!key) {
|
||||
return null;
|
||||
@@ -259,15 +258,8 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null {
|
||||
}
|
||||
|
||||
export function getDefaultVizPanel(): VizPanel {
|
||||
const defaultPluginId =
|
||||
config.featureToggles.dashboardNewLayouts || config.featureToggles.newVizSuggestions
|
||||
? UNCONFIGURED_PANEL_PLUGIN_ID
|
||||
: 'timeseries';
|
||||
|
||||
const newPanelTitle =
|
||||
config.featureToggles.newVizSuggestions && defaultPluginId === UNCONFIGURED_PANEL_PLUGIN_ID
|
||||
? ''
|
||||
: t('dashboard.new-panel-title', 'New panel');
|
||||
const defaultPluginId = config.featureToggles.dashboardNewLayouts ? UNCONFIGURED_PANEL_PLUGIN_ID : 'timeseries';
|
||||
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
|
||||
|
||||
return new VizPanel({
|
||||
title: newPanelTitle,
|
||||
|
||||
@@ -128,7 +128,6 @@ export class LogListModel implements LogRowModel {
|
||||
clone._wrapLogMessage = true;
|
||||
clone._body = undefined;
|
||||
clone._highlightTokens = undefined;
|
||||
clone.collapsed = false;
|
||||
return clone;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { DataSourceInstanceSettings, dateTimeFormat, locationUtil, TypedVariableModel } from '@grafana/data';
|
||||
import { dateTimeFormat } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { locationService, reportInteraction, config } from '@grafana/runtime';
|
||||
import { Panel } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen';
|
||||
import { AnnotationQuery, Dashboard } from '@grafana/schema/dist/esm/veneer/dashboard.types';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Box, Legend, TextLink } from '@grafana/ui';
|
||||
import { Form } from 'app/core/components/Form/Form';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { addLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { clearLoadedDashboard, importDashboard } from '../state/actions';
|
||||
import { DashboardSource, DataSourceInput, ImportDashboardDTO, LibraryPanelInputState } from '../state/reducers';
|
||||
import { DashboardSource, ImportDashboardDTO } from '../state/reducers';
|
||||
|
||||
import { ImportDashboardForm } from './ImportDashboardForm';
|
||||
|
||||
@@ -51,80 +45,9 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
||||
uidReset: false,
|
||||
};
|
||||
|
||||
onSubmit = async (form: ImportDashboardDTO) => {
|
||||
onSubmit = (form: ImportDashboardDTO) => {
|
||||
reportInteraction(IMPORT_FINISHED_EVENT_NAME);
|
||||
|
||||
const { dashboard, inputs, folder } = this.props;
|
||||
|
||||
// when kubernetesDashboard are enabled, we bypass api/dashboard/import
|
||||
// and hit the k8s dashboard API directly
|
||||
if (config.featureToggles.kubernetesDashboards) {
|
||||
// 1. process datasources so the template placeholder is replaced with the actual value user selected
|
||||
|
||||
const annotations = dashboard.annotations.list.map((annotation: AnnotationQuery) => {
|
||||
return processAnnotation(annotation, inputs, form);
|
||||
});
|
||||
|
||||
const panels = dashboard.panels.map((panel: Panel) => {
|
||||
return processPanel(panel, inputs, form);
|
||||
});
|
||||
|
||||
const variables = dashboard.templating.list.map((variable: TypedVariableModel) => {
|
||||
return processVariable(variable, inputs, form);
|
||||
});
|
||||
|
||||
const dashboardWithDataSources: Dashboard = {
|
||||
...dashboard,
|
||||
title: form.title,
|
||||
annotations,
|
||||
panels,
|
||||
templating: {
|
||||
list: variables,
|
||||
},
|
||||
uid: form.uid,
|
||||
};
|
||||
|
||||
const newLibraryPanels = inputs.libraryPanels.filter((lp) => lp.state === LibraryPanelInputState.New);
|
||||
|
||||
// for library panels that don't exist in the instance, we create them by hitting the library panel API
|
||||
for (const lp of newLibraryPanels) {
|
||||
const libPanelWithPanelModel = new PanelModel(lp.model.model);
|
||||
let { scopedVars, ...panelSaveModel } = libPanelWithPanelModel.getSaveModel();
|
||||
panelSaveModel = {
|
||||
libraryPanel: {
|
||||
name: lp.model.name,
|
||||
uid: lp.model.uid,
|
||||
},
|
||||
...panelSaveModel,
|
||||
};
|
||||
|
||||
try {
|
||||
await addLibraryPanel(panelSaveModel, folder.uid);
|
||||
} catch (error) {
|
||||
console.error('Error adding library panel during dashboard import', error);
|
||||
}
|
||||
}
|
||||
|
||||
const dashboardK8SPayload: SaveDashboardCommand<Dashboard> = {
|
||||
dashboard: dashboardWithDataSources,
|
||||
k8s: {
|
||||
annotations: {
|
||||
'grafana.app/folder': form.folder.uid,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// hit v1 API directly
|
||||
const result = await getDashboardAPI('v1').saveDashboard(dashboardK8SPayload);
|
||||
|
||||
if (result.url) {
|
||||
const dashboardUrl = locationUtil.stripBaseFromUrl(result.url);
|
||||
locationService.push(dashboardUrl);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.importDashboard(form);
|
||||
};
|
||||
|
||||
@@ -203,116 +126,3 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
||||
|
||||
export const ImportDashboardOverview = connector(ImportDashboardOverviewUnConnected);
|
||||
ImportDashboardOverview.displayName = 'ImportDashboardOverview';
|
||||
|
||||
function hasUid(query: Record<string, unknown> | {}): query is { uid: string } {
|
||||
return 'uid' in query && typeof query['uid'] === 'string';
|
||||
}
|
||||
|
||||
/*
|
||||
Checks whether the templateized uid matches the user prodvided datasource input
|
||||
*/
|
||||
function checkUserInputMatch(
|
||||
templateizedUid: string,
|
||||
datasourceInputs: DataSourceInput[],
|
||||
userDsInputs: DataSourceInstanceSettings[]
|
||||
) {
|
||||
const dsName = templateizedUid.replace(/\$\{(.*)\}/, '$1');
|
||||
const input = datasourceInputs?.find((ds) => ds.name === dsName);
|
||||
const userInput = input && userDsInputs.find((ds) => ds.type === input.pluginId);
|
||||
return userInput;
|
||||
}
|
||||
|
||||
function processAnnotation(
|
||||
annotation: AnnotationQuery,
|
||||
inputs: { dataSources: DataSourceInput[] },
|
||||
form: ImportDashboardDTO
|
||||
): AnnotationQuery {
|
||||
if (annotation.datasource && annotation.datasource.uid && annotation.datasource.uid.startsWith('$')) {
|
||||
const userInput = checkUserInputMatch(annotation.datasource.uid, inputs.dataSources, form.dataSources);
|
||||
if (userInput) {
|
||||
return {
|
||||
...annotation,
|
||||
datasource: {
|
||||
...annotation.datasource,
|
||||
uid: userInput.uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
function processPanel(panel: Panel, inputs: { dataSources: DataSourceInput[] }, form: ImportDashboardDTO): Panel {
|
||||
if (panel.datasource && panel.datasource.uid && panel.datasource.uid.startsWith('$')) {
|
||||
const userInput = checkUserInputMatch(panel.datasource.uid, inputs.dataSources, form.dataSources);
|
||||
|
||||
const queries = panel.targets?.map((target) => {
|
||||
if (target.datasource && hasUid(target.datasource) && target.datasource.uid.startsWith('$')) {
|
||||
const userInput = checkUserInputMatch(target.datasource.uid, inputs.dataSources, form.dataSources);
|
||||
if (userInput) {
|
||||
return {
|
||||
...target,
|
||||
datasource: {
|
||||
...target.datasource,
|
||||
uid: userInput.uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return target;
|
||||
});
|
||||
|
||||
if (userInput) {
|
||||
return {
|
||||
...panel,
|
||||
targets: queries,
|
||||
datasource: {
|
||||
...panel.datasource,
|
||||
uid: userInput.uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
function processVariable(
|
||||
variable: TypedVariableModel,
|
||||
inputs: { dataSources: DataSourceInput[] },
|
||||
form: ImportDashboardDTO
|
||||
): TypedVariableModel {
|
||||
if (variable.type === 'query') {
|
||||
if (variable.datasource && variable.datasource.uid?.startsWith('$')) {
|
||||
const userInput = checkUserInputMatch(variable.datasource.uid, inputs.dataSources, form.dataSources);
|
||||
if (userInput) {
|
||||
return {
|
||||
...variable,
|
||||
datasource: {
|
||||
...variable.datasource,
|
||||
uid: userInput.uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (variable.type === 'datasource') {
|
||||
if (variable.current && variable.current.value && String(variable.current.value).startsWith('$')) {
|
||||
const userInput = checkUserInputMatch(String(variable.current.value), inputs.dataSources, form.dataSources);
|
||||
if (userInput) {
|
||||
return {
|
||||
...variable,
|
||||
current: {
|
||||
selected: variable.current.selected,
|
||||
text: userInput.name,
|
||||
value: userInput.uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Icon, Text, useStyles2 } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getAllSuggestions } from '../../suggestions/getAllSuggestions';
|
||||
|
||||
@@ -32,21 +31,6 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel, t
|
||||
return result;
|
||||
}, [searchQuery, suggestions, trackSearch]);
|
||||
|
||||
const hasData = data?.series && data.series.length > 0 && !data.series.every((frame) => frame.length === 0);
|
||||
|
||||
if (config.featureToggles.newVizSuggestions && !hasData && !searchQuery) {
|
||||
return (
|
||||
<div className={styles.emptyStateWrapper}>
|
||||
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.new-panel.suggestions.empty-state-message">
|
||||
Run a query to start seeing suggested visualizations
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// This div is needed in some places to make AutoSizer work
|
||||
<div>
|
||||
@@ -132,18 +116,5 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
marginBottom: theme.spacing(1),
|
||||
justifyContent: 'space-evenly',
|
||||
}),
|
||||
emptyStateWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: theme.spacing(4),
|
||||
textAlign: 'center',
|
||||
minHeight: '200px',
|
||||
}),
|
||||
emptyStateIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5100,12 +5100,8 @@
|
||||
"new-panel": {
|
||||
"configure-button": "Configure",
|
||||
"configure-button-menu": "Toggle menu",
|
||||
"empty-state-message": "Run a query to visualize it here or go to all visualizations to add other panel types",
|
||||
"menu-open-panel-editor": "Configure",
|
||||
"menu-use-library-panel": "Use library panel",
|
||||
"suggestions": {
|
||||
"empty-state-message": "Run a query to start seeing suggested visualizations"
|
||||
}
|
||||
"menu-use-library-panel": "Use library panel"
|
||||
},
|
||||
"new-panel-title": "New panel",
|
||||
"on-create-new-row": {
|
||||
@@ -6215,6 +6211,7 @@
|
||||
"classic": "Classic",
|
||||
"json": "JSON",
|
||||
"v1-resource": "V1 Resource",
|
||||
"v2-resource": "V2 Resource",
|
||||
"yaml": "YAML"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user