From fd955f90ac8d6c57b0d138f21a14f3f1dc275346 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 14 Jan 2026 09:48:07 +0100 Subject: [PATCH 01/13] Alerting: Enable server-side folder search for GMA rules (#116201) * Alerting: Support backend filtering for folder search Updates the Grafana managed rules API and filter logic to support server-side filtering by folder (namespace). Changes: - Add `searchFolder` parameter to `getGrafanaGroups` API endpoint - Map filter state `namespace` to `searchFolder` in backend filter - Disable client-side namespace filtering when backend filtering is enabled - Update tests to verify correct behavior for folder search with backend filters * Add missing property in filter options * Update tests --- .../alerting/unified/api/prometheusApi.ts | 3 ++ .../rule-list/hooks/grafanaFilter.test.ts | 39 +++++++++++++++---- .../unified/rule-list/hooks/grafanaFilter.ts | 3 +- .../hooks/prometheusGroupsGenerator.ts | 1 + .../rule-list/paginationLimits.test.ts | 22 +---------- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/public/app/features/alerting/unified/api/prometheusApi.ts b/public/app/features/alerting/unified/api/prometheusApi.ts index 9432da368b6..c03b52eab4e 100644 --- a/public/app/features/alerting/unified/api/prometheusApi.ts +++ b/public/app/features/alerting/unified/api/prometheusApi.ts @@ -46,6 +46,7 @@ export type GrafanaPromRulesOptions = Omit { expect(frontendFilter.ruleMatches(regularRule)).toBe(true); expect(frontendFilter.ruleMatches(pluginRule)).toBe(true); }); + + it('should include searchFolder in backend filter when namespace is provided', () => { + const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' })); + + expect(backendFilter.searchFolder).toBe('my-folder'); + }); + + it('should skip namespace filtering on frontend when backend filtering is enabled', () => { + const group: PromRuleGroupDTO = { + name: 'Test Group', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + const { frontendFilter } = getGrafanaFilter(getFilter({ namespace: 'staging' })); + // Should return true because namespace filter is null (handled by backend) + expect(frontendFilter.groupMatches(group)).toBe(true); + }); }); describe('when alertingUIUseBackendFilters is disabled', () => { @@ -537,6 +556,12 @@ describe('grafana-managed rules', () => { expect(backendFilter.searchGroupName).toBeUndefined(); }); + it('should not include searchFolder in backend filter', () => { + const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' })); + + expect(backendFilter.searchFolder).toBeUndefined(); + }); + it('should perform groupName filtering on frontend', () => { const group: PromRuleGroupDTO = { name: 'CPU Usage Alerts', @@ -706,8 +731,8 @@ describe('grafana-managed rules', () => { expect(frontendFilter.groupMatches(group)).toBe(true); }); - it('should still apply always-frontend filters (namespace)', () => { - // Namespace filter should still work + it('should skip namespace filtering on frontend', () => { + // Namespace filter should be handled by backend const group: PromRuleGroupDTO = { name: 'Test Group', file: 'production/alerts', @@ -719,7 +744,7 @@ describe('grafana-managed rules', () => { expect(nsFilter.groupMatches(group)).toBe(true); const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' })); - expect(nsFilter2.groupMatches(group)).toBe(false); + expect(nsFilter2.groupMatches(group)).toBe(true); }); it('should skip dataSourceNames filtering on frontend (handled by backend)', () => { @@ -807,8 +832,8 @@ describe('grafana-managed rules', () => { expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(false); }); - it('should return true for client-side only filters', () => { - expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + it('should return false for namespace filter (handled by backend)', () => { + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false); }); it('should return false for plugins filter (handled by backend when feature toggle is enabled)', () => { @@ -862,8 +887,8 @@ describe('grafana-managed rules', () => { expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); - // Should return true for: always-frontend filters only (namespace) - expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + // Should return false for: namespace (handled by backend) + expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false); // plugins is backend-handled when both feature toggles are enabled expect(hasGrafanaClientSideFilters(getFilter({ plugins: 'hide' }))).toBe(false); diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts index e8c4cf3c44a..cca395a9cb2 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts @@ -96,6 +96,7 @@ export function getGrafanaFilter(filterState: Partial) { datasources: ruleFilterConfig.dataSourceNames ? undefined : datasourceUids, ruleMatchers: ruleMatchersBackendFilter, plugins: ruleFilterConfig.plugins ? undefined : normalizedFilterState.plugins, + searchFolder: groupFilterConfig.namespace ? undefined : normalizedFilterState.namespace, }; return { @@ -134,7 +135,7 @@ function buildGrafanaFilterConfigs() { }; const groupFilterConfig: GroupFilterConfig = { - namespace: namespaceFilter, + namespace: useBackendFilters ? null : namespaceFilter, groupName: useBackendFilters ? null : groupNameFilter, }; diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index add1097fa0f..e2cb1247ac8 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -45,6 +45,7 @@ interface GrafanaPromApiFilter { contactPoint?: string; title?: string; searchGroupName?: string; + searchFolder?: string; type?: 'alerting' | 'recording'; dashboardUid?: string; } diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts index 5d6b7c97782..648cfc18190 100644 --- a/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts +++ b/public/app/features/alerting/unified/rule-list/paginationLimits.test.ts @@ -75,6 +75,7 @@ describe('paginationLimits', () => { { contactPoint: 'slack' }, { dataSourceNames: ['prometheus'] }, { labels: ['severity=critical'] }, + { namespace: 'production' }, ])( 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', (filterState) => { @@ -84,16 +85,6 @@ describe('paginationLimits', () => { expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); } ); - - it.each>([ - { namespace: 'production' }, - { ruleState: PromAlertingRuleState.Firing, namespace: 'production' }, - ])('should return large limits for both when frontend filters are used: %p', (filterState) => { - const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); - - expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - }); }); describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => { @@ -158,6 +149,7 @@ describe('paginationLimits', () => { { contactPoint: 'slack' }, { dataSourceNames: ['prometheus'] }, { labels: ['severity=critical'] }, + { namespace: 'production' }, ])( 'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p', (filterState) => { @@ -167,16 +159,6 @@ describe('paginationLimits', () => { expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); } ); - - it.each>([{ namespace: 'production' }])( - 'should return large limits for both when frontend filters are used: %p', - (filterState) => { - const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState)); - - expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE }); - } - ); }); }); }); From 9d1d0e72c2a40f5b8836898141370c38b1735acd Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Wed, 14 Jan 2026 10:04:29 +0100 Subject: [PATCH 02/13] Alerting: add sync timer support (#114602) - add new feature flag to support enabling the dispatcher sync timer on the alertmanager - this attempts to synchronize the flushes across HA nodes to decrease amount of duplicate notifications --------- Co-authored-by: Yuri Tseretyan --- .../src/types/featureToggles.gen.ts | 4 ++ pkg/services/featuremgmt/registry.go | 11 +++- pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 17 +++++- pkg/services/ngalert/ngalert.go | 4 ++ pkg/services/ngalert/notifier/alertmanager.go | 26 ++++++++++ .../ngalert/notifier/dispatch_timer.go | 16 ++++++ .../ngalert/notifier/dispatch_timer_test.go | 36 +++++++++++++ pkg/services/ngalert/notifier/file_store.go | 10 ++++ .../ngalert/notifier/file_store_test.go | 45 ++++++++++++++++ .../ngalert/notifier/multiorg_alertmanager.go | 3 +- pkg/services/ngalert/notifier/state.go | 5 +- pkg/services/ngalert/notifier/testing.go | 52 +++++++++++++++++-- pkg/services/ngalert/remote/alertmanager.go | 24 +++++++-- .../client/alertmanager_configuration.go | 5 ++ 16 files changed, 251 insertions(+), 12 deletions(-) create mode 100644 pkg/services/ngalert/notifier/dispatch_timer.go create mode 100644 pkg/services/ngalert/notifier/dispatch_timer_test.go diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index aa0581004e1..cdcd4b53092 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1251,4 +1251,8 @@ export interface FeatureToggles { * Enables profiles exemplars support in profiles drilldown */ profilesExemplars?: boolean; + /** + * Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods + */ + alertingSyncDispatchTimer?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4c0456e9457..6b716789f8c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -981,7 +981,8 @@ var ( Stage: FeatureStageDeprecated, Owner: grafanaPartnerPluginsSquad, Expression: "true", // Enabled by default for now - }, { + }, + { Name: "alertingFilterV2", Description: "Enable the new alerting search experience", Stage: FeatureStageExperimental, @@ -2069,6 +2070,14 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, FrontendOnly: false, }, + { + Name: "alertingSyncDispatchTimer", + Description: "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + RequiresRestart: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index caba7cdab90..09c75a82041 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -280,3 +280,4 @@ multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true smoothingTransformation,experimental,@grafana/datapro,false,false,true secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false +alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d68fa56ec8c..17498d7afb9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -789,4 +789,8 @@ const ( // FlagProfilesExemplars // Enables profiles exemplars support in profiles drilldown FlagProfilesExemplars = "profilesExemplars" + + // FlagAlertingSyncDispatchTimer + // Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods + FlagAlertingSyncDispatchTimer = "alertingSyncDispatchTimer" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bfefc20f08b..071f1b0671e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -511,6 +511,20 @@ "frontend": true } }, + { + "metadata": { + "name": "alertingSyncDispatchTimer", + "resourceVersion": "1766161788928", + "creationTimestamp": "2025-12-19T16:29:48Z" + }, + "spec": { + "description": "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "alertingTriage", @@ -662,7 +676,8 @@ "metadata": { "name": "auditLoggingAppPlatform", "resourceVersion": "1767013056996", - "creationTimestamp": "2025-12-29T12:57:36Z" + "creationTimestamp": "2025-12-29T12:57:36Z", + "deletionTimestamp": "2026-01-06T09:18:36Z" }, "spec": { "description": "Enable audit logging with Kubernetes under app platform", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 53e49621117..5177107a602 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -213,6 +213,9 @@ func (ng *AlertNG) init() error { SkipVerify: ng.Cfg.Smtp.SkipVerify, StaticHeaders: ng.Cfg.Smtp.StaticHeaders, } + runtimeConfig := remoteClient.RuntimeConfig{ + DispatchTimer: notifier.GetDispatchTimer(ng.FeatureToggles).String(), + } cfg := remote.AlertmanagerConfig{ BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password, @@ -222,6 +225,7 @@ func (ng *AlertNG) init() error { ExternalURL: ng.Cfg.AppURL, SmtpConfig: smtpCfg, Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout, + RuntimeConfig: runtimeConfig, } autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, invalidReceiverAction notifier.InvalidReceiversAction) error { return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, invalidReceiverAction, ng.FeatureToggles) diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index f192ed88058..6d81d51dc75 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -33,6 +33,9 @@ const ( // How long we keep silences in the kvstore after they've expired. silenceRetention = 5 * 24 * time.Hour + + // How long we keep flushes in the kvstore after they've expired. + flushRetention = 5 * 24 * time.Hour ) type AlertingStore interface { @@ -44,8 +47,10 @@ type AlertingStore interface { type stateStore interface { SaveSilences(ctx context.Context, st alertingNotify.State) (int64, error) SaveNotificationLog(ctx context.Context, st alertingNotify.State) (int64, error) + SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error) GetSilences(ctx context.Context) (string, error) GetNotificationLog(ctx context.Context) (string, error) + GetFlushLog(ctx context.Context) (string, error) } type alertmanager struct { @@ -101,6 +106,10 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A if err != nil { return nil, err } + flushLog, err := stateStore.GetFlushLog(ctx) + if err != nil { + return nil, err + } silencesOptions := maintenanceOptions{ initialState: silences, @@ -123,12 +132,29 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A } l := log.New("ngalert.notifier") + dispatchTimer := GetDispatchTimer(featureToggles) + + var flushLogOptions *maintenanceOptions + if dispatchTimer == alertingNotify.DispatchTimerSync { + flushLogOptions = &maintenanceOptions{ + initialState: flushLog, + retention: flushRetention, + maintenanceFrequency: maintenanceInterval, + maintenanceFunc: func(state alertingNotify.State) (int64, error) { + // Detached context here is to make sure that when the service is shut down the persist operation is executed. + return stateStore.SaveFlushLog(context.Background(), state) + }, + } + } + opts := alertingNotify.GrafanaAlertmanagerOpts{ ExternalURL: cfg.AppURL, AlertStoreCallback: nil, PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout, Silences: silencesOptions, Nflog: nflogOptions, + FlushLog: flushLogOptions, + DispatchTimer: dispatchTimer, Limits: alertingNotify.Limits{ MaxSilences: cfg.UnifiedAlerting.AlertmanagerMaxSilencesCount, MaxSilenceSizeBytes: cfg.UnifiedAlerting.AlertmanagerMaxSilenceSizeBytes, diff --git a/pkg/services/ngalert/notifier/dispatch_timer.go b/pkg/services/ngalert/notifier/dispatch_timer.go new file mode 100644 index 00000000000..04eaf8cb296 --- /dev/null +++ b/pkg/services/ngalert/notifier/dispatch_timer.go @@ -0,0 +1,16 @@ +package notifier + +import ( + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +// GetDispatchTimer returns the appropriate dispatch timer based on feature toggles. +func GetDispatchTimer(features featuremgmt.FeatureToggles) (dt alertingNotify.DispatchTimer) { + //nolint:staticcheck // not yet migrated to OpenFeature + enabled := features.IsEnabledGlobally(featuremgmt.FlagAlertingSyncDispatchTimer) + if enabled { + dt = alertingNotify.DispatchTimerSync + } + return +} diff --git a/pkg/services/ngalert/notifier/dispatch_timer_test.go b/pkg/services/ngalert/notifier/dispatch_timer_test.go new file mode 100644 index 00000000000..3b42a562a32 --- /dev/null +++ b/pkg/services/ngalert/notifier/dispatch_timer_test.go @@ -0,0 +1,36 @@ +package notifier + +import ( + "testing" + + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/stretchr/testify/require" +) + +func TestGetDispatchTimer(t *testing.T) { + tests := []struct { + name string + featureFlagValue bool + expected alertingNotify.DispatchTimer + }{ + { + name: "feature flag enabled returns sync timer", + featureFlagValue: true, + expected: alertingNotify.DispatchTimerSync, + }, + { + name: "feature flag disabled returns default timer", + featureFlagValue: false, + expected: alertingNotify.DispatchTimerDefault, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + features := featuremgmt.WithFeatures(featuremgmt.FlagAlertingSyncDispatchTimer, tt.featureFlagValue) + result := GetDispatchTimer(features) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/services/ngalert/notifier/file_store.go b/pkg/services/ngalert/notifier/file_store.go index e9628cb536e..3bb128d692d 100644 --- a/pkg/services/ngalert/notifier/file_store.go +++ b/pkg/services/ngalert/notifier/file_store.go @@ -15,6 +15,7 @@ const ( KVNamespace = "alertmanager" NotificationLogFilename = "notifications" SilencesFilename = "silences" + FlushLogFilename = "flushes" ) // FileStore is in charge of persisting the alertmanager files to the database. @@ -42,6 +43,10 @@ func (fileStore *FileStore) GetNotificationLog(ctx context.Context) (string, err return fileStore.contentFor(ctx, NotificationLogFilename) } +func (fileStore *FileStore) GetFlushLog(ctx context.Context) (string, error) { + return fileStore.contentFor(ctx, FlushLogFilename) +} + // contentFor returns the content for the given Alertmanager kvstore key. func (fileStore *FileStore) contentFor(ctx context.Context, filename string) (string, error) { // Then, let's attempt to read it from the database. @@ -74,6 +79,11 @@ func (fileStore *FileStore) SaveNotificationLog(ctx context.Context, st alerting return fileStore.persist(ctx, NotificationLogFilename, st) } +// SaveFlushLog saves the flush log to the database and returns the size of the unencoded state. +func (fileStore *FileStore) SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error) { + return fileStore.persist(ctx, FlushLogFilename, st) +} + // persist takes care of persisting the binary representation of internal state to the database as a base64 encoded string. func (fileStore *FileStore) persist(ctx context.Context, filename string, st alertingNotify.State) (int64, error) { var size int64 diff --git a/pkg/services/ngalert/notifier/file_store_test.go b/pkg/services/ngalert/notifier/file_store_test.go index 1952eb5a0f1..7d4de602a0f 100644 --- a/pkg/services/ngalert/notifier/file_store_test.go +++ b/pkg/services/ngalert/notifier/file_store_test.go @@ -106,3 +106,48 @@ func TestFileStore_NotificationLog(t *testing.T) { t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded)) } } + +func TestFileStore_FlushLog(t *testing.T) { + store := fakes.NewFakeKVStore(t) + ctx := context.Background() + var orgId int64 = 1 + + // Initialize kvstore with empty flush log state. + initialState := flushLogState{} // FlushLog uses the same structure as nflog + decodedState, err := initialState.MarshalBinary() + require.NoError(t, err) + encodedState := base64.StdEncoding.EncodeToString(decodedState) + err = store.Set(ctx, orgId, KVNamespace, FlushLogFilename, encodedState) + require.NoError(t, err) + + fs := NewFileStore(orgId, store) + + // Load initial (empty). + flushLog, err := fs.GetFlushLog(ctx) + require.NoError(t, err) + decoded, err := decodeFlushLogState(strings.NewReader(flushLog)) + require.NoError(t, err) + if !cmp.Equal(initialState, decoded) { + t.Errorf("Unexpected Diff: %v", cmp.Diff(initialState, decoded)) + } + + // Save new flush log state. + now := time.Now() + oneHour := now.Add(time.Hour) + + v1 := createFlushLog(1, now, oneHour) + v2 := createFlushLog(2, now, oneHour) + newState := flushLogState{1: v1, 2: v2} + size, err := fs.SaveFlushLog(ctx, newState) + require.NoError(t, err) + require.Greater(t, size, int64(0)) + + // Load new. + flushLog, err = fs.GetFlushLog(ctx) + require.NoError(t, err) + decoded, err = decodeFlushLogState(strings.NewReader(flushLog)) + require.NoError(t, err) + if !cmp.Equal(newState, decoded) { + t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded)) + } +} diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 4aa0151d18f..a10aee29ef6 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -82,6 +82,7 @@ type Alertmanager interface { type ExternalState struct { Silences []byte Nflog []byte + FlushLog []byte } // StateMerger describes a type that is able to merge external state (nflog, silences) with its own. @@ -378,7 +379,7 @@ func (moa *MultiOrgAlertmanager) SyncAlertmanagersForOrgs(ctx context.Context, o func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context, activeOrganizations map[int64]struct{}, ) { - storedFiles := []string{NotificationLogFilename, SilencesFilename} + storedFiles := []string{NotificationLogFilename, SilencesFilename, FlushLogFilename} for _, fileName := range storedFiles { keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName) if err != nil { diff --git a/pkg/services/ngalert/notifier/state.go b/pkg/services/ngalert/notifier/state.go index c8551d2ed1a..04ba9d9a31d 100644 --- a/pkg/services/ngalert/notifier/state.go +++ b/pkg/services/ngalert/notifier/state.go @@ -5,5 +5,8 @@ func (am *alertmanager) MergeState(state ExternalState) error { if err := am.Base.MergeNflog(state.Nflog); err != nil { return err } - return am.Base.MergeSilences(state.Silences) + if err := am.Base.MergeSilences(state.Silences); err != nil { + return err + } + return am.Base.MergeFlushLog(state.FlushLog) } diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index 9fccf6d2f0d..c2b2190183f 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -11,6 +11,7 @@ import ( "time" "github.com/matttproud/golang_protobuf_extensions/pbutil" + "github.com/prometheus/alertmanager/flushlog/flushlogpb" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/common/model" @@ -228,15 +229,13 @@ func (f *FakeOrgStore) FetchOrgIds(_ context.Context) ([]int64, error) { return f.orgs, nil } -type NoValidation struct { -} +type NoValidation struct{} func (n NoValidation) Validate(_ models.NotificationSettings) error { return nil } -type RejectingValidation struct { -} +type RejectingValidation struct{} func (n RejectingValidation) Validate(s models.NotificationSettings) error { return ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: s.Receiver}} @@ -365,6 +364,51 @@ func createNotificationLog(groupKey string, receiverName string, sentAt, expires } } +// https://github.com/grafana/prometheus-alertmanager/blob/main/flushlog/flushlog.go#L136-L136 +type flushLogState map[uint64]*flushlogpb.MeshFlushLog + +func (s flushLogState) MarshalBinary() ([]byte, error) { + var buf bytes.Buffer + + for _, e := range s { + if _, err := pbutil.WriteDelimited(&buf, e); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func createFlushLog(groupFingerprint uint64, ts, expiresAt time.Time) *flushlogpb.MeshFlushLog { + return &flushlogpb.MeshFlushLog{ + FlushLog: &flushlogpb.FlushLog{ + GroupFingerprint: groupFingerprint, + Timestamp: ts, + }, + ExpiresAt: expiresAt, + } +} + +// decodeFlushLogState copied from decodeState in prometheus-alertmanager/flushlog/flushlog.go +func decodeFlushLogState(r io.Reader) (flushLogState, error) { + st := flushLogState{} + for { + var e flushlogpb.MeshFlushLog + _, err := pbutil.ReadDelimited(r, &e) + if err == nil { + if e.FlushLog == nil || e.FlushLog.GroupFingerprint == 0 || e.FlushLog.Timestamp.IsZero() { + return nil, errInvalidState + } + st[e.FlushLog.GroupFingerprint] = &e + continue + } + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + return st, nil +} + type call struct { Method string Args []interface{} diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 60740d935af..07fa5e3138f 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -47,6 +47,7 @@ import ( type stateStore interface { GetSilences(ctx context.Context) (string, error) GetNotificationLog(ctx context.Context) (string, error) + GetFlushLog(ctx context.Context) (string, error) } // AutogenFn is a function that adds auto-generated routes to a configuration. @@ -86,6 +87,8 @@ type Alertmanager struct { promoteConfig bool externalURL string + + runtimeConfig remoteClient.RuntimeConfig } type AlertmanagerConfig struct { @@ -111,6 +114,9 @@ type AlertmanagerConfig struct { // Timeout for the HTTP client. Timeout time.Duration + + // RuntimeConfig specifies runtime behavior settings for the remote Alertmanager. + RuntimeConfig remoteClient.RuntimeConfig } func (cfg *AlertmanagerConfig) Validate() error { @@ -203,6 +209,7 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto externalURL: cfg.ExternalURL, promoteConfig: cfg.PromoteConfig, smtp: cfg.SmtpConfig, + runtimeConfig: cfg.RuntimeConfig, } // Parse the default configuration once and remember its hash so we can compare it later. @@ -331,10 +338,11 @@ func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, crea AlertmanagerConfig: mergeResult.Config, Templates: templates, }, - CreatedAt: createdAtEpoch, - Promoted: am.promoteConfig, - ExternalURL: am.externalURL, - SmtpConfig: am.smtp, + CreatedAt: createdAtEpoch, + Promoted: am.promoteConfig, + ExternalURL: am.externalURL, + SmtpConfig: am.smtp, + RuntimeConfig: am.runtimeConfig, } cfgHash, err := calculateUserGrafanaConfigHash(payload) @@ -388,6 +396,8 @@ func (am *Alertmanager) GetRemoteState(ctx context.Context) (notifier.ExternalSt rs.Silences = p.Data case "nfl": rs.Nflog = p.Data + case "fls": + rs.FlushLog = p.Data default: return rs, fmt.Errorf("unknown part key %q", p.Key) } @@ -677,6 +687,12 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) { } parts = append(parts, alertingClusterPB.Part{Key: notifier.NotificationLogFilename, Data: []byte(notificationLog)}) + flushLog, err := am.state.GetFlushLog(ctx) + if err != nil { + return "", fmt.Errorf("error getting flush log: %w", err) + } + parts = append(parts, alertingClusterPB.Part{Key: notifier.FlushLogFilename, Data: []byte(flushLog)}) + fs := alertingClusterPB.FullState{ Parts: parts, } diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index a53132a8812..75246a6f32d 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -29,6 +29,10 @@ func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) { return definition.MarshalJSONWithSecrets((*cfg)(u)) } +type RuntimeConfig struct { + DispatchTimer string `json:"dispatch_timer"` +} + type UserGrafanaConfig struct { GrafanaAlertmanagerConfig GrafanaAlertmanagerConfig `json:"configuration"` Hash string `json:"configuration_hash"` @@ -37,6 +41,7 @@ type UserGrafanaConfig struct { Promoted bool `json:"promoted"` ExternalURL string `json:"external_url"` SmtpConfig SmtpConfig `json:"smtp_config"` + RuntimeConfig RuntimeConfig `json:"runtime_config"` } func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) { From 78d507d285aeda286a95353ae09aa725453e4b99 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 14 Jan 2026 11:50:37 +0200 Subject: [PATCH 03/13] Dynamic Dashboards: Change the stage of the feature toggle (#116189) --- .../configure-grafana/feature-toggles/index.md | 1 + packages/grafana-data/src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 2 +- pkg/services/featuremgmt/toggles_gen.json | 11 +++++++---- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index b7f55555e07..813efb29eba 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -83,6 +83,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `reportingRetries` | Enables rendering retries for the reporting feature | | `externalServiceAccounts` | Automatic service account and token setup for plugins | | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | +| `dashboardNewLayouts` | Enables new dashboard layouts | | `pdfTables` | Enables generating table data as PDF in reporting | | `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | | `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index cdcd4b53092..1581af4e3d7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -356,7 +356,7 @@ export interface FeatureToggles { */ dashboardScene?: boolean; /** - * Enables experimental new dashboard layouts + * Enables new dashboard layouts */ dashboardNewLayouts?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6b716789f8c..b436639a896 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -574,8 +574,8 @@ var ( }, { Name: "dashboardNewLayouts", - Description: "Enables experimental new dashboard layouts", - Stage: FeatureStageExperimental, + Description: "Enables new dashboard layouts", + Stage: FeatureStagePublicPreview, FrontendOnly: false, // The restore backend feature changes behavior based on this flag Owner: grafanaDashboardsSquad, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 09c75a82041..c605f098bb5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -79,7 +79,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true -dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false +dashboardNewLayouts,preview,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true drilldownRecommendations,experimental,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 17498d7afb9..3a71abab00a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -260,7 +260,7 @@ const ( FlagAnnotationPermissionUpdate = "annotationPermissionUpdate" // FlagDashboardNewLayouts - // Enables experimental new dashboard layouts + // Enables new dashboard layouts FlagDashboardNewLayouts = "dashboardNewLayouts" // FlagPdfTables diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 071f1b0671e..66cff415ec4 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1030,12 +1030,15 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1764664939750", - "creationTimestamp": "2024-10-23T08:55:45Z" + "resourceVersion": "1768382835527", + "creationTimestamp": "2024-10-23T08:55:45Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC" + } }, "spec": { - "description": "Enables experimental new dashboard layouts", - "stage": "experimental", + "description": "Enables new dashboard layouts", + "stage": "preview", "codeowner": "@grafana/dashboards-squad" } }, From d680537ea1072b14759eaba4daa9e56a729a1a53 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 14 Jan 2026 11:05:16 +0100 Subject: [PATCH 04/13] Advisor: Simplify interface used (#116191) --- apps/advisor/pkg/app/checks/datasourcecheck/check.go | 4 ++-- .../pkg/app/checks/datasourcecheck/missing_plugin_step.go | 2 +- apps/advisor/pkg/app/checks/ifaces.go | 8 ++++++++ apps/advisor/pkg/app/checks/plugincheck/check.go | 4 ++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index cae29e181fd..c35c47e45f6 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -28,7 +28,7 @@ type check struct { PluginStore pluginstore.Store PluginContextProvider PluginContextProvider PluginClient plugins.Client - PluginRepo repo.Service + PluginRepo checks.PluginInfoGetter GrafanaVersion string pluginCanBeInstalledCache map[string]bool pluginExistsCacheMu sync.RWMutex @@ -39,7 +39,7 @@ func New( pluginStore pluginstore.Store, pluginContextProvider PluginContextProvider, pluginClient plugins.Client, - pluginRepo repo.Service, + pluginRepo checks.PluginInfoGetter, grafanaVersion string, ) checks.Check { return &check{ diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go b/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go index 1d784f0a544..9b70f5d0896 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go @@ -15,7 +15,7 @@ import ( type missingPluginStep struct { PluginStore pluginstore.Store - PluginRepo repo.Service + PluginRepo checks.PluginInfoGetter GrafanaVersion string } diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go index 6573253b557..2b205933151 100644 --- a/apps/advisor/pkg/app/checks/ifaces.go +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/logging" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/pkg/plugins/repo" ) // Check returns metadata about the check being executed and the list of Steps @@ -37,3 +38,10 @@ type Step interface { // Run executes the step for an item and returns a report Run(ctx context.Context, log logging.Logger, obj *advisorv0alpha1.CheckSpec, item any) ([]advisorv0alpha1.CheckReportFailure, error) } + +// PluginInfoGetter is a minimal interface for retrieving plugin information from a repository. +// It contains only the GetPluginsInfo method used by plugincheck and datasourcecheck. +type PluginInfoGetter interface { + // GetPluginsInfo will return a list of plugins from grafana.com/api/plugins. + GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) +} diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 3d261f81b67..00bc293e86c 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -17,7 +17,7 @@ const ( func New( pluginStore pluginstore.Store, - pluginRepo repo.Service, + pluginRepo checks.PluginInfoGetter, updateChecker pluginchecker.PluginUpdateChecker, pluginErrorResolver plugins.ErrorResolver, grafanaVersion string, @@ -33,7 +33,7 @@ func New( type check struct { PluginStore pluginstore.Store - PluginRepo repo.Service + PluginRepo checks.PluginInfoGetter updateChecker pluginchecker.PluginUpdateChecker pluginErrorResolver plugins.ErrorResolver GrafanaVersion string From afd84f033511d68fc4c787ca06794e132b37baf1 Mon Sep 17 00:00:00 2001 From: Natalia Bernarte Oses Date: Wed, 14 Jan 2026 11:10:51 +0100 Subject: [PATCH 05/13] Datagrid: Deprecate panel (#116071) * deprecate datagrid * Update docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../panels-visualizations/visualizations/datagrid/index.md | 4 +++- public/app/plugins/panel/datagrid/plugin.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md index b54326968a6..3a4a448ae85 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/datagrid/index.md @@ -30,7 +30,9 @@ refs: # Datagrid -{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}} +{{< admonition type="caution" >}} +Starting with Grafana 12.4, Datagrid is deprecated. It will be removed in version 13.0. +{{< /admonition >}} Datagrids offer you the ability to create, edit, and fine-tune data within Grafana. As such, this panel can act as a data source for other panels inside a dashboard. diff --git a/public/app/plugins/panel/datagrid/plugin.json b/public/app/plugins/panel/datagrid/plugin.json index 72d430b0b87..4baeb456ea1 100644 --- a/public/app/plugins/panel/datagrid/plugin.json +++ b/public/app/plugins/panel/datagrid/plugin.json @@ -2,7 +2,7 @@ "type": "panel", "name": "Datagrid", "id": "datagrid", - "state": "beta", + "state": "deprecated", "info": { "author": { From 0d1e0bc21cf690e5df837f765b21a5ab6c0a9467 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 14 Jan 2026 11:29:43 +0100 Subject: [PATCH 06/13] PanelMenu: use openInNewTab links extensions API correctly (#116200) * Extensons: Make links use openInNewTab API * Use openInNewTab api correctly in the UI * Bump scenes * Fx circular dep * test * Revert "test" This reverts commit 8784a7992c60889dda824433331b7072ea80b0cd. --- package.json | 4 ++-- packages/grafana-data/src/index.ts | 2 +- packages/grafana-data/src/types/dataLink.ts | 3 +-- packages/grafana-data/src/types/linkTarget.ts | 4 ++++ packages/grafana-data/src/types/navModel.ts | 2 +- packages/grafana-data/src/types/panel.ts | 2 ++ .../plugins/extensions/getPluginExtensions.ts | 1 + .../app/features/plugins/extensions/utils.tsx | 2 ++ yarn.lock | 22 +++++++++---------- 9 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 packages/grafana-data/src/types/linkTarget.ts diff --git a/package.json b/package.json index a6887b216e4..0a39ff67aea 100644 --- a/package.json +++ b/package.json @@ -293,8 +293,8 @@ "@grafana/plugin-ui": "^0.11.1", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "v6.52.1", - "@grafana/scenes-react": "v6.52.1", + "@grafana/scenes": "6.52.2", + "@grafana/scenes-react": "6.52.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 6027b566764..5ed081b00f0 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -844,7 +844,6 @@ export { DataLinkConfigOrigin, SupportedTransformationType, type InternalDataLink, - type LinkTarget, type LinkModel, type LinkModelSupplier, VariableOrigin, @@ -852,6 +851,7 @@ export { VariableSuggestionsScope, OneClickMode, } from './types/dataLink'; +export { type LinkTarget } from './types/linkTarget'; export { type Action, type ActionModel, diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 815b67f0352..ad556a75c76 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,5 +1,6 @@ import { ScopedVars } from './ScopedVars'; import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore'; +import { LinkTarget } from './linkTarget'; import { InterpolateFunction } from './panel'; import { DataQuery } from './query'; import { TimeRange } from './time'; @@ -88,8 +89,6 @@ export interface InternalDataLink { range?: TimeRange; } -export type LinkTarget = '_blank' | '_self' | undefined; - /** * Processed Link Model. The values are ready to use */ diff --git a/packages/grafana-data/src/types/linkTarget.ts b/packages/grafana-data/src/types/linkTarget.ts new file mode 100644 index 00000000000..2cdd963da7a --- /dev/null +++ b/packages/grafana-data/src/types/linkTarget.ts @@ -0,0 +1,4 @@ +/** + * Target for links - controls whether link opens in new tab or same tab + */ +export type LinkTarget = '_blank' | '_self' | undefined; diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index f9ebb23fc07..815b9d04e2d 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -1,7 +1,7 @@ import { ComponentType } from 'react'; -import { LinkTarget } from './dataLink'; import { IconName } from './icon'; +import { LinkTarget } from './linkTarget'; export interface NavLinkDTO { id?: string; diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index acf1e36c905..b9d6491cf9b 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -11,6 +11,7 @@ import { DataFrame } from './dataFrame'; import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource'; import { FieldConfigSource } from './fieldOverrides'; import { IconName } from './icon'; +import { LinkTarget } from './linkTarget'; import { OptionEditorConfig } from './options'; import { PluginMeta } from './plugin'; import { AbsoluteTimeRange, TimeRange, TimeZone } from './time'; @@ -191,6 +192,7 @@ export interface PanelMenuItem { onClick?: (event: React.MouseEvent) => void; shortcut?: string; href?: string; + target?: LinkTarget; subMenu?: PanelMenuItem[]; } diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index c81fd7c26f4..6f02a7daf77 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -141,6 +141,7 @@ export const getPluginExtensions: GetExtensions = ({ description: overrides?.description || addedLink.description || '', path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined, category: overrides?.category || addedLink.category, + openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab, }; extensions.push(extension); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index adb1ed8616d..0046aab0fe6 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -420,6 +420,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel href: extension.path, onClick: extension.onClick, iconClassName: extension.icon, + target: extension.openInNewTab ? '_blank' : undefined, }); continue; } @@ -433,6 +434,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel href: extension.path, onClick: extension.onClick, iconClassName: extension.icon, + target: extension.openInNewTab ? '_blank' : undefined, }); } diff --git a/yarn.lock b/yarn.lock index 41cc10d8353..1069acd8544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,11 +3789,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:v6.52.1": - version: 6.52.1 - resolution: "@grafana/scenes-react@npm:6.52.1" +"@grafana/scenes-react@npm:6.52.2": + version: 6.52.2 + resolution: "@grafana/scenes-react@npm:6.52.2" dependencies: - "@grafana/scenes": "npm:6.52.1" + "@grafana/scenes": "npm:6.52.2" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3805,7 +3805,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/2f7c6ca8e26befd331808afb0cb934e2991e889a4de78be1122c536219676261c59c6204510761a1d4250fd44a3767818f0f225d23b2e7243cfc17baf8ca6ca3 + checksum: 10/c393faf6612e78254dab79b15cc970448d74ba9784ccda623953c5dbc21d91a8da94b7ad7d0d294eac51314cc193c419a7cb48295fd50b1f9c4472699669eb3e languageName: node linkType: hard @@ -3835,9 +3835,9 @@ __metadata: languageName: node linkType: hard -"@grafana/scenes@npm:6.52.1, @grafana/scenes@npm:v6.52.1": - version: 6.52.1 - resolution: "@grafana/scenes@npm:6.52.1" +"@grafana/scenes@npm:6.52.2": + version: 6.52.2 + resolution: "@grafana/scenes@npm:6.52.2" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3857,7 +3857,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/d6172b51121e03c7dcbf30046772f99fc45922c1f7b360a7c3d2c0391300e378f306cb78251dda3b30895679379c38db30e4d52fee67a56cd95f18f38aadf3fb + checksum: 10/f6dbe20db78bb1aa09cc38025534917887713d73119a172febb44700837ed859363ee0436b5f4bda6bc063f9432115e32519ab4c8da7834cf1fc22d43fea7711 languageName: node linkType: hard @@ -19790,8 +19790,8 @@ __metadata: "@grafana/plugin-ui": "npm:^0.11.1" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:v6.52.1" - "@grafana/scenes-react": "npm:v6.52.1" + "@grafana/scenes": "npm:6.52.2" + "@grafana/scenes-react": "npm:6.52.2" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*" From 170ac31c5ad5527c17e838949ce68dc5c5752c34 Mon Sep 17 00:00:00 2001 From: Alejandro Fraenkel Date: Wed, 14 Jan 2026 11:58:11 +0100 Subject: [PATCH 07/13] Alerting: Add alertingNavigationV2 feature toggle (#116215) feat(alerting): add alertingNavigationV2 feature toggle Introduces a new feature toggle to enable the improved Alerting navigation structure with grouped menu items. This toggle will allow: - Safe incremental rollout of navigation changes - Quick rollback if issues arise - Handling BE/FE deployment timing differences Toggle details: - Name: alertingNavigationV2 - Stage: Experimental - Owner: @grafana/alerting-squad - Default: false (disabled) - Affects: Both backend (navtree) and frontend (navigation hooks) --- .../grafana-data/src/types/featureToggles.gen.ts | 4 ++++ pkg/services/featuremgmt/registry.go | 7 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 12 ++++++++++++ 5 files changed, 28 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 1581af4e3d7..eed0d330481 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -531,6 +531,10 @@ export interface FeatureToggles { */ alertingListViewV2?: boolean; /** + * Enables the new Alerting navigation structure with improved menu grouping + */ + alertingNavigationV2?: boolean; + /** * Enables saved searches for alert rules list */ alertingSavedSearches?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b436639a896..72623cba2fa 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -879,6 +879,13 @@ var ( Owner: grafanaAlertingSquad, FrontendOnly: true, }, + { + Name: "alertingNavigationV2", + Description: "Enables the new Alerting navigation structure with improved menu grouping", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: false, + }, { Name: "alertingSavedSearches", Description: "Enables saved searches for alert rules list", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c605f098bb5..61505b65571 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -121,6 +121,7 @@ dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false dashboardTemplates,preview,@grafana/sharing-squad,false,false,false alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true +alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 3a71abab00a..db2b4484e42 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -371,6 +371,10 @@ const ( // Enables a flow to get started with a new dashboard from a template FlagDashboardTemplates = "dashboardTemplates" + // FlagAlertingNavigationV2 + // Enables the new Alerting navigation structure with improved menu grouping + FlagAlertingNavigationV2 = "alertingNavigationV2" + // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 66cff415ec4..4832f93c309 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -348,6 +348,18 @@ "expression": "true" } }, + { + "metadata": { + "name": "alertingNavigationV2", + "resourceVersion": "1768320918269", + "creationTimestamp": "2026-01-13T16:15:18Z" + }, + "spec": { + "description": "Enables the new Alerting navigation structure with improved menu grouping", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, { "metadata": { "name": "alertingNotificationHistory", From 987c1fc6b68e1ae63f807f13fa3d01a2a7c80d14 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Wed, 14 Jan 2026 12:07:53 +0100 Subject: [PATCH 08/13] feat(unified): add index scoring model config (#116210) * feat(unified): add bm25 index scoring model We want try BM25 scoring model since they have global scoring which we can probably re-use for fan-in/fan-out logic https://github.com/blevesearch/bleve/blob/32d98823c4b7482c62cc6c847508ed7659c23c37/docs/scoring.md#global-scoring * fix(plugins): update plugin test data --- pkg/setting/setting.go | 1 + pkg/setting/setting_unified_storage.go | 4 +++ pkg/storage/unified/search/bleve.go | 7 +++- .../unified/search/bleve_integration_test.go | 31 +++++++++++++++++ pkg/storage/unified/search/bleve_mappings.go | 6 ++-- .../unified/search/bleve_mappings_test.go | 2 +- .../unified/search/bleve_search_test.go | 2 ++ pkg/storage/unified/search/bleve_test.go | 3 ++ pkg/storage/unified/search/options.go | 1 + .../unified/sql/test/integration_test.go | 34 ++++++++++++------- .../api/plugins/data/expectedListResp.json | 30 ++++++++-------- 11 files changed, 89 insertions(+), 32 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9667b82b9fa..1e26b9067ef 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -600,6 +600,7 @@ type Cfg struct { IndexRebuildInterval time.Duration IndexCacheTTL time.Duration IndexMinUpdateInterval time.Duration // Don't update index if it was updated less than this interval ago. + IndexScoringModel string // Note: Temporary config to switch the index scoring model and will be removed soon. MaxFileIndexAge time.Duration // Max age of file-based indexes. Index older than this will be rebuilt asynchronously. MinFileIndexBuildVersion string // Minimum version of Grafana that built the file-based index. If index was built with older Grafana, it will be rebuilt asynchronously. EnableSharding bool diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 21a3f455993..b47e8879826 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -123,6 +123,10 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.IndexRebuildInterval = section.Key("index_rebuild_interval").MustDuration(24 * time.Hour) cfg.IndexCacheTTL = section.Key("index_cache_ttl").MustDuration(10 * time.Minute) cfg.IndexMinUpdateInterval = section.Key("index_min_update_interval").MustDuration(0) + cfg.IndexScoringModel = section.Key("index_scoring_model").MustString("") + if cfg.IndexScoringModel != "" { + cfg.Logger.Info("Index scoring model set", "model", cfg.IndexScoringModel) + } cfg.SprinklesApiServer = section.Key("sprinkles_api_server").String() cfg.SprinklesApiServerPageLimit = section.Key("sprinkles_api_server_page_limit").MustInt(10000) cfg.CACertPath = section.Key("ca_cert_path").String() diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index eec7290633b..a988b71aa38 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -81,6 +81,11 @@ type BleveOptions struct { // Indexes that are not owned by current instance are eligible for cleanup. // If nil, all indexes are owned by the current instance. OwnsIndex func(key resource.NamespacedResource) (bool, error) + + // ScoringModel defines the scoring model used for the bleve indexes + // Default: index.TFIDFScoring + // Supported values: index.TFIDFScoring and index.BM25Scoring + ScoringModel string } type bleveBackend struct { @@ -368,7 +373,7 @@ func (b *bleveBackend) BuildIndex( attribute.String("reason", indexBuildReason), ) - mapper, err := GetBleveMappings(fields) + mapper, err := GetBleveMappings(b.opts.ScoringModel, fields) if err != nil { return nil, err } diff --git a/pkg/storage/unified/search/bleve_integration_test.go b/pkg/storage/unified/search/bleve_integration_test.go index 819fd5a8d9a..1f34444574f 100644 --- a/pkg/storage/unified/search/bleve_integration_test.go +++ b/pkg/storage/unified/search/bleve_integration_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + index "github.com/blevesearch/bleve_index_api" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -19,6 +20,7 @@ func TestBleveSearchBackend(t *testing.T) { backend, err := NewBleveBackend(BleveOptions{ Root: tempDir, FileThreshold: 5, + ScoringModel: index.BM25Scoring, }, nil) require.NoError(t, err) require.NotNil(t, backend) @@ -52,3 +54,32 @@ func TestSearchBackendBenchmark(t *testing.T) { unitest.BenchmarkSearchBackend(t, backend, opts) } + +func BenchmarkScoringModels(b *testing.B) { + models := []string{index.TFIDFScoring, index.BM25Scoring} + + for _, model := range models { + b.Run(model, func(b *testing.B) { + tempDir := b.TempDir() + + backend, err := NewBleveBackend(BleveOptions{ + Root: tempDir, + ScoringModel: model, + }, nil) + require.NoError(b, err) + require.NotNil(b, backend) + + b.Cleanup(backend.Stop) + + opts := &unitest.BenchmarkOptions{ + NumResources: 1000, + Concurrency: 4, + NumNamespaces: 10, + NumGroups: 10, + NumResourceTypes: 10, + } + + unitest.BenchmarkSearchBackend(b, backend, opts) + }) + } +} diff --git a/pkg/storage/unified/search/bleve_mappings.go b/pkg/storage/unified/search/bleve_mappings.go index 43adcbc607e..20eb2ffb8df 100644 --- a/pkg/storage/unified/search/bleve_mappings.go +++ b/pkg/storage/unified/search/bleve_mappings.go @@ -5,13 +5,15 @@ import ( "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" "github.com/blevesearch/bleve/v2/mapping" - "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) -func GetBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) { +func GetBleveMappings(scoringModel string, fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) { mapper := bleve.NewIndexMapping() + if scoringModel != "" { + mapper.ScoringModel = scoringModel + } err := RegisterCustomAnalyzers(mapper) if err != nil { diff --git a/pkg/storage/unified/search/bleve_mappings_test.go b/pkg/storage/unified/search/bleve_mappings_test.go index 3b8027ee06e..821cf987990 100644 --- a/pkg/storage/unified/search/bleve_mappings_test.go +++ b/pkg/storage/unified/search/bleve_mappings_test.go @@ -13,7 +13,7 @@ import ( ) func TestDocumentMapping(t *testing.T) { - mappings, err := search.GetBleveMappings(nil) + mappings, err := search.GetBleveMappings("", nil) require.NoError(t, err) data := resource.IndexableDocument{ Title: "title", diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index c10aa3f6726..b221a60a7d6 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/blevesearch/bleve/v2" + index "github.com/blevesearch/bleve_index_api" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -258,6 +259,7 @@ func newTestDashboardsIndex(t testing.TB, threshold int64, size int64, writer re backend, err := search.NewBleveBackend(search.BleveOptions{ Root: t.TempDir(), FileThreshold: threshold, // use in-memory for tests + ScoringModel: index.BM25Scoring, }, nil) require.NoError(t, err) diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index c9c3967cd58..a88951a100a 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/blevesearch/bleve/v2" + index "github.com/blevesearch/bleve_index_api" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -50,6 +51,7 @@ func TestBleveBackend(t *testing.T) { backend, err := NewBleveBackend(BleveOptions{ Root: tmpdir, FileThreshold: 5, // with more than 5 items we create a file on disk + ScoringModel: index.BM25Scoring, }, nil) require.NoError(t, err) t.Cleanup(backend.Stop) @@ -773,6 +775,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro IndexCacheTTL: defaultIndexCacheTTL, Logger: log.NewNopLogger(), BuildVersion: buildVersion, + ScoringModel: index.BM25Scoring, } for _, opt := range options { opt(&opts) diff --git a/pkg/storage/unified/search/options.go b/pkg/storage/unified/search/options.go index d450e9ae24b..64cf074f52c 100644 --- a/pkg/storage/unified/search/options.go +++ b/pkg/storage/unified/search/options.go @@ -46,6 +46,7 @@ func NewSearchOptions( BuildVersion: cfg.BuildVersion, OwnsIndex: ownsIndexFn, IndexMinUpdateInterval: cfg.IndexMinUpdateInterval, + ScoringModel: cfg.IndexScoringModel, }, indexMetrics) if err != nil { diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 166e2dac372..f73bb61d679 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + index "github.com/blevesearch/bleve_index_api" "github.com/go-jose/go-jose/v4/jwt" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" @@ -129,21 +130,28 @@ func TestIntegrationSearchAndStorage(t *testing.T) { ctx := context.Background() - // Create a new bleve backend - search, err := search.NewBleveBackend(search.BleveOptions{ - FileThreshold: 0, - Root: t.TempDir(), - }, nil) - require.NoError(t, err) - require.NotNil(t, search) - t.Cleanup(search.Stop) + scoringModels := []string{index.TFIDFScoring, index.BM25Scoring} - // Create a new resource backend - storage, _ := newTestBackend(t, false, 0) - require.NotNil(t, storage) + for _, model := range scoringModels { + t.Run(model, func(t *testing.T) { + // Create a new bleve backend + search, err := search.NewBleveBackend(search.BleveOptions{ + FileThreshold: 0, + Root: t.TempDir(), + ScoringModel: model, + }, nil) + require.NoError(t, err) + require.NotNil(t, search) + t.Cleanup(search.Stop) - // Run the shared storage and search tests - unitest.RunTestSearchAndStorage(t, ctx, storage, search) + // Create a new resource backend + storage, _ := newTestBackend(t, false, 0) + require.NotNil(t, storage) + + // Run the shared storage and search tests + unitest.RunTestSearchAndStorage(t, ctx, storage, search) + }) + } } func TestClientServer(t *testing.T) { diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 24f705eccd1..3d1ccce6a59 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -209,7 +209,7 @@ "path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png" } ], - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": [ "azure", @@ -589,7 +589,7 @@ "hasUpdate": false, "defaultNavUrl": "/plugins/datagrid/", "category": "", - "state": "beta", + "state": "deprecated", "signature": "internal", "signatureType": "", "signatureOrg": "", @@ -880,7 +880,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -934,7 +934,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": [ "grafana", @@ -1000,7 +1000,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1217,7 +1217,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1325,7 +1325,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1375,7 +1375,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1425,7 +1425,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1575,7 +1575,7 @@ }, "build": {}, "screenshots": null, - "version": "", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -1629,7 +1629,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": [ "grafana", @@ -1734,7 +1734,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -2042,7 +2042,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -2092,7 +2092,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, @@ -2445,7 +2445,7 @@ }, "build": {}, "screenshots": null, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "updated": "", "keywords": null }, From 040854c8af5e5a556e4fa05ae95df0c966338ae4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 14 Jan 2026 14:55:05 +0300 Subject: [PATCH 09/13] Search: Allow query field selection (#116238) --- pkg/storage/unified/proto/search.proto | 31 ++ pkg/storage/unified/resource/document.go | 1 - pkg/storage/unified/resourcepb/search.pb.go | 432 ++++++++++++------ .../unified/resourcepb/search_grpc.pb.go | 4 + pkg/storage/unified/search/bleve.go | 85 ++-- pkg/tests/apis/dashboard/search_test.go | 58 ++- ...-query.json => t01-query-single-word.json} | 6 +- .../searchV0/t02-query-multiple-words.json | 17 + ...xt-panel.json => t03-with-text-panel.json} | 0 .../searchV0/t04-title-ngram-prefix.json | 17 + .../searchV0/t05-title-ngram-middle-word.json | 17 + 11 files changed, 474 insertions(+), 194 deletions(-) rename pkg/tests/apis/dashboard/testdata/searchV0/{t01-simple-query.json => t01-query-single-word.json} (88%) create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json rename pkg/tests/apis/dashboard/testdata/searchV0/{t02-with-text-panel.json => t03-with-text-panel.json} (100%) create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json diff --git a/pkg/storage/unified/proto/search.proto b/pkg/storage/unified/proto/search.proto index 5018c97c9db..62c6afb323a 100644 --- a/pkg/storage/unified/proto/search.proto +++ b/pkg/storage/unified/proto/search.proto @@ -9,11 +9,13 @@ import "resource.proto"; // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics service ResourceIndex { + // Query for documents rpc Search(ResourceSearchRequest) returns (ResourceSearchResponse); // Get the resource stats rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse); + // Rebuild the search index rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse); } @@ -49,6 +51,20 @@ message ResourceStatsResponse { repeated Stats stats = 2; } +// This controls what query and analyzers are applied to the specified field +// See: https://blevesearch.com/docs/Analyzers/ +enum QueryFieldType { + // Picks a reasonable analyzer given the input. Currently this always uses TEXT + // In the future, it may change to depend on the indexed field type + DEFAULT = 0; + // Use free text analyzer. The query is broken into a normalized set of tokens + TEXT = 1; + // The query must exactly match the indexed token + KEYWORD = 2; + // Like a text query, but the position and offsets influence the score + PHRASE = 3; +} + // Search within a single resource message ResourceSearchRequest { message Sort { @@ -64,6 +80,18 @@ message ResourceSearchRequest { // date queries } + // Defines the field in the index to query + // Boost is optional, and allows weighting the field higher in the results + message QueryField { + // The field name in the index to query + string name = 1; + + QueryFieldType type = 2; + + // Boost value for this field + float boost = 3; + } + // The key must include namespace + group + resource ListOptions options = 1; @@ -99,6 +127,9 @@ message ResourceSearchRequest { int64 page = 11; int64 permission = 12; + + // Optionally specify which fields are included in the query + repeated QueryField query_fields = 13; } message ResourceSearchResponse { diff --git a/pkg/storage/unified/resource/document.go b/pkg/storage/unified/resource/document.go index 4e528b96df0..6a41689c0da 100644 --- a/pkg/storage/unified/resource/document.go +++ b/pkg/storage/unified/resource/document.go @@ -290,7 +290,6 @@ const SEARCH_FIELD_NAMESPACE = "namespace" const SEARCH_FIELD_NAME = "name" const SEARCH_FIELD_RV = "rv" const SEARCH_FIELD_TITLE = "title" -const SEARCH_FIELD_TITLE_NGRAM = "title_ngram" const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase const SEARCH_FIELD_DESCRIPTION = "description" const SEARCH_FIELD_TAGS = "tags" diff --git a/pkg/storage/unified/resourcepb/search.pb.go b/pkg/storage/unified/resourcepb/search.pb.go index 459e9aa3429..e523c112093 100644 --- a/pkg/storage/unified/resourcepb/search.pb.go +++ b/pkg/storage/unified/resourcepb/search.pb.go @@ -21,6 +21,65 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// This controls what query and analyzers are applied to the specified field +// See: https://blevesearch.com/docs/Analyzers/ +type QueryFieldType int32 + +const ( + // Picks a reasonable analyzer given the input. Currently this always uses TEXT + // In the future, it may change to depend on the indexed field type + QueryFieldType_DEFAULT QueryFieldType = 0 + // Use free text analyzer. The query is broken into a normalized set of tokens + QueryFieldType_TEXT QueryFieldType = 1 + // The query must exactly match the indexed token + QueryFieldType_KEYWORD QueryFieldType = 2 + // Like a text query, but the position and offsets influence the score + QueryFieldType_PHRASE QueryFieldType = 3 +) + +// Enum value maps for QueryFieldType. +var ( + QueryFieldType_name = map[int32]string{ + 0: "DEFAULT", + 1: "TEXT", + 2: "KEYWORD", + 3: "PHRASE", + } + QueryFieldType_value = map[string]int32{ + "DEFAULT": 0, + "TEXT": 1, + "KEYWORD": 2, + "PHRASE": 3, + } +) + +func (x QueryFieldType) Enum() *QueryFieldType { + p := new(QueryFieldType) + *p = x + return p +} + +func (x QueryFieldType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryFieldType) Descriptor() protoreflect.EnumDescriptor { + return file_search_proto_enumTypes[0].Descriptor() +} + +func (QueryFieldType) Type() protoreflect.EnumType { + return &file_search_proto_enumTypes[0] +} + +func (x QueryFieldType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryFieldType.Descriptor instead. +func (QueryFieldType) EnumDescriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{0} +} + // Get statistics across multiple resources // For these queries, we do not need authorization to see the actual values type ResourceStatsRequest struct { @@ -165,10 +224,12 @@ type ResourceSearchRequest struct { // the return fields (empty will return everything) Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"` // explain each result (added to the each row) - Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` - IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` - Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` - Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` + Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` + IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` + Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` + Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` + // Optionally specify which fields are included in the query + QueryFields []*ResourceSearchRequest_QueryField `protobuf:"bytes,13,rep,name=query_fields,json=queryFields,proto3" json:"query_fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -287,6 +348,13 @@ func (x *ResourceSearchRequest) GetPermission() int64 { return 0 } +func (x *ResourceSearchRequest) GetQueryFields() []*ResourceSearchRequest_QueryField { + if x != nil { + return x.QueryFields + } + return nil +} + type ResourceSearchResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -670,6 +738,70 @@ func (x *ResourceSearchRequest_Facet) GetLimit() int64 { return 0 } +// Defines the field in the index to query +// Boost is optional, and allows weighting the field higher in the results +type ResourceSearchRequest_QueryField struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The field name in the index to query + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type QueryFieldType `protobuf:"varint,2,opt,name=type,proto3,enum=resource.QueryFieldType" json:"type,omitempty"` + // Boost value for this field + Boost float32 `protobuf:"fixed32,3,opt,name=boost,proto3" json:"boost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceSearchRequest_QueryField) Reset() { + *x = ResourceSearchRequest_QueryField{} + mi := &file_search_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceSearchRequest_QueryField) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceSearchRequest_QueryField) ProtoMessage() {} + +func (x *ResourceSearchRequest_QueryField) ProtoReflect() protoreflect.Message { + mi := &file_search_proto_msgTypes[9] + 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 ResourceSearchRequest_QueryField.ProtoReflect.Descriptor instead. +func (*ResourceSearchRequest_QueryField) Descriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{2, 2} +} + +func (x *ResourceSearchRequest_QueryField) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ResourceSearchRequest_QueryField) GetType() QueryFieldType { + if x != nil { + return x.Type + } + return QueryFieldType_DEFAULT +} + +func (x *ResourceSearchRequest_QueryField) GetBoost() float32 { + if x != nil { + return x.Boost + } + return 0 +} + type ResourceSearchResponse_Facet struct { state protoimpl.MessageState `protogen:"open.v1"` Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` @@ -685,7 +817,7 @@ type ResourceSearchResponse_Facet struct { func (x *ResourceSearchResponse_Facet) Reset() { *x = ResourceSearchResponse_Facet{} - mi := &file_search_proto_msgTypes[10] + mi := &file_search_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -697,7 +829,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[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -751,7 +883,7 @@ type ResourceSearchResponse_TermFacet struct { func (x *ResourceSearchResponse_TermFacet) Reset() { *x = ResourceSearchResponse_TermFacet{} - mi := &file_search_proto_msgTypes[11] + mi := &file_search_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -763,7 +895,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[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -818,7 +950,7 @@ var file_search_proto_rawDesc = string([]byte{ 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0x8e, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x22, 0xc3, 0x06, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, @@ -846,93 +978,109 @@ var file_search_proto_rawDesc = string([]byte{ 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, - 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, - 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2a, 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, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, - 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63, - 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, - 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, - 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x74, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0b, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x1a, 0x64, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, + 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 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, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 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, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66, - 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 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, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, - 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, - 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a, - 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 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, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, + 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, + 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 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, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, + 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, + 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 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, 0x54, 0x65, 0x72, 0x6d, 0x46, + 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, + 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x3c, 0x0a, 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, 0x2a, 0x40, 0x0a, 0x0e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, + 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54, + 0x45, 0x58, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x45, 0x59, 0x57, 0x4f, 0x52, 0x44, + 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x48, 0x52, 0x41, 0x53, 0x45, 0x10, 0x03, 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, }) var ( @@ -947,53 +1095,58 @@ func file_search_proto_rawDescGZIP() []byte { return file_search_proto_rawDescData } -var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_search_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 14) 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 + (QueryFieldType)(0), // 0: resource.QueryFieldType + (*ResourceStatsRequest)(nil), // 1: resource.ResourceStatsRequest + (*ResourceStatsResponse)(nil), // 2: resource.ResourceStatsResponse + (*ResourceSearchRequest)(nil), // 3: resource.ResourceSearchRequest + (*ResourceSearchResponse)(nil), // 4: resource.ResourceSearchResponse + (*RebuildIndexesRequest)(nil), // 5: resource.RebuildIndexesRequest + (*RebuildIndexesResponse)(nil), // 6: resource.RebuildIndexesResponse + (*ResourceStatsResponse_Stats)(nil), // 7: resource.ResourceStatsResponse.Stats + (*ResourceSearchRequest_Sort)(nil), // 8: resource.ResourceSearchRequest.Sort + (*ResourceSearchRequest_Facet)(nil), // 9: resource.ResourceSearchRequest.Facet + (*ResourceSearchRequest_QueryField)(nil), // 10: resource.ResourceSearchRequest.QueryField + nil, // 11: resource.ResourceSearchRequest.FacetEntry + (*ResourceSearchResponse_Facet)(nil), // 12: resource.ResourceSearchResponse.Facet + (*ResourceSearchResponse_TermFacet)(nil), // 13: resource.ResourceSearchResponse.TermFacet + nil, // 14: resource.ResourceSearchResponse.FacetEntry + (*ErrorResult)(nil), // 15: resource.ErrorResult + (*ListOptions)(nil), // 16: resource.ListOptions + (*ResourceKey)(nil), // 17: resource.ResourceKey + (*ResourceTable)(nil), // 18: 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 + 15, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult + 7, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats + 16, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions + 17, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey + 8, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort + 11, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry + 10, // 6: resource.ResourceSearchRequest.query_fields:type_name -> resource.ResourceSearchRequest.QueryField + 15, // 7: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult + 17, // 8: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey + 18, // 9: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable + 14, // 10: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry + 17, // 11: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey + 15, // 12: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult + 0, // 13: resource.ResourceSearchRequest.QueryField.type:type_name -> resource.QueryFieldType + 9, // 14: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet + 13, // 15: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet + 12, // 16: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet + 3, // 17: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest + 1, // 18: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest + 5, // 19: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest + 4, // 20: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse + 2, // 21: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse + 6, // 22: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse + 20, // [20:23] is the sub-list for method output_type + 17, // [17:20] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_search_proto_init() } @@ -1007,13 +1160,14 @@ func file_search_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)), - NumEnums: 0, - NumMessages: 13, + NumEnums: 1, + NumMessages: 14, NumExtensions: 0, NumServices: 1, }, GoTypes: file_search_proto_goTypes, DependencyIndexes: file_search_proto_depIdxs, + EnumInfos: file_search_proto_enumTypes, MessageInfos: file_search_proto_msgTypes, }.Build() File_search_proto = out.File diff --git a/pkg/storage/unified/resourcepb/search_grpc.pb.go b/pkg/storage/unified/resourcepb/search_grpc.pb.go index d69cbd14e38..d8db878ef55 100644 --- a/pkg/storage/unified/resourcepb/search_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/search_grpc.pb.go @@ -31,9 +31,11 @@ const ( // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexClient interface { + // Query for documents 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) + // Rebuild the search index RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) } @@ -82,9 +84,11 @@ func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildInd // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexServer interface { + // Query for documents Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error) // Get the resource stats GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) + // Rebuild the search index RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index a988b71aa38..785d81af3c8 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -1182,6 +1182,7 @@ func (b *bleveIndex) getIndex( return b.index, nil } +// nolint:gocyclo func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resourcepb.ErrorResult) { ctx, span := tracer.Start(ctx, "search.bleveIndex.toBleveSearchRequest") defer span.End() @@ -1240,42 +1241,62 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R } } - if len(req.Query) > 1 && strings.Contains(req.Query, "*") { - // wildcard query is expensive - should be used with caution - wildcard := bleve.NewWildcardQuery(req.Query) - queries = append(queries, wildcard) - } + if len(req.Query) > 1 { + if strings.Contains(req.Query, "*") { + // wildcard query is expensive - should be used with caution + wildcard := bleve.NewWildcardQuery(req.Query) + queries = append(queries, wildcard) + } else { + // When using a + searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) + disjoin := bleve.NewDisjunctionQuery() + queries = append(queries, disjoin) - if req.Query != "" && !strings.Contains(req.Query, "*") { - // Add a text query - searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) + queryFields := req.QueryFields + if len(queryFields) == 0 { + queryFields = []*resourcepb.ResourceSearchRequest_QueryField{ + { + Name: resource.SEARCH_FIELD_TITLE, + Type: resourcepb.QueryFieldType_KEYWORD, + Boost: 10, // exact match -- includes ngrams! If they lived on their own field, we could score them differently + }, { + Name: resource.SEARCH_FIELD_TITLE, + Type: resourcepb.QueryFieldType_TEXT, + Boost: 2, // standard analyzer (with ngrams!) + }, { + Name: resource.SEARCH_FIELD_TITLE_PHRASE, + Type: resourcepb.QueryFieldType_TEXT, + Boost: 5, // standard analyzer + }, + } + } - // There are multiple ways to match the query string to documents. The following queries are ordered by priority: + for _, field := range queryFields { + switch field.Type { + case resourcepb.QueryFieldType_TEXT, resourcepb.QueryFieldType_DEFAULT: + q := bleve.NewMatchQuery(removeSmallTerms(req.Query)) // removeSmallTerms should be part of the analyzer + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = standard.Name // analyze the text + q.Operator = query.MatchQueryOperatorAnd // all terms must match + disjoin.AddQuery(q) - // Query 1: Match the exact query string - queryExact := bleve.NewMatchQuery(req.Query) - queryExact.SetBoost(10.0) - queryExact.SetField(resource.SEARCH_FIELD_TITLE) - queryExact.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token - queryExact.Operator = query.MatchQueryOperatorAnd // This doesn't make a difference for keyword analyzer, we add it just to be explicit. - searchQuery := bleve.NewDisjunctionQuery(queryExact) + case resourcepb.QueryFieldType_KEYWORD: + q := bleve.NewMatchQuery(req.Query) + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token + disjoin.AddQuery(q) - // Query 2: Phrase query with standard analyzer - queryPhrase := bleve.NewMatchPhraseQuery(req.Query) - queryPhrase.SetBoost(5.0) - queryPhrase.SetField(resource.SEARCH_FIELD_TITLE) - queryPhrase.Analyzer = standard.Name - searchQuery.AddQuery(queryPhrase) - - // Query 3: Match query with standard analyzer - queryAnalyzed := bleve.NewMatchQuery(removeSmallTerms(req.Query)) - queryAnalyzed.SetField(resource.SEARCH_FIELD_TITLE) - queryAnalyzed.SetBoost(2.0) - queryAnalyzed.Analyzer = standard.Name - queryAnalyzed.Operator = query.MatchQueryOperatorAnd // Make sure all terms from the query are matched - searchQuery.AddQuery(queryAnalyzed) - - queries = append(queries, searchQuery) + case resourcepb.QueryFieldType_PHRASE: + q := bleve.NewMatchPhraseQuery(req.Query) + q.SetBoost(float64(field.Boost)) + q.SetField(field.Name) + q.Analyzer = standard.Name + disjoin.AddQuery(q) + } + } + } } switch len(queries) { diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 2227c67287e..df03e6a9670 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -97,7 +97,7 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { require.Equal(t, 16, fileCount, "file count from %s", devenv) // Helper to call search - callSearch := func(user apis.User, params string) dashboardV0.SearchResults { + callSearch := func(user apis.User, params map[string]string) dashboardV0.SearchResults { require.NotNil(t, user) ns := user.Identity.GetNamespace() cfg := dynamic.ConfigFor(user.NewRestConfig()) @@ -107,17 +107,12 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { var statusCode int req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search"). + //Param("explain", "true") // helpful to understand which field made things match Param("limit", "1000"). Param("type", "dashboard") // Only search dashboards - for kv := range strings.SplitSeq(params, "&") { - if kv == "" { - continue - } - parts := strings.SplitN(kv, "=", 2) - if len(parts) == 2 { - req = req.Param(parts[0], parts[1]) - } + for k, v := range params { + req = req.Param(k, v) } res := req.Do(ctx).StatusCode(&statusCode) require.NoError(t, res.Error()) @@ -140,22 +135,47 @@ func TestIntegrationSearchDevDashboards(t *testing.T) { testCases := []struct { name string user apis.User - params string + params map[string]string }{ { - name: "all", - user: helper.Org1.Admin, - params: "", // only dashboards + name: "all", + user: helper.Org1.Admin, }, { - name: "simple-query", - user: helper.Org1.Admin, - params: "query=stacking", + name: "query-single-word", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "stacking", + }, }, { - name: "with-text-panel", - user: helper.Org1.Admin, - params: "field=panel_types&panelType=text", + name: "query-multiple-words", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "graph softMin", // must match ALL terms + }, + }, + { + name: "with-text-panel", + user: helper.Org1.Admin, + params: map[string]string{ + "field": "panel_types", // return panel types + "panelType": "text", + }, + }, + { + name: "title-ngram-prefix", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "zer", // should match "Zero Decimals Y Ticks" + }, + }, + { + name: "title-ngram-middle-word", + user: helper.Org1.Admin, + params: map[string]string{ + "query": "decim", // should match "Zero Decimals Y Ticks" + }, }, } for i, tc := range testCases { diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json b/pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json similarity index 88% rename from pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json rename to pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json index 6c9a935dfe8..02eed11383a 100644 --- a/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t01-query-single-word.json @@ -10,7 +10,7 @@ "panel-tests", "graph-ng" ], - "score": 0.658 + "score": 0.284 }, { "resource": "dashboards", @@ -21,8 +21,8 @@ "panel-tests", "graph-ng" ], - "score": 0.625 + "score": 0.269 } ], - "maxScore": 0.658 + "maxScore": 0.284 } \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json b/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json new file mode 100644 index 00000000000..270801994c0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-soft-limits", + "title": "Panel Tests - Graph NG - softMin/softMax", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.024 + } + ], + "maxScore": 0.024 +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json b/pkg/tests/apis/dashboard/testdata/searchV0/t03-with-text-panel.json similarity index 100% rename from pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json rename to pkg/tests/apis/dashboard/testdata/searchV0/t03-with-text-panel.json diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json b/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json new file mode 100644 index 00000000000..8059db130a0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-y-ticks-zero-decimals", + "title": "Zero Decimals Y Ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.35 + } + ], + "maxScore": 0.35 +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json b/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json new file mode 100644 index 00000000000..8059db130a0 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json @@ -0,0 +1,17 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-y-ticks-zero-decimals", + "title": "Zero Decimals Y Ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.35 + } + ], + "maxScore": 0.35 +} \ No newline at end of file From 8bad33de4c9f1354643ef3d2af4f4c4bf7e14254 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 14 Jan 2026 13:05:23 +0100 Subject: [PATCH 10/13] Grafana/data: Fix theme types schema resolution (#116240) * fix(grafana-data): copy theme schema json to types so declaration resolves * refactor(grafana-data): move node scripts out of source code * feat(grafana-data): generate types for theme schema * chore(codeowners): update for grafana-data/scripts file move * feat(grafana-data): put back copy plugin for theme json files * revert(grafana-data): remove definition output * feat(grafana-data): make builds great again * minor tidy up --------- Co-authored-by: Ashley Harrison --- .github/CODEOWNERS | 1 + packages/grafana-data/package.json | 11 ++++++- packages/grafana-data/rollup.config.ts | 23 +++++++++++-- .../grafana-data/scripts/generateSchema.ts | 22 +++++++++++++ packages/grafana-data/src/internal/index.ts | 1 - packages/grafana-data/src/themes/registry.ts | 28 +++++++++++++++- .../src/themes/scripts/generateSchema.ts | 19 ----------- .../src/themes/themeDefinitions/index.ts | 12 ------- packages/grafana-data/src/unstable.ts | 2 +- packages/grafana-data/tsconfig.json | 3 +- .../theme-playground/ThemePlayground.tsx | 33 +++++++++++++++++-- scripts/validate-npm-packages.sh | 1 + yarn.lock | 1 + 13 files changed, 116 insertions(+), 41 deletions(-) create mode 100644 packages/grafana-data/scripts/generateSchema.ts delete mode 100644 packages/grafana-data/src/themes/scripts/generateSchema.ts delete mode 100644 packages/grafana-data/src/themes/themeDefinitions/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0dda8519ef6..6b40b814064 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -543,6 +543,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform /packages/grafana-data/test/ @grafana/grafana-frontend-platform /packages/grafana-data/typings/ @grafana/grafana-frontend-platform +/packages/grafana-data/scripts/ @grafana/grafana-frontend-platform /packages/grafana-data/src/**/*logs* @grafana/observability-logs /packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 384666ea7f8..60db9295fb4 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -35,6 +35,14 @@ }, "./test": { "@grafana-app/source": "./test/index.ts" + }, + "./themes/schema.generated.json": { + "@grafana-app/source": "./src/themes/schema.generated.json", + "default": "./dist/esm/themes/schema.generated.json" + }, + "./themes/definitions/*.json": { + "@grafana-app/source": "./src/themes/themeDefinitions/*.json", + "default": "./dist/esm/themes/themeDefinitions/*.json" } }, "publishConfig": { @@ -52,7 +60,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json", - "themes-schema": "tsx ./src/themes/scripts/generateSchema.ts" + "themes-schema": "tsx ./scripts/generateSchema.ts" }, "dependencies": { "@braintree/sanitize-url": "7.0.1", @@ -102,6 +110,7 @@ "react-dom": "18.3.1", "rimraf": "6.0.1", "rollup": "^4.22.4", + "rollup-plugin-copy": "3.5.0", "rollup-plugin-esbuild": "6.2.1", "rollup-plugin-node-externals": "^8.0.0", "tsx": "^4.21.0", diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 0c40d731724..50af331c37c 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -1,21 +1,40 @@ import json from '@rollup/plugin-json'; import { createRequire } from 'node:module'; +import copy from 'rollup-plugin-copy'; import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts'; const rq = createRequire(import.meta.url); const pkg = rq('./package.json'); +const grafanaDataPlugins = [ + ...plugins, + copy({ + targets: [ + { + src: 'src/themes/schema.generated.json', + dest: 'dist/esm/', + }, + { + src: 'src/themes/themeDefinitions/*.json', + dest: 'dist/esm/', + }, + ], + flatten: false, + }), + json(), +]; + export default [ { input: entryPoint, - plugins: [...plugins, json()], + plugins: grafanaDataPlugins, output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, { input: 'src/unstable.ts', - plugins: [...plugins, json()], + plugins: grafanaDataPlugins, output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')], treeshake: false, }, diff --git a/packages/grafana-data/scripts/generateSchema.ts b/packages/grafana-data/scripts/generateSchema.ts new file mode 100644 index 00000000000..f461999376e --- /dev/null +++ b/packages/grafana-data/scripts/generateSchema.ts @@ -0,0 +1,22 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { NewThemeOptionsSchema } from '../src/themes/createTheme'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const jsonOut = path.join(__dirname, '..', 'src', 'themes', 'schema.generated.json'); + +fs.writeFileSync( + jsonOut, + JSON.stringify( + NewThemeOptionsSchema.toJSONSchema({ + target: 'draft-07', + }), + undefined, + 2 + ) +); + +console.log('Successfully generated theme schema'); diff --git a/packages/grafana-data/src/internal/index.ts b/packages/grafana-data/src/internal/index.ts index 1b1e3c64a7d..230cdd2cbf9 100644 --- a/packages/grafana-data/src/internal/index.ts +++ b/packages/grafana-data/src/internal/index.ts @@ -93,7 +93,6 @@ export { DataTransformerID } from '../transformations/transformers/ids'; export { mergeTransformer } from '../transformations/transformers/merge'; export { getThemeById } from '../themes/registry'; -export * as experimentalThemeDefinitions from '../themes/themeDefinitions'; export { GrafanaEdition } from '../types/config'; export { SIPrefix } from '../valueFormats/symbolFormatters'; diff --git a/packages/grafana-data/src/themes/registry.ts b/packages/grafana-data/src/themes/registry.ts index 4fca3c5d7be..cfa4a10c6e4 100644 --- a/packages/grafana-data/src/themes/registry.ts +++ b/packages/grafana-data/src/themes/registry.ts @@ -1,7 +1,18 @@ import { Registry, RegistryItem } from '../utils/Registry'; import { createTheme, NewThemeOptionsSchema } from './createTheme'; -import * as extraThemes from './themeDefinitions'; +import aubergine from './themeDefinitions/aubergine.json'; +import debug from './themeDefinitions/debug.json'; +import desertbloom from './themeDefinitions/desertbloom.json'; +import gildedgrove from './themeDefinitions/gildedgrove.json'; +import gloom from './themeDefinitions/gloom.json'; +import mars from './themeDefinitions/mars.json'; +import matrix from './themeDefinitions/matrix.json'; +import sapphiredusk from './themeDefinitions/sapphiredusk.json'; +import synthwave from './themeDefinitions/synthwave.json'; +import tron from './themeDefinitions/tron.json'; +import victorian from './themeDefinitions/victorian.json'; +import zen from './themeDefinitions/zen.json'; import { GrafanaTheme2 } from './types'; export interface ThemeRegistryItem extends RegistryItem { @@ -9,6 +20,21 @@ export interface ThemeRegistryItem extends RegistryItem { build: () => GrafanaTheme2; } +const extraThemes: { [key: string]: unknown } = { + aubergine, + debug, + desertbloom, + gildedgrove, + gloom, + mars, + matrix, + sapphiredusk, + synthwave, + tron, + victorian, + zen, +}; + /** * @internal * Only for internal use, never use this from a plugin diff --git a/packages/grafana-data/src/themes/scripts/generateSchema.ts b/packages/grafana-data/src/themes/scripts/generateSchema.ts deleted file mode 100644 index 09369f5e67f..00000000000 --- a/packages/grafana-data/src/themes/scripts/generateSchema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -import { NewThemeOptionsSchema } from '../createTheme'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -fs.writeFileSync( - path.join(__dirname, '../schema.generated.json'), - JSON.stringify( - NewThemeOptionsSchema.toJSONSchema({ - target: 'draft-07', - }), - undefined, - 2 - ) -); diff --git a/packages/grafana-data/src/themes/themeDefinitions/index.ts b/packages/grafana-data/src/themes/themeDefinitions/index.ts deleted file mode 100644 index b4270192032..00000000000 --- a/packages/grafana-data/src/themes/themeDefinitions/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { default as aubergine } from './aubergine.json'; -export { default as debug } from './debug.json'; -export { default as desertbloom } from './desertbloom.json'; -export { default as gildedgrove } from './gildedgrove.json'; -export { default as mars } from './mars.json'; -export { default as matrix } from './matrix.json'; -export { default as sapphiredusk } from './sapphiredusk.json'; -export { default as synthwave } from './synthwave.json'; -export { default as tron } from './tron.json'; -export { default as victorian } from './victorian.json'; -export { default as zen } from './zen.json'; -export { default as gloom } from './gloom.json'; diff --git a/packages/grafana-data/src/unstable.ts b/packages/grafana-data/src/unstable.ts index 3200085428a..43c2ff3071f 100644 --- a/packages/grafana-data/src/unstable.ts +++ b/packages/grafana-data/src/unstable.ts @@ -9,4 +9,4 @@ * and be subject to the standard policies */ -export { default as themeJsonSchema } from './themes/schema.generated.json'; +export {}; diff --git a/packages/grafana-data/tsconfig.json b/packages/grafana-data/tsconfig.json index 8e6013e32d9..3513caf9127 100644 --- a/packages/grafana-data/tsconfig.json +++ b/packages/grafana-data/tsconfig.json @@ -8,7 +8,8 @@ "emitDeclarationOnly": true, "isolatedModules": true, "rootDirs": ["."], - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "resolveJsonModule": true }, "exclude": ["dist/**/*"], "include": [ diff --git a/public/app/features/theme-playground/ThemePlayground.tsx b/public/app/features/theme-playground/ThemePlayground.tsx index 85dee240c7f..d331f3932bc 100644 --- a/public/app/features/theme-playground/ThemePlayground.tsx +++ b/public/app/features/theme-playground/ThemePlayground.tsx @@ -2,8 +2,20 @@ import { css } from '@emotion/css'; import { useId, useState } from 'react'; import { createTheme, GrafanaTheme2, NewThemeOptions } from '@grafana/data'; -import { experimentalThemeDefinitions, NewThemeOptionsSchema } from '@grafana/data/internal'; -import { themeJsonSchema } from '@grafana/data/unstable'; +import { NewThemeOptionsSchema } from '@grafana/data/internal'; +import aubergine from '@grafana/data/themes/definitions/aubergine.json'; +import debug from '@grafana/data/themes/definitions/debug.json'; +import desertbloom from '@grafana/data/themes/definitions/desertbloom.json'; +import gildedgrove from '@grafana/data/themes/definitions/gildedgrove.json'; +import gloom from '@grafana/data/themes/definitions/gloom.json'; +import mars from '@grafana/data/themes/definitions/mars.json'; +import matrix from '@grafana/data/themes/definitions/matrix.json'; +import sapphiredusk from '@grafana/data/themes/definitions/sapphiredusk.json'; +import synthwave from '@grafana/data/themes/definitions/synthwave.json'; +import tron from '@grafana/data/themes/definitions/tron.json'; +import victorian from '@grafana/data/themes/definitions/victorian.json'; +import zen from '@grafana/data/themes/definitions/zen.json'; +import themeJsonSchema from '@grafana/data/themes/schema.generated.json'; import { t } from '@grafana/i18n'; import { useChromeHeaderHeight } from '@grafana/runtime'; import { CodeEditor, Combobox, Field, Stack, useStyles2 } from '@grafana/ui'; @@ -34,8 +46,23 @@ const themeMap: Record = { }, }; +const experimentalDefinitions: Record = { + aubergine, + debug, + desertbloom, + gildedgrove, + gloom, + mars, + matrix, + sapphiredusk, + synthwave, + tron, + victorian, + zen, +}; + // Add additional themes -for (const [name, json] of Object.entries(experimentalThemeDefinitions)) { +for (const [name, json] of Object.entries(experimentalDefinitions)) { const result = NewThemeOptionsSchema.safeParse(json); if (!result.success) { console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`); diff --git a/scripts/validate-npm-packages.sh b/scripts/validate-npm-packages.sh index a3e07c4d7be..80e36b968d1 100755 --- a/scripts/validate-npm-packages.sh +++ b/scripts/validate-npm-packages.sh @@ -11,6 +11,7 @@ failed_checks=() for file in "$ARTIFACTS_DIR"/*.tgz; do echo "🔍 Checking NPM package: $file" + # If you need to debug ATTW issues, pass "--format json" to get verbose output. if ! NODE_OPTIONS="-C @grafana-app/source" yarn attw "$file" --ignore-rules "false-cjs" --profile "node16"; then echo "attw check failed for $file" echo "" diff --git a/yarn.lock b/yarn.lock index 1069acd8544..d16b10ef5f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3324,6 +3324,7 @@ __metadata: react-use: "npm:17.6.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" + rollup-plugin-copy: "npm:3.5.0" rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-node-externals: "npm:^8.0.0" rxjs: "npm:7.8.2" From 48625d67e5adffd276f4bf19227a50523ae50d54 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 14 Jan 2026 15:15:19 +0300 Subject: [PATCH 11/13] Chore: update blevesearch dependencies (#116251) --- go.mod | 23 +++++++-------- go.sum | 46 ++++++++++++++--------------- go.work.sum | 29 ++++++++++++++++-- pkg/storage/unified/search/bleve.go | 2 +- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index fe3e62e3fde..ade26f2e7d1 100644 --- a/go.mod +++ b/go.mod @@ -44,8 +44,8 @@ require ( github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad - github.com/blevesearch/bleve/v2 v2.5.0 // @grafana/grafana-search-and-storage - github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve/v2 v2.5.7 // @grafana/grafana-search-and-storage + github.com/blevesearch/bleve_index_api v1.3.0 // @grafana/grafana-search-and-storage github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // @grafana/grafana-backend-group @@ -365,22 +365,22 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect - github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.25 // indirect + github.com/blevesearch/geo v0.2.4 // indirect + github.com/blevesearch/go-faiss v1.0.26 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/vellum v1.1.0 // indirect - github.com/blevesearch/zapx/v11 v11.4.1 // indirect - github.com/blevesearch/zapx/v12 v12.4.1 // indirect - github.com/blevesearch/zapx/v13 v13.4.1 // indirect - github.com/blevesearch/zapx/v14 v14.4.1 // indirect - github.com/blevesearch/zapx/v15 v15.4.1 // indirect - github.com/blevesearch/zapx/v16 v16.2.2 // indirect + github.com/blevesearch/zapx/v11 v11.4.2 // indirect + github.com/blevesearch/zapx/v12 v12.4.2 // indirect + github.com/blevesearch/zapx/v13 v13.4.2 // indirect + github.com/blevesearch/zapx/v14 v14.4.2 // indirect + github.com/blevesearch/zapx/v15 v15.4.2 // indirect + github.com/blevesearch/zapx/v16 v16.2.8 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/blugelabs/ice v1.0.0 // indirect github.com/blugelabs/ice/v2 v2.0.1 // indirect @@ -443,7 +443,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect diff --git a/go.sum b/go.sum index 67d95c625b6..f997af7c68e 100644 --- a/go.sum +++ b/go.sum @@ -931,14 +931,14 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= -github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk= -github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY= -github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= -github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= -github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= +github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= +github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E= +github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= +github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= +github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= +github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= @@ -947,8 +947,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+ github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -960,18 +960,18 @@ github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+ github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= -github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= -github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= -github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= -github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= -github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= -github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= -github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= -github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= -github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= -github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= -github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk= -github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk= +github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= +github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= +github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= +github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= +github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= +github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= +github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM= @@ -1442,8 +1442,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= diff --git a/go.work.sum b/go.work.sum index d064248a16a..6388285d2bf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -520,14 +520,40 @@ github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= +github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= +github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s= +github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E= +github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= +github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= +github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= +github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= +github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= +github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= +github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= +github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= +github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= +github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= +github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= @@ -998,8 +1024,6 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ= github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= @@ -1092,6 +1116,7 @@ github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 785d81af3c8..09ef2dc9230 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -1898,7 +1898,7 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade if err != nil { return nil, err } - filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(d *search.DocumentMatch) bool { + filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(_ *search.SearchContext, d *search.DocumentMatch) bool { // The doc ID has the format: /// // IndexInternalID will be the same as the doc ID when using an in-memory index, but when using a file-based // index it becomes a binary encoded number that has some other internal meaning. Using ExternalID() will get the From 7143324229c7808f970f2f952dbc88cb024ebccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 14 Jan 2026 07:51:42 -0500 Subject: [PATCH 12/13] Elasticsearch: Add support for serverless connections (#114855) * serverless connecction * Adding api key * fix * addressing pr comments * fixing tests * refactoring * changing to value semantic * addressing pr comments * minor changes --------- Co-authored-by: Lucas Francisco Lopez --- .../api/elasticsearch/elasticsearch_test.go | 21 +- pkg/tsdb/elasticsearch/client/client.go | 7 +- pkg/tsdb/elasticsearch/client/cluster_info.go | 51 +++++ .../elasticsearch/client/cluster_info_test.go | 188 ++++++++++++++++++ pkg/tsdb/elasticsearch/elasticsearch.go | 14 ++ pkg/tsdb/elasticsearch/elasticsearch_test.go | 57 ++++++ pkg/tsdb/elasticsearch/healthcheck.go | 9 +- .../configuration/ApiKeyConfig.tsx | 22 ++ .../configuration/ConfigEditor.tsx | 16 +- .../plugins/datasource/elasticsearch/types.ts | 5 + 10 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 pkg/tsdb/elasticsearch/client/cluster_info.go create mode 100644 pkg/tsdb/elasticsearch/client/cluster_info_test.go create mode 100644 public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 09277c944f0..651dd74e9e2 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -24,6 +24,24 @@ func TestMain(m *testing.M) { testsuite.Run(m) } +// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints. +// It responds to GET / with cluster info (required for datasource initialization) +// and returns 401 Unauthorized for all other requests. +func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`)) + default: + if onRequest != nil { + onRequest(r) + } + w.WriteHeader(http.StatusUnauthorized) + } + } +} + func TestIntegrationElasticsearch(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -35,9 +53,8 @@ func TestIntegrationElasticsearch(t *testing.T) { ctx := context.Background() var outgoingRequest *http.Request - outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) { outgoingRequest = r - w.WriteHeader(http.StatusUnauthorized) })) t.Cleanup(outgoingServer.Close) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index fbb3e09f092..49b24651609 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -35,6 +35,7 @@ type DatasourceInfo struct { Interval string MaxConcurrentShardRequests int64 IncludeFrozen bool + ClusterInfo ClusterInfo } type ConfiguredFields struct { @@ -197,7 +198,11 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque func (c *baseClientImpl) getMultiSearchQueryParameters() string { var qs []string - qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests)) + // if the build flavor is not serverless, we can use the max concurrent shard requests + // this is because serverless clusters do not support max concurrent shard requests + if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 { + qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests)) + } if c.ds.IncludeFrozen { qs = append(qs, "ignore_throttled=false") diff --git a/pkg/tsdb/elasticsearch/client/cluster_info.go b/pkg/tsdb/elasticsearch/client/cluster_info.go new file mode 100644 index 00000000000..eb89189804f --- /dev/null +++ b/pkg/tsdb/elasticsearch/client/cluster_info.go @@ -0,0 +1,51 @@ +package es + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type VersionInfo struct { + BuildFlavor string `json:"build_flavor"` +} + +// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint. +// It is used to determine cluster capabilities and configuration like whether the cluster is serverless. +type ClusterInfo struct { + Version VersionInfo `json:"version"` +} + +const ( + BuildFlavorServerless = "serverless" +) + +// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint. +// It returns the cluster build flavor which is used to determine if the cluster is serverless. +func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) { + resp, err := httpCli.Get(url) + if err != nil { + return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode) + } + + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("error closing response body: %w", closeErr) + } + }() + + err = json.NewDecoder(resp.Body).Decode(&clusterInfo) + if err != nil { + return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err) + } + + return clusterInfo, nil +} + +func (ci ClusterInfo) IsServerless() bool { + return ci.Version.BuildFlavor == BuildFlavorServerless +} diff --git a/pkg/tsdb/elasticsearch/client/cluster_info_test.go b/pkg/tsdb/elasticsearch/client/cluster_info_test.go new file mode 100644 index 00000000000..0fdcc46e813 --- /dev/null +++ b/pkg/tsdb/elasticsearch/client/cluster_info_test.go @@ -0,0 +1,188 @@ +package es + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetClusterInfo(t *testing.T) { + t.Run("Should successfully get cluster info", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "test-cluster", + "cluster_name": "elasticsearch", + "cluster_uuid": "abc123", + "version": { + "number": "8.0.0", + "build_flavor": "default", + "build_type": "tar", + "build_hash": "abc123", + "build_date": "2023-01-01T00:00:00.000Z", + "build_snapshot": false, + "lucene_version": "9.0.0" + } + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.NotNil(t, clusterInfo) + assert.Equal(t, "default", clusterInfo.Version.BuildFlavor) + }) + + t.Run("Should successfully get serverless cluster info", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "serverless-cluster", + "cluster_name": "elasticsearch", + "cluster_uuid": "def456", + "version": { + "number": "8.11.0", + "build_flavor": "serverless", + "build_type": "docker", + "build_hash": "def456", + "build_date": "2023-11-01T00:00:00.000Z", + "build_snapshot": false, + "lucene_version": "9.8.0" + } + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.NotNil(t, clusterInfo) + assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor) + assert.True(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return error when HTTP request fails", func(t *testing.T) { + clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999") + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "error getting ES cluster info") + }) + + t.Run("Should return error when response body is invalid JSON", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{"invalid json`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "error decoding ES cluster info") + }) + + t.Run("Should handle empty version object", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ + "name": "test-cluster", + "version": {} + }`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.NoError(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Equal(t, "", clusterInfo.Version.BuildFlavor) + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should handle HTTP error status codes", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusUnauthorized) + _, err := rw.Write([]byte(`{"error": "Unauthorized"}`)) + require.NoError(t, err) + })) + + t.Cleanup(func() { + ts.Close() + }) + + clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL) + + require.Error(t, err) + require.Equal(t, ClusterInfo{}, clusterInfo) + assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info") + }) +} + +func TestClusterInfo_IsServerless(t *testing.T) { + t.Run("Should return true when build_flavor is serverless", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: BuildFlavorServerless, + }, + } + + assert.True(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is default", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "default", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is empty", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) { + clusterInfo := ClusterInfo{ + Version: VersionInfo{ + BuildFlavor: "unknown", + }, + } + + assert.False(t, clusterInfo.IsServerless()) + }) + + t.Run("should return false when cluster info is empty", func(t *testing.T) { + clusterInfo := ClusterInfo{} + assert.False(t, clusterInfo.IsServerless()) + }) +} diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index 40073bf1740..0432bbcee20 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -88,6 +88,14 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins httpCliOpts.SigV4.Service = "es" } + apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool) + if ok && apiKeyAuth { + apiKey := settings.DecryptedSecureJSONData["apiKey"] + if apiKey != "" { + httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey) + } + } + httpCli, err := httpClientProvider.New(httpCliOpts) if err != nil { return nil, err @@ -151,6 +159,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins includeFrozen = false } + clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL) + if err != nil { + return nil, err + } + configuredFields := es.ConfiguredFields{ TimeField: timeField, LogLevelField: logLevelField, @@ -166,6 +179,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins ConfiguredFields: configuredFields, Interval: interval, IncludeFrozen: includeFrozen, + ClusterInfo: clusterInfo, } return model, nil } diff --git a/pkg/tsdb/elasticsearch/elasticsearch_test.go b/pkg/tsdb/elasticsearch/elasticsearch_test.go index 8ab3cabc7e5..35ec1f814ce 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch_test.go +++ b/pkg/tsdb/elasticsearch/elasticsearch_test.go @@ -3,6 +3,8 @@ package elasticsearch import ( "context" "encoding/json" + "net/http" + "net/http/httptest" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -18,8 +20,26 @@ type datasourceInfo struct { Interval string `json:"interval"` } +// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint +func mockElasticsearchServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // Return a mock Elasticsearch cluster info response + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "version": map[string]interface{}{ + "build_flavor": "serverless", + "number": "8.0.0", + }, + }) + })) +} + func TestNewInstanceSettings(t *testing.T) { t.Run("fields exist", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 5, @@ -28,6 +48,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -37,6 +58,9 @@ func TestNewInstanceSettings(t *testing.T) { t.Run("timeField", func(t *testing.T) { t.Run("is nil", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ MaxConcurrentShardRequests: 5, Interval: "Daily", @@ -46,6 +70,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -54,6 +79,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("is empty", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ MaxConcurrentShardRequests: 5, Interval: "Daily", @@ -64,6 +92,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -74,6 +103,9 @@ func TestNewInstanceSettings(t *testing.T) { t.Run("maxConcurrentShardRequests", func(t *testing.T) { t.Run("no maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", } @@ -81,6 +113,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -90,6 +123,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("string maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: "10", @@ -98,6 +134,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -107,6 +144,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("number maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 10, @@ -115,6 +155,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -124,6 +165,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("zero maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 0, @@ -132,6 +176,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -141,6 +186,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("negative maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: -10, @@ -149,6 +197,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -158,6 +207,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("float maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: 10.5, @@ -166,6 +218,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } @@ -175,6 +228,9 @@ func TestNewInstanceSettings(t *testing.T) { }) t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) { + server := mockElasticsearchServer() + defer server.Close() + dsInfo := datasourceInfo{ TimeField: "@timestamp", MaxConcurrentShardRequests: "invalid", @@ -183,6 +239,7 @@ func TestNewInstanceSettings(t *testing.T) { require.NoError(t, err) dsSettings := backend.DataSourceInstanceSettings{ + URL: server.URL, JSONData: json.RawMessage(settingsJSON), } diff --git a/pkg/tsdb/elasticsearch/healthcheck.go b/pkg/tsdb/elasticsearch/healthcheck.go index 928945691de..cb5d0a866db 100644 --- a/pkg/tsdb/elasticsearch/healthcheck.go +++ b/pkg/tsdb/elasticsearch/healthcheck.go @@ -28,7 +28,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque Message: "Health check failed: Failed to get data source info", }, nil } - healthStatusUrl, err := url.Parse(ds.URL) if err != nil { logger.Error("Failed to parse data source URL", "error", err) @@ -38,6 +37,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque }, nil } + // If the cluster is serverless, return a healthy result + if ds.ClusterInfo.IsServerless() { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Elasticsearch Serverless data source is healthy.", + }, nil + } + // check that ES is healthy healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health") healthStatusUrl.RawQuery = "wait_for_status=yellow" diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx new file mode 100644 index 00000000000..8433160d4f2 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ApiKeyConfig.tsx @@ -0,0 +1,22 @@ +import { onUpdateDatasourceSecureJsonDataOption, updateDatasourcePluginResetOption } from '@grafana/data'; +import { InlineField, SecretInput } from '@grafana/ui'; + +import { Props } from './ConfigEditor'; + +export const ApiKeyConfig = (props: Props) => { + const { options } = props; + + return ( + + updateDatasourcePluginResetOption(props, 'apiKey')} + onChange={onUpdateDatasourceSecureJsonDataOption(props, 'apiKey')} + /> + + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx index 5961ebef510..57e3f8dc92a 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -14,14 +14,15 @@ import { import { config } from '@grafana/runtime'; import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui'; -import { ElasticsearchOptions } from '../types'; +import { ElasticsearchOptions, ElasticsearchSecureJsonData } from '../types'; +import { ApiKeyConfig } from './ApiKeyConfig'; import { DataLinks } from './DataLinks'; import { ElasticDetails } from './ElasticDetails'; import { LogsConfig } from './LogsConfig'; import { coerceOptions, isValidOptions } from './utils'; -export type Props = DataSourcePluginOptionsEditorProps; +export type Props = DataSourcePluginOptionsEditorProps; export const ConfigEditor = (props: Props) => { const { options, onOptionsChange } = props; @@ -48,6 +49,16 @@ export const ConfigEditor = (props: Props) => { authProps.selectedMethod = options.jsonData.sigV4Auth ? 'custom-sigv4' : authProps.selectedMethod; } + authProps.customMethods = [ + { + id: 'custom-api-key', + label: 'API Key', + description: 'API Key authentication', + component: , + }, + ]; + authProps.selectedMethod = options.jsonData.apiKeyAuth ? 'custom-api-key' : authProps.selectedMethod; + return ( <> {options.access === 'direct' && ( @@ -73,6 +84,7 @@ export const ConfigEditor = (props: Props) => { jsonData: { ...options.jsonData, sigV4Auth: method === 'custom-sigv4', + apiKeyAuth: method === 'custom-api-key', oauthPassThru: method === AuthMethod.OAuthForward, }, }); diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts index 4645a2a824f..d435f1b4594 100644 --- a/public/app/plugins/datasource/elasticsearch/types.ts +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -64,6 +64,11 @@ export interface ElasticsearchOptions extends DataSourceJsonData { sigV4Auth?: boolean; oauthPassThru?: boolean; defaultQueryMode?: QueryType; + apiKeyAuth?: boolean; +} + +export interface ElasticsearchSecureJsonData { + apiKey?: string; } export type QueryType = 'metrics' | 'logs' | 'raw_data' | 'raw_document'; From c1a46fdcb51135f2a6e5e77b7a874822de8d3ccd Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 14 Jan 2026 13:54:21 +0100 Subject: [PATCH 13/13] Elasticsearch: Decoupling from core (#115900) * Complete decoupling of backend - Replace usage of featuremgmt - Copy simplejson - Add standalone logic * Complete frontend decoupling - Fix imports - Copy store and reducer logic * Add required files for full decoupling * Regen cue * Prettier * Remove unneeded script * Jest fix * Add jest config * Lint * Lit * Prune suppresions --- .golangci.yml | 2 + eslint-suppressions.json | 25 - jest.config.js | 1 + .../x/ElasticsearchDataQuery_types.gen.ts | 2 +- .../api/plugins/data/expectedListResp.json | 2 +- pkg/tsdb/elasticsearch/aggregation_factory.go | 2 +- pkg/tsdb/elasticsearch/client/client.go | 3 +- pkg/tsdb/elasticsearch/client/client_test.go | 2 +- .../client/search_request_test.go | 2 +- .../elasticsearch/data_query_processor.go | 2 +- pkg/tsdb/elasticsearch/data_query_settings.go | 2 +- .../metrics_response_processor.go | 2 +- pkg/tsdb/elasticsearch/models.go | 2 +- pkg/tsdb/elasticsearch/parse_query.go | 2 +- .../raw_dsl_aggregation_parser.go | 2 +- pkg/tsdb/elasticsearch/response_parser.go | 2 +- pkg/tsdb/elasticsearch/response_utils.go | 2 +- .../elasticsearch/simplejson/simplejson.go | 582 ++++++++++++++++++ .../simplejson/simplejson_go11.go | 90 +++ .../simplejson/simplejson_test.go | 274 +++++++++ .../elasticsearch/standalone/datasource.go | 48 ++ pkg/tsdb/elasticsearch/standalone/main.go | 23 + .../app/features/plugins/built_in_plugins.ts | 3 - .../datasource/elasticsearch/CHANGELOG.md | 0 .../DateHistogramSettingsEditor.test.tsx | 3 +- .../DateHistogramSettingsEditor.tsx | 6 +- .../FiltersSettingsEditor/index.tsx | 2 +- .../FiltersSettingsEditor/state/actions.ts | 2 +- .../state/reducer.test.ts | 5 +- .../FiltersSettingsEditor/state/reducer.ts | 3 +- .../FiltersSettingsEditor/utils.ts | 2 +- .../TermsSettingsEditor.test.tsx | 9 +- .../SettingsEditor/TermsSettingsEditor.tsx | 12 +- .../SettingsEditor/index.tsx | 2 +- .../SettingsEditor/useDescription.ts | 5 +- .../BucketAggregationsEditor/state/actions.ts | 6 +- .../state/reducer.test.ts | 7 +- .../BucketAggregationsEditor/state/reducer.ts | 14 +- .../BucketScriptSettingsEditor/index.tsx | 8 +- .../state/reducer.test.ts | 3 +- .../state/reducer.ts | 3 +- .../BucketScriptSettingsEditor/utils.ts | 2 +- .../SettingsEditor/SettingField.tsx | 7 +- .../TopMetricsSettingsEditor.tsx | 2 +- .../SettingsEditor/index.test.tsx | 2 +- .../SettingsEditor/index.tsx | 6 +- .../SettingsEditor/useDescription.ts | 3 +- .../MetricAggregationsEditor/state/actions.ts | 3 +- .../state/reducer.test.ts | 8 +- .../MetricAggregationsEditor/state/reducer.ts | 4 +- .../elasticsearch/components/reducerTester.ts | 2 +- .../datasource/elasticsearch/jest-setup.js | 1 + .../datasource/elasticsearch/jest.config.js | 3 + .../datasource/elasticsearch/package.json | 62 ++ .../datasource/elasticsearch/plugin.json | 8 +- .../datasource/elasticsearch/project.json | 9 + .../elasticsearch/reducers/actions/cleanUp.ts | 11 + .../datasource/elasticsearch/reducers/root.ts | 21 + .../elasticsearch/store/configureStore.ts | 47 ++ .../datasource/elasticsearch/store/store.ts | 26 + .../datasource/elasticsearch/tsconfig.json | 8 + .../datasource/elasticsearch/types/store.ts | 46 ++ .../elasticsearch/webpack.config.ts | 9 + yarn.lock | 47 ++ 64 files changed, 1378 insertions(+), 128 deletions(-) create mode 100644 pkg/tsdb/elasticsearch/simplejson/simplejson.go create mode 100644 pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go create mode 100644 pkg/tsdb/elasticsearch/simplejson/simplejson_test.go create mode 100644 pkg/tsdb/elasticsearch/standalone/datasource.go create mode 100644 pkg/tsdb/elasticsearch/standalone/main.go create mode 100644 public/app/plugins/datasource/elasticsearch/CHANGELOG.md create mode 100644 public/app/plugins/datasource/elasticsearch/jest-setup.js create mode 100644 public/app/plugins/datasource/elasticsearch/jest.config.js create mode 100644 public/app/plugins/datasource/elasticsearch/package.json create mode 100644 public/app/plugins/datasource/elasticsearch/project.json create mode 100644 public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts create mode 100644 public/app/plugins/datasource/elasticsearch/reducers/root.ts create mode 100644 public/app/plugins/datasource/elasticsearch/store/configureStore.ts create mode 100644 public/app/plugins/datasource/elasticsearch/store/store.ts create mode 100644 public/app/plugins/datasource/elasticsearch/tsconfig.json create mode 100644 public/app/plugins/datasource/elasticsearch/types/store.ts create mode 100644 public/app/plugins/datasource/elasticsearch/webpack.config.ts diff --git a/.golangci.yml b/.golangci.yml index 069e88632ff..d7037bf6fac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -121,6 +121,8 @@ linters: - '**/pkg/tsdb/zipkin/**/*' - '**/pkg/tsdb/jaeger/*' - '**/pkg/tsdb/jaeger/**/*' + - '**/pkg/tsdb/elasticsearch/*' + - '**/pkg/tsdb/elasticsearch/**/*' deny: - pkg: github.com/grafana/grafana/pkg/api desc: Core plugins are not allowed to depend on Grafana core packages diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 70df9a82829..25d3225375e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3743,46 +3743,21 @@ "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 2 - } - }, "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 } }, - "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/jest.config.js b/jest.config.js index 17a2ce9ca32..f9d431cf5d3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -82,6 +82,7 @@ module.exports = { // Decoupled plugins run their own tests so ignoring them here. '/public/app/plugins/datasource/azuremonitor', '/public/app/plugins/datasource/cloud-monitoring', + '/public/app/plugins/datasource/elasticsearch', '/public/app/plugins/datasource/grafana-postgresql-datasource', '/public/app/plugins/datasource/grafana-pyroscope-datasource', '/public/app/plugins/datasource/grafana-testdata-datasource', diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index 8d06591b46b..1627b2dc29b 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.4.0-pre"; +export const pluginVersion = "%VERSION%"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 3d1ccce6a59..83debc4c410 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -639,7 +639,7 @@ ] }, "dependencies": { - "grafanaDependency": "", + "grafanaDependency": "\u003e=11.6.0", "grafanaVersion": "*", "plugins": [], "extensions": { diff --git a/pkg/tsdb/elasticsearch/aggregation_factory.go b/pkg/tsdb/elasticsearch/aggregation_factory.go index cc3e597e50b..3f702b745b4 100644 --- a/pkg/tsdb/elasticsearch/aggregation_factory.go +++ b/pkg/tsdb/elasticsearch/aggregation_factory.go @@ -3,8 +3,8 @@ package elasticsearch import ( "regexp" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // addDateHistogramAgg adds a date histogram aggregation to the aggregation builder diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 49b24651609..12e1a8f5df4 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) // Used in logging to mark a stage @@ -160,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch resSpan.End() }() - improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing) + improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing") msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled) if err != nil { return nil, err diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index 8f257873232..b8afb048c00 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func TestClient_ExecuteMultisearch(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/client/search_request_test.go b/pkg/tsdb/elasticsearch/client/search_request_test.go index 80113b4996e..7e2c592dddb 100644 --- a/pkg/tsdb/elasticsearch/client/search_request_test.go +++ b/pkg/tsdb/elasticsearch/client/search_request_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func TestSearchRequest(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/data_query_processor.go b/pkg/tsdb/elasticsearch/data_query_processor.go index 288d6ce30de..4dc0afb109a 100644 --- a/pkg/tsdb/elasticsearch/data_query_processor.go +++ b/pkg/tsdb/elasticsearch/data_query_processor.go @@ -6,8 +6,8 @@ import ( "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // processQuery processes a single query and adds it to the multi-search request builder diff --git a/pkg/tsdb/elasticsearch/data_query_settings.go b/pkg/tsdb/elasticsearch/data_query_settings.go index fe286ccaeda..519eb6dc96d 100644 --- a/pkg/tsdb/elasticsearch/data_query_settings.go +++ b/pkg/tsdb/elasticsearch/data_query_settings.go @@ -3,7 +3,7 @@ package elasticsearch import ( "strconv" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // setFloatPath converts a string value at the specified path to float64 diff --git a/pkg/tsdb/elasticsearch/metrics_response_processor.go b/pkg/tsdb/elasticsearch/metrics_response_processor.go index 1e60a732d64..619180ccf90 100644 --- a/pkg/tsdb/elasticsearch/metrics_response_processor.go +++ b/pkg/tsdb/elasticsearch/metrics_response_processor.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // metricsResponseProcessor handles processing of metrics query responses diff --git a/pkg/tsdb/elasticsearch/models.go b/pkg/tsdb/elasticsearch/models.go index adb18554339..8df08182588 100644 --- a/pkg/tsdb/elasticsearch/models.go +++ b/pkg/tsdb/elasticsearch/models.go @@ -4,7 +4,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // Query represents the time series query model of the datasource diff --git a/pkg/tsdb/elasticsearch/parse_query.go b/pkg/tsdb/elasticsearch/parse_query.go index e1bfa189ab9..4d7b0cf7d5e 100644 --- a/pkg/tsdb/elasticsearch/parse_query.go +++ b/pkg/tsdb/elasticsearch/parse_query.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, error) { diff --git a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go index b092763b57d..a569c92e7db 100644 --- a/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go +++ b/pkg/tsdb/elasticsearch/raw_dsl_aggregation_parser.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // AggregationParser parses raw Elasticsearch DSL aggregations diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index d05ca92e19b..2c0c5d33810 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -15,9 +15,9 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/grafana/grafana/pkg/tsdb/elasticsearch/instrumentation" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) const ( diff --git a/pkg/tsdb/elasticsearch/response_utils.go b/pkg/tsdb/elasticsearch/response_utils.go index c101633d0c2..5dfd1f38360 100644 --- a/pkg/tsdb/elasticsearch/response_utils.go +++ b/pkg/tsdb/elasticsearch/response_utils.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson" ) // flatten flattens multi-level objects to single level objects. It uses dot notation to join keys. diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson.go b/pkg/tsdb/elasticsearch/simplejson/simplejson.go new file mode 100644 index 00000000000..d7759ac3c2b --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson.go @@ -0,0 +1,582 @@ +// Package simplejson provides a wrapper for arbitrary JSON objects that adds methods to access properties. +// Use of this package in place of types and the standard library's encoding/json package is strongly discouraged. +// +// Don't lint for stale code, since it's a copied library and we might as well keep the whole thing. +// nolint:unused +package simplejson + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "log" +) + +// returns the current implementation version +func Version() string { + return "0.5.0" +} + +type Json struct { + data any +} + +func (j *Json) FromDB(data []byte) error { + j.data = make(map[string]any) + + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +func (j *Json) ToDB() ([]byte, error) { + if j == nil || j.data == nil { + return nil, nil + } + + return j.Encode() +} + +func (j *Json) Scan(val any) error { + switch v := val.(type) { + case []byte: + if len(v) == 0 { + return nil + } + return json.Unmarshal(v, &j) + case string: + if len(v) == 0 { + return nil + } + return json.Unmarshal([]byte(v), &j) + default: + return fmt.Errorf("unsupported type: %T", v) + } +} + +func (j *Json) Value() (driver.Value, error) { + return j.ToDB() +} + +// DeepCopyInto creates a copy by serializing JSON +func (j *Json) DeepCopyInto(out *Json) { + b, err := j.Encode() + if err == nil { + _ = out.UnmarshalJSON(b) + } +} + +// DeepCopy will make a deep copy of the JSON object +func (j *Json) DeepCopy() *Json { + if j == nil { + return nil + } + out := new(Json) + j.DeepCopyInto(out) + return out +} + +// NewJson returns a pointer to a new `Json` object +// after unmarshaling `body` bytes +func NewJson(body []byte) (*Json, error) { + j := new(Json) + err := j.UnmarshalJSON(body) + if err != nil { + return nil, err + } + return j, nil +} + +// MustJson returns a pointer to a new `Json` object, panicking if `body` cannot be parsed. +func MustJson(body []byte) *Json { + j, err := NewJson(body) + + if err != nil { + panic(fmt.Sprintf("could not unmarshal JSON: %q", err)) + } + + return j +} + +// New returns a pointer to a new, empty `Json` object +func New() *Json { + return &Json{ + data: make(map[string]any), + } +} + +// NewFromAny returns a pointer to a new `Json` object with provided data. +func NewFromAny(data any) *Json { + return &Json{data: data} +} + +// Interface returns the underlying data +func (j *Json) Interface() any { + return j.data +} + +// Encode returns its marshaled data as `[]byte` +func (j *Json) Encode() ([]byte, error) { + return j.MarshalJSON() +} + +// EncodePretty returns its marshaled data as `[]byte` with indentation +func (j *Json) EncodePretty() ([]byte, error) { + return json.MarshalIndent(&j.data, "", " ") +} + +// Implements the json.Marshaler interface. +func (j *Json) MarshalJSON() ([]byte, error) { + return json.Marshal(&j.data) +} + +// Set modifies `Json` map by `key` and `value` +// Useful for changing single key/value in a `Json` object easily. +func (j *Json) Set(key string, val any) { + m, err := j.Map() + if err != nil { + return + } + m[key] = val +} + +// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path, +// and then finally writing in the value +func (j *Json) SetPath(branch []string, val any) { + if len(branch) == 0 { + j.data = val + return + } + + // in order to insert our branch, we need map[string]any + if _, ok := (j.data).(map[string]any); !ok { + // have to replace with something suitable + j.data = make(map[string]any) + } + curr := j.data.(map[string]any) + + for i := 0; i < len(branch)-1; i++ { + b := branch[i] + // key exists? + if _, ok := curr[b]; !ok { + n := make(map[string]any) + curr[b] = n + curr = n + continue + } + + // make sure the value is the right sort of thing + if _, ok := curr[b].(map[string]any); !ok { + // have to replace with something suitable + n := make(map[string]any) + curr[b] = n + } + + curr = curr[b].(map[string]any) + } + + // add remaining k/v + curr[branch[len(branch)-1]] = val +} + +// Del modifies `Json` map by deleting `key` if it is present. +func (j *Json) Del(key string) { + m, err := j.Map() + if err != nil { + return + } + delete(m, key) +} + +// Get returns a pointer to a new `Json` object +// for `key` in its `map` representation +// +// useful for chaining operations (to traverse a nested JSON): +// +// js.Get("top_level").Get("dict").Get("value").Int() +func (j *Json) Get(key string) *Json { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val} + } + } + return &Json{nil} +} + +// GetPath searches for the item as specified by the branch +// without the need to deep dive using Get()'s. +// +// js.GetPath("top_level", "dict") +func (j *Json) GetPath(branch ...string) *Json { + jin := j + for _, p := range branch { + jin = jin.Get(p) + } + return jin +} + +// GetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation +// +// this is the analog to Get when accessing elements of +// a json array instead of a json object: +// +// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() +func (j *Json) GetIndex(index int) *Json { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]} + } + } + return &Json{nil} +} + +// CheckGetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation, and a `bool` +// indicating success or failure +// +// useful for chained operations when success is important: +// +// if data, ok := js.Get("top_level").CheckGetIndex(0); ok { +// log.Println(data) +// } +func (j *Json) CheckGetIndex(index int) (*Json, bool) { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]}, true + } + } + return nil, false +} + +// SetIndex modifies `Json` array by `index` and `value` +// for `index` in its `array` representation +func (j *Json) SetIndex(index int, val any) { + a, err := j.Array() + if err == nil { + if len(a) > index { + a[index] = val + } + } +} + +// CheckGet returns a pointer to a new `Json` object and +// a `bool` identifying success or failure +// +// useful for chained operations when success is important: +// +// if data, ok := js.Get("top_level").CheckGet("inner"); ok { +// log.Println(data) +// } +func (j *Json) CheckGet(key string) (*Json, bool) { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val}, true + } + } + return nil, false +} + +// Map type asserts to `map` +func (j *Json) Map() (map[string]any, error) { + if m, ok := (j.data).(map[string]any); ok { + return m, nil + } + return nil, errors.New("type assertion to map[string]any failed") +} + +// Array type asserts to an `array` +func (j *Json) Array() ([]any, error) { + if a, ok := (j.data).([]any); ok { + return a, nil + } + return nil, errors.New("type assertion to []any failed") +} + +// Bool type asserts to `bool` +func (j *Json) Bool() (bool, error) { + if s, ok := (j.data).(bool); ok { + return s, nil + } + return false, errors.New("type assertion to bool failed") +} + +// String type asserts to `string` +func (j *Json) String() (string, error) { + if s, ok := (j.data).(string); ok { + return s, nil + } + return "", errors.New("type assertion to string failed") +} + +// Bytes type asserts to `[]byte` +func (j *Json) Bytes() ([]byte, error) { + if s, ok := (j.data).(string); ok { + return []byte(s), nil + } + return nil, errors.New("type assertion to []byte failed") +} + +// StringArray type asserts to an `array` of `string` +func (j *Json) StringArray() ([]string, error) { + arr, err := j.Array() + if err != nil { + return nil, err + } + retArr := make([]string, 0, len(arr)) + for _, a := range arr { + if a == nil { + retArr = append(retArr, "") + continue + } + s, ok := a.(string) + if !ok { + return nil, err + } + retArr = append(retArr, s) + } + return retArr, nil +} + +// MustArray guarantees the return of a `[]any` (with optional default) +// +// useful when you want to iterate over array values in a succinct manner: +// +// for i, v := range js.Get("results").MustArray() { +// fmt.Println(i, v) +// } +func (j *Json) MustArray(args ...[]any) []any { + var def []any + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustArray() received too many arguments %d", len(args)) + } + + a, err := j.Array() + if err == nil { + return a + } + + return def +} + +// MustMap guarantees the return of a `map[string]any` (with optional default) +// +// useful when you want to iterate over map values in a succinct manner: +// +// for k, v := range js.Get("dictionary").MustMap() { +// fmt.Println(k, v) +// } +func (j *Json) MustMap(args ...map[string]any) map[string]any { + var def map[string]any + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustMap() received too many arguments %d", len(args)) + } + + a, err := j.Map() + if err == nil { + return a + } + + return def +} + +// MustString guarantees the return of a `string` (with optional default) +// +// useful when you explicitly want a `string` in a single value return context: +// +// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default")) +func (j *Json) MustString(args ...string) string { + var def string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustString() received too many arguments %d", len(args)) + } + + s, err := j.String() + if err == nil { + return s + } + + return def +} + +// MustStringArray guarantees the return of a `[]string` (with optional default) +// +// useful when you want to iterate over array values in a succinct manner: +// +// for i, s := range js.Get("results").MustStringArray() { +// fmt.Println(i, s) +// } +func (j *Json) MustStringArray(args ...[]string) []string { + var def []string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustStringArray() received too many arguments %d", len(args)) + } + + a, err := j.StringArray() + if err == nil { + return a + } + + return def +} + +// MustInt guarantees the return of an `int` (with optional default) +// +// useful when you explicitly want an `int` in a single value return context: +// +// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150)) +func (j *Json) MustInt(args ...int) int { + var def int + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt() received too many arguments %d", len(args)) + } + + i, err := j.Int() + if err == nil { + return i + } + + return def +} + +// MustFloat64 guarantees the return of a `float64` (with optional default) +// +// useful when you explicitly want a `float64` in a single value return context: +// +// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150)) +func (j *Json) MustFloat64(args ...float64) float64 { + var def float64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustFloat64() received too many arguments %d", len(args)) + } + + f, err := j.Float64() + if err == nil { + return f + } + + return def +} + +// MustBool guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// +// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true)) +func (j *Json) MustBool(args ...bool) bool { + var def bool + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustBool() received too many arguments %d", len(args)) + } + + b, err := j.Bool() + if err == nil { + return b + } + + return def +} + +// MustInt64 guarantees the return of an `int64` (with optional default) +// +// useful when you explicitly want an `int64` in a single value return context: +// +// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150)) +func (j *Json) MustInt64(args ...int64) int64 { + var def int64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt64() received too many arguments %d", len(args)) + } + + i, err := j.Int64() + if err == nil { + return i + } + + return def +} + +// MustUInt64 guarantees the return of an `uint64` (with optional default) +// +// useful when you explicitly want an `uint64` in a single value return context: +// +// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150)) +func (j *Json) MustUint64(args ...uint64) uint64 { + var def uint64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustUint64() received too many arguments %d", len(args)) + } + + i, err := j.Uint64() + if err == nil { + return i + } + + return def +} + +// MarshalYAML implements yaml.Marshaller. +func (j *Json) MarshalYAML() (any, error) { + return j.data, nil +} + +// UnmarshalYAML implements yaml.Unmarshaller. +func (j *Json) UnmarshalYAML(unmarshal func(any) error) error { + var data any + if err := unmarshal(&data); err != nil { + return err + } + j.data = data + return nil +} diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go b/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go new file mode 100644 index 00000000000..88748985576 --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson_go11.go @@ -0,0 +1,90 @@ +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "reflect" + "strconv" +) + +// Implements the json.Unmarshaler interface. +func (j *Json) UnmarshalJSON(p []byte) error { + dec := json.NewDecoder(bytes.NewBuffer(p)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +// NewFromReader returns a *Json by decoding from an io.Reader +func NewFromReader(r io.Reader) (*Json, error) { + j := new(Json) + dec := json.NewDecoder(r) + dec.UseNumber() + err := dec.Decode(&j.data) + return j, err +} + +// Float64 coerces into a float64 +func (j *Json) Float64() (float64, error) { + switch n := j.data.(type) { + case json.Number: + return n.Float64() + case float32, float64: + return reflect.ValueOf(j.data).Float(), nil + case int, int8, int16, int32, int64: + return float64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return float64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int coerces into an int +func (j *Json) Int() (int, error) { + switch n := j.data.(type) { + case json.Number: + i, err := n.Int64() + if err != nil { + return 0, err + } + return int(i), nil + case float32, float64: + return int(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return int(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return int(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int64 coerces into an int64 +func (j *Json) Int64() (int64, error) { + switch n := j.data.(type) { + case json.Number: + return n.Int64() + case float32, float64: + return int64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return reflect.ValueOf(j.data).Int(), nil + case uint, uint8, uint16, uint32, uint64: + return int64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Uint64 coerces into an uint64 +func (j *Json) Uint64() (uint64, error) { + switch n := j.data.(type) { + case json.Number: + return strconv.ParseUint(n.String(), 10, 64) + case float32, float64: + return uint64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return uint64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return reflect.ValueOf(j.data).Uint(), nil + } + return 0, errors.New("invalid value type") +} diff --git a/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go b/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go new file mode 100644 index 00000000000..efc786bc745 --- /dev/null +++ b/pkg/tsdb/elasticsearch/simplejson/simplejson_test.go @@ -0,0 +1,274 @@ +package simplejson + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSimplejson(t *testing.T) { + var ok bool + var err error + + js, err := NewJson([]byte(`{ + "test": { + "string_array": ["asdf", "ghjk", "zxcv"], + "string_array_null": ["abc", null, "efg"], + "array": [1, "2", 3], + "arraywithsubs": [{"subkeyone": 1}, + {"subkeytwo": 2, "subkeythree": 3}], + "int": 10, + "float": 5.150, + "string": "simplejson", + "bool": true, + "sub_obj": {"a": 1} + } + }`)) + + assert.NotEqual(t, nil, js) + assert.Equal(t, nil, err) + + _, ok = js.CheckGet("test") + assert.Equal(t, true, ok) + + _, ok = js.CheckGet("missing_key") + assert.Equal(t, false, ok) + + aws := js.Get("test").Get("arraywithsubs") + assert.NotEqual(t, nil, aws) + var awsval int + awsval, _ = aws.GetIndex(0).Get("subkeyone").Int() + assert.Equal(t, 1, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int() + assert.Equal(t, 2, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeythree").Int() + assert.Equal(t, 3, awsval) + + arr := js.Get("test").Get("array") + assert.NotEqual(t, nil, arr) + val, ok := arr.CheckGetIndex(0) + assert.Equal(t, ok, true) + valInt, _ := val.Int() + assert.Equal(t, valInt, 1) + val, ok = arr.CheckGetIndex(1) + assert.Equal(t, ok, true) + valStr, _ := val.String() + assert.Equal(t, valStr, "2") + val, ok = arr.CheckGetIndex(2) + assert.Equal(t, ok, true) + valInt, _ = val.Int() + assert.Equal(t, valInt, 3) + _, ok = arr.CheckGetIndex(3) + assert.Equal(t, ok, false) + + i, _ := js.Get("test").Get("int").Int() + assert.Equal(t, 10, i) + + f, _ := js.Get("test").Get("float").Float64() + assert.Equal(t, 5.150, f) + + s, _ := js.Get("test").Get("string").String() + assert.Equal(t, "simplejson", s) + + b, _ := js.Get("test").Get("bool").Bool() + assert.Equal(t, true, b) + + mi := js.Get("test").Get("int").MustInt() + assert.Equal(t, 10, mi) + + mi2 := js.Get("test").Get("missing_int").MustInt(5150) + assert.Equal(t, 5150, mi2) + + ms := js.Get("test").Get("string").MustString() + assert.Equal(t, "simplejson", ms) + + ms2 := js.Get("test").Get("missing_string").MustString("fyea") + assert.Equal(t, "fyea", ms2) + + ma2 := js.Get("test").Get("missing_array").MustArray([]any{"1", 2, "3"}) + assert.Equal(t, ma2, []any{"1", 2, "3"}) + + msa := js.Get("test").Get("string_array").MustStringArray() + assert.Equal(t, msa[0], "asdf") + assert.Equal(t, msa[1], "ghjk") + assert.Equal(t, msa[2], "zxcv") + + msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa2[0], "asdf") + assert.Equal(t, msa2[1], "ghjk") + assert.Equal(t, msa2[2], "zxcv") + + msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa3, []string{"1", "2", "3"}) + + mm2 := js.Get("test").Get("missing_map").MustMap(map[string]any{"found": false}) + assert.Equal(t, mm2, map[string]any{"found": false}) + + strs, err := js.Get("test").Get("string_array").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs[0], "asdf") + assert.Equal(t, strs[1], "ghjk") + assert.Equal(t, strs[2], "zxcv") + + strs2, err := js.Get("test").Get("string_array_null").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs2[0], "abc") + assert.Equal(t, strs2[1], "") + assert.Equal(t, strs2[2], "efg") + + gp, _ := js.GetPath("test", "string").String() + assert.Equal(t, "simplejson", gp) + + gp2, _ := js.GetPath("test", "int").Int() + assert.Equal(t, 10, gp2) + + assert.Equal(t, js.Get("test").Get("bool").MustBool(), true) + + js.Set("float2", 300.0) + assert.Equal(t, js.Get("float2").MustFloat64(), 300.0) + + js.Set("test2", "setTest") + assert.Equal(t, "setTest", js.Get("test2").MustString()) + + js.Del("test2") + assert.NotEqual(t, "setTest", js.Get("test2").MustString()) + + js.Get("test").Get("sub_obj").Set("a", 2) + assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt()) + + js.GetPath("test", "sub_obj").Set("a", 3) + assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt()) +} + +func TestStdlibInterfaces(t *testing.T) { + val := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + val2 := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + + raw := `{"name":"myobject","params":{"string":"simplejson"}}` + + assert.Equal(t, nil, json.Unmarshal([]byte(raw), val)) + + assert.Equal(t, "myobject", val.Name) + assert.NotEqual(t, nil, val.Params.data) + s, _ := val.Params.Get("string").String() + assert.Equal(t, "simplejson", s) + + p, err := json.Marshal(val) + assert.Equal(t, nil, err) + assert.Equal(t, nil, json.Unmarshal(p, val2)) + assert.Equal(t, val, val2) // stable +} + +func TestSet(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.Set("baz", "bing") + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestReplace(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`)) + assert.Equal(t, nil, err) + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestSetPath(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"foo", "bar"}, "baz") + + s, err := js.GetPath("foo", "bar").String() + assert.Equal(t, nil, err) + assert.Equal(t, "baz", s) +} + +func TestSetPathNoPath(t *testing.T) { + js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`)) + assert.Equal(t, nil, err) + + f := js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 1.0) + + js.SetPath([]string{}, map[string]any{"foo": "bar"}) + + s, err := js.GetPath("foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) + + f = js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 99.0) +} + +func TestPathWillAugmentExisting(t *testing.T) { + js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "d"}, "dd") + + cases := []struct { + path []string + outcome string + }{ + { + path: []string{"this", "a"}, + outcome: "aa", + }, + { + path: []string{"this", "b"}, + outcome: "bb", + }, + { + path: []string{"this", "c"}, + outcome: "cc", + }, + { + path: []string{"this", "d"}, + outcome: "dd", + }, + } + + for _, tc := range cases { + s, err := js.GetPath(tc.path...).String() + assert.Equal(t, nil, err) + assert.Equal(t, tc.outcome, s) + } +} + +func TestPathWillOverwriteExisting(t *testing.T) { + // notice how "a" is 0.1 - but then we'll try to set at path a, foo + js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "a", "foo"}, "bar") + + s, err := js.GetPath("this", "a", "foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) +} + +func TestMustJson(t *testing.T) { + js := MustJson([]byte(`{"foo": "bar"}`)) + assert.Equal(t, js.Get("foo").MustString(), "bar") + + assert.PanicsWithValue(t, "could not unmarshal JSON: \"unexpected EOF\"", func() { + MustJson([]byte(`{`)) + }) +} diff --git a/pkg/tsdb/elasticsearch/standalone/datasource.go b/pkg/tsdb/elasticsearch/standalone/datasource.go new file mode 100644 index 00000000000..6b9b8ac3f82 --- /dev/null +++ b/pkg/tsdb/elasticsearch/standalone/datasource.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + elasticsearch "github.com/grafana/grafana/pkg/tsdb/elasticsearch" +) + +var ( + _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CallResourceHandler = (*Datasource)(nil) +) + +func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &Datasource{ + Service: elasticsearch.ProvideService(httpclient.NewProvider()), + }, nil +} + +type Datasource struct { + Service *elasticsearch.Service +} + +func contextualMiddlewares(ctx context.Context) context.Context { + cfg := backend.GrafanaConfigFromContext(ctx) + responseLimitMiddleware := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit()) + ctx = httpclient.WithContextualMiddleware(ctx, responseLimitMiddleware) + return ctx +} + +func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + ctx = contextualMiddlewares(ctx) + return d.Service.QueryData(ctx, req) +} + +func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + ctx = contextualMiddlewares(ctx) + return d.Service.CallResource(ctx, req, sender) +} + +func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + ctx = contextualMiddlewares(ctx) + return d.Service.CheckHealth(ctx, req) +} diff --git a/pkg/tsdb/elasticsearch/standalone/main.go b/pkg/tsdb/elasticsearch/standalone/main.go new file mode 100644 index 00000000000..22bd4169339 --- /dev/null +++ b/pkg/tsdb/elasticsearch/standalone/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + // Start listening to requests sent from Grafana. This call is blocking so + // it won't finish until Grafana shuts down the process or the plugin choose + // to exit by itself using os.Exit. Manage automatically manages life cycle + // of datasource instances. It accepts datasource instance factory as first + // argument. This factory will be automatically called on incoming request + // from Grafana to create different instances of SampleDatasource (per datasource + // ID). When datasource configuration changed Dispose method will be called and + // new datasource instance created using NewSampleDatasource factory. + if err := datasource.Manage("elasticsearch", NewDatasource, datasource.ManageOpts{}); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index a529952eff3..03025e484cb 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -4,8 +4,6 @@ const cloudwatchPlugin = async () => await import(/* webpackChunkName: "cloudwatchPlugin" */ 'app/plugins/datasource/cloudwatch/module'); const dashboardDSPlugin = async () => await import(/* webpackChunkName "dashboardDSPlugin" */ 'app/plugins/datasource/dashboard/module'); -const elasticsearchPlugin = async () => - await import(/* webpackChunkName: "elasticsearchPlugin" */ 'app/plugins/datasource/elasticsearch/module'); const grafanaPlugin = async () => await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module'); const influxdbPlugin = async () => @@ -75,7 +73,6 @@ const builtInPlugins: Record Promise | null, - options: OptionsOrGroups> + options: OptionsOrGroups, GroupBase>> ) => { // TODO: would be extremely nice here to allow only template variables and values that are // valid date histogram's Interval options - const valueExists = (options as Array>).some(hasValue(inputValue)); + const valueExists = options.some(hasValue(inputValue)); // we also don't want users to create "empty" values return !valueExists && inputValue.trim().length > 0; }; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx index 6fded5be996..ddfd02b0cc4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx @@ -3,8 +3,8 @@ import { uniqueId } from 'lodash'; import { useEffect, useRef } from 'react'; import { InlineField, Input, QueryField } from '@grafana/ui'; -import { Filters } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filters } from '../../../../../dataquery.gen'; import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer'; import { AddRemove } from '../../../../AddRemove'; import { changeBucketAggregationSetting } from '../../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts index e60a664f066..a5e3cb3d172 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../../dataquery.gen'; export const addFilter = createAction('@bucketAggregations/filter/add'); export const removeFilter = createAction('@bucketAggregations/filter/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts index 56b5ac9555c..eb03fcc96a3 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts @@ -1,6 +1,5 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../../dataquery.gen'; +import { reducerTester } from '../../../../../reducerTester'; import { addFilter, changeFilter, removeFilter } from './actions'; import { reducer } from './reducer'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts index 022de8233b4..b99818d1850 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from 'redux'; -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Filter } from '../../../../../../dataquery.gen'; import { defaultFilter } from '../utils'; import { addFilter, changeFilter, removeFilter } from './actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts index adf5646381d..3538a497bf4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts @@ -1,3 +1,3 @@ -import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { Filter } from '../../../../../dataquery.gen'; export const defaultFilter = (): Filter => ({ label: '', query: '*' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx index 14862e8f664..9012730c8e2 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx @@ -1,14 +1,7 @@ import { fireEvent, screen } from '@testing-library/react'; import selectEvent from 'react-select-event'; -import { - Average, - Derivative, - ElasticsearchDataQuery, - Terms, - TopMetrics, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Average, Derivative, ElasticsearchDataQuery, Terms, TopMetrics } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { renderWithESProvider } from '../../../../test-helpers/render'; import { describeMetric } from '../../../../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx index e1bc64980ab..852b7d8bb84 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx @@ -2,15 +2,9 @@ import { uniqueId } from 'lodash'; import { useRef } from 'react'; import { SelectableValue } from '@grafana/data'; -import { InlineField, Select, Input } from '@grafana/ui'; -import { - Terms, - ExtendedStats, - ExtendedStatMetaType, - Percentiles, - MetricAggregation, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { InlineField, Input, Select } from '@grafana/ui'; +import { ExtendedStats, MetricAggregation, Percentiles, Terms } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { describeMetric } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; @@ -105,7 +99,7 @@ function createOrderByOptionsForExtendedStats(metric: ExtendedStats): Selectable if (!metric.meta) { return []; } - const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[]; + const metaKeys = Object.keys(metric.meta); return metaKeys .filter((key) => metric.meta?.[key]) .map((key) => { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx index 9191ca25d1c..63be91fad00 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx @@ -2,8 +2,8 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useRef } from 'react'; import { InlineField, Input } from '@grafana/ui'; -import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { BucketAggregation } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { SettingsEditorContainer } from '../../SettingsEditorContainer'; import { changeBucketAggregationSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts index 3e4e5cfea7c..a0f60b799a4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts @@ -1,7 +1,6 @@ -import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation } from '../../../../dataquery.gen'; import { defaultGeoHashPrecisionString } from '../../../../queryDef'; -import { describeMetric, convertOrderByToMetricId } from '../../../../utils'; +import { convertOrderByToMetricId, describeMetric } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts index dfab9ac0279..e3dff091246 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts @@ -1,10 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { - BucketAggregation, - BucketAggregationType, - BucketAggregationWithField, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../../../../dataquery.gen'; export const addBucketAggregation = createAction('@bucketAggs/add'); export const removeBucketAggregation = createAction('@bucketAggs/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index f4a5cc02dde..462f5938b81 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -1,9 +1,4 @@ -import { - BucketAggregation, - DateHistogram, - ElasticsearchDataQuery, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation, DateHistogram, ElasticsearchDataQuery } from '../../../../dataquery.gen'; import { defaultBucketAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts index 5ba29e656d8..789405c97be 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { BucketAggregation, ElasticsearchDataQuery, Terms } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { BucketAggregation, ElasticsearchDataQuery, Terms } from '../../../../dataquery.gen'; import { defaultBucketAgg } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; @@ -47,11 +46,12 @@ export const createReducer = } /* - TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations - in the new selected one (such as field or some settings). - It the future would be nice to have the same behavior but it's hard without a proper definition, - as Elasticsearch will error sometimes if some settings are not compatible. - */ + TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations + in the new selected one (such as field or some settings). + It the future would be nice to have the same behavior but it's hard without a proper definition, + as Elasticsearch will error sometimes if some settings are not compatible. + */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { id: bucketAgg.id, type: action.payload.newType, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx index 70d53eddbc4..348a8a630ce 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx @@ -2,10 +2,10 @@ import { css } from '@emotion/css'; import { uniqueId } from 'lodash'; import { Fragment, useEffect } from 'react'; -import { Input, InlineLabel } from '@grafana/ui'; -import { BucketScript, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { InlineLabel, Input } from '@grafana/ui'; -import { useStatelessReducer, useDispatch } from '../../../../../hooks/useStatelessReducer'; +import { BucketScript, MetricAggregation } from '../../../../../dataquery.gen'; +import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer'; import { AddRemove } from '../../../../AddRemove'; import { MetricPicker } from '../../../../MetricPicker'; import { changeMetricAttribute } from '../../state/actions'; @@ -13,9 +13,9 @@ import { SettingField } from '../SettingField'; import { addPipelineVariable, + changePipelineVariableMetric, removePipelineVariable, renamePipelineVariable, - changePipelineVariableMetric, } from './state/actions'; import { reducer } from './state/reducer'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts index 02fc628ecc3..276ca285c64 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts @@ -1,5 +1,4 @@ -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { PipelineVariable } from '../../../../../../dataquery.gen'; import { reducerTester } from '../../../../../reducerTester'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts index 406b1f5b590..8d798a5717b 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { PipelineVariable } from '../../../../../../dataquery.gen'; import { defaultPipelineVariable, generatePipelineVariableName } from '../utils'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts index e2da781d190..4c3991c69a9 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts @@ -1,4 +1,4 @@ -import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { PipelineVariable } from '../../../../../dataquery.gen'; export const defaultPipelineVariable = (name: string): PipelineVariable => ({ name, pipelineAgg: '' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx index e4a3ad907a9..588b4692f5e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx @@ -2,11 +2,8 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useState } from 'react'; import { InlineField, Input, TextArea } from '@grafana/ui'; -import { - MetricAggregationWithSettings, - MetricAggregationWithInlineScript, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; +import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { getScriptValue } from '../../../../utils'; import { SettingKeyOf } from '../../../types'; @@ -33,9 +30,11 @@ export function SettingField (object: { value: string }) => object.value === value; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts index 9adff8781b1..b0b52dd39e7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts @@ -1,7 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { MetricAggregation, MetricAggregationWithSettings } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { MetricAggregation, MetricAggregationWithSettings } from '../../../../dataquery.gen'; import { MetricAggregationWithMeta } from '../../../../types'; export const addMetric = createAction('@metrics/add'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 9dcbaa9f974..38a4e0f05d1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -1,10 +1,4 @@ -import { - MetricAggregation, - ElasticsearchDataQuery, - Derivative, - ExtendedStats, -} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { Derivative, ElasticsearchDataQuery, ExtendedStats, MetricAggregation } from '../../../../dataquery.gen'; import { defaultMetricAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts index c0dab7bd4b1..57acf1e87e5 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts @@ -1,7 +1,6 @@ import { Action } from '@reduxjs/toolkit'; -import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; - +import { ElasticsearchDataQuery, MetricAggregation } from '../../../../dataquery.gen'; import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; @@ -57,6 +56,7 @@ export const reducer = ( It the future would be nice to have the same behavior but it's hard without a proper definition, as Elasticsearch will error sometimes if some settings are not compatible. */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { id: metric.id, type: action.payload.type, diff --git a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts index 79e2fddbc2d..b6a53173cdd 100644 --- a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts +++ b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types/store'; +import { StoreState } from '../types/store'; type GrafanaReducer = (state: S, action: A) => S; diff --git a/public/app/plugins/datasource/elasticsearch/jest-setup.js b/public/app/plugins/datasource/elasticsearch/jest-setup.js new file mode 100644 index 00000000000..c85bf9d3a57 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/jest-setup.js @@ -0,0 +1 @@ +import '@grafana/plugin-configs/jest/jest-setup'; diff --git a/public/app/plugins/datasource/elasticsearch/jest.config.js b/public/app/plugins/datasource/elasticsearch/jest.config.js new file mode 100644 index 00000000000..fabef448081 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/jest.config.js @@ -0,0 +1,3 @@ +import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js'; + +export default defaultConfig; diff --git a/public/app/plugins/datasource/elasticsearch/package.json b/public/app/plugins/datasource/elasticsearch/package.json new file mode 100644 index 00000000000..bd1470fa2c6 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/package.json @@ -0,0 +1,62 @@ +{ + "name": "@grafana-plugins/elasticsearch", + "description": "Grafana data source for Elasticsearch", + "private": true, + "version": "12.4.0-pre", + "dependencies": { + "@emotion/css": "11.13.5", + "@grafana/aws-sdk": "0.8.3", + "@grafana/data": "12.4.0-pre", + "@grafana/plugin-ui": "^0.11.1", + "@grafana/runtime": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", + "@reduxjs/toolkit": "2.10.1", + "lodash": "4.17.21", + "lucene": "^2.1.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-redux": "9.2.0", + "react-select": "5.10.2", + "react-use": "17.6.0", + "redux": "5.0.1", + "redux-thunk": "3.1.0", + "rxjs": "7.8.2", + "semver": "7.7.3", + "tslib": "2.8.1" + }, + "devDependencies": { + "@grafana/e2e-selectors": "12.4.0-pre", + "@grafana/plugin-configs": "12.4.0-pre", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.6.4", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/jest": "29.5.14", + "@types/lodash": "4.17.20", + "@types/lucene": "^2", + "@types/node": "24.10.1", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.5", + "@types/semver": "7.7.1", + "jest": "29.7.0", + "react-select-event": "5.5.1", + "ts-node": "10.9.2", + "typescript": "5.9.2", + "webpack": "5.101.0" + }, + "peerDependencies": { + "@grafana/runtime": "*" + }, + "resolutions": { + "redux": "^5.0.0" + }, + "scripts": { + "build": "webpack -c ./webpack.config.ts --env production", + "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "webpack -w -c ./webpack.config.ts --env development", + "test": "jest --watch --onlyChanged", + "test:ci": "jest --maxWorkers 4" + }, + "packageManager": "yarn@4.11.0" +} diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index 0e056ffa447..9440fcfed54 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -2,6 +2,7 @@ "type": "datasource", "name": "Elasticsearch", "id": "elasticsearch", + "executable": "gpx_elasticsearch", "category": "logging", "info": { "description": "Open source logging & analytics database", @@ -27,7 +28,8 @@ "name": "Documentation", "url": "https://grafana.com/docs/grafana/latest/datasources/elasticsearch/" } - ] + ], + "version": "%VERSION%" }, "alerting": true, "annotations": true, @@ -36,5 +38,9 @@ "backend": true, "queryOptions": { "minInterval": true + }, + "dependencies": { + "grafanaDependency": ">=11.6.0", + "plugins": [] } } diff --git a/public/app/plugins/datasource/elasticsearch/project.json b/public/app/plugins/datasource/elasticsearch/project.json new file mode 100644 index 00000000000..4247352791d --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/project.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "tags": ["scope:plugin", "type:datasource"], + "targets": { + "build": {}, + "dev": {} + } +} diff --git a/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts b/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts new file mode 100644 index 00000000000..3aa81581e06 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/reducers/actions/cleanUp.ts @@ -0,0 +1,11 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { StoreState } from '../../types/store'; + +export type CleanUpAction = (state: StoreState) => void; + +export interface CleanUpPayload { + cleanupAction: CleanUpAction; +} + +export const cleanUpAction = createAction('core/cleanUpState'); diff --git a/public/app/plugins/datasource/elasticsearch/reducers/root.ts b/public/app/plugins/datasource/elasticsearch/reducers/root.ts new file mode 100644 index 00000000000..5e13826691a --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/reducers/root.ts @@ -0,0 +1,21 @@ +import { ReducersMapObject } from '@reduxjs/toolkit'; +import { Action as AnyAction, combineReducers } from 'redux'; + +const addedReducers = { + defaultReducer: (state = {}) => state, + templating: (state = { lastKey: 'key' }) => state, +}; + +export const addReducer = (newReducers: ReducersMapObject) => { + Object.assign(addedReducers, newReducers); +}; + +export const createRootReducer = () => { + const appReducer = combineReducers({ + ...addedReducers, + }); + + return (state: Parameters[0], action: AnyAction) => { + return appReducer(state, action); + }; +}; diff --git a/public/app/plugins/datasource/elasticsearch/store/configureStore.ts b/public/app/plugins/datasource/elasticsearch/store/configureStore.ts new file mode 100644 index 00000000000..319cccd193d --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/store/configureStore.ts @@ -0,0 +1,47 @@ +import { createListenerMiddleware, configureStore as reduxConfigureStore } from '@reduxjs/toolkit'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { Middleware } from 'redux'; + +import { addReducer, createRootReducer } from '../reducers/root'; +import { StoreState } from '../types/store'; + +import { setStore } from './store'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function addRootReducer(reducers: any) { + // this is ok now because we add reducers before configureStore is called + // in the future if we want to add reducers during runtime + // we'll have to solve this in a more dynamic way + addReducer(reducers); +} + +const listenerMiddleware = createListenerMiddleware(); +const extraMiddleware: Middleware[] = []; + +export function addExtraMiddleware(middleware: Middleware) { + extraMiddleware.push(middleware); +} + +export function configureStore(initialState?: Partial) { + const store = reduxConfigureStore({ + reducer: createRootReducer(), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( + listenerMiddleware.middleware, + ...extraMiddleware + ), + devTools: process.env.NODE_ENV !== 'production', + preloadedState: { + ...initialState, + }, + }); + + // this enables "refetchOnFocus" and "refetchOnReconnect" for RTK Query + setupListeners(store.dispatch); + + setStore(store); + return store; +} + +export type RootState = ReturnType['getState']>; +export type AppDispatch = ReturnType['dispatch']; diff --git a/public/app/plugins/datasource/elasticsearch/store/store.ts b/public/app/plugins/datasource/elasticsearch/store/store.ts new file mode 100644 index 00000000000..aaccaca84f5 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/store/store.ts @@ -0,0 +1,26 @@ +import { Store } from 'redux'; + +import { StoreState } from '../types/store'; + +export let store: Store; + +export function setStore(newStore: Store) { + store = newStore; +} + +export function getState(): StoreState { + if (!store || !store.getState) { + return { defaultReducer: () => ({}), templating: { lastKey: 'key' } }; // used by tests + } + + return store.getState(); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function dispatch(action: any) { + if (!store || !store.getState) { + return; + } + + return store.dispatch(action); +} diff --git a/public/app/plugins/datasource/elasticsearch/tsconfig.json b/public/app/plugins/datasource/elasticsearch/tsconfig.json new file mode 100644 index 00000000000..40352099203 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "types": ["node", "jest", "@testing-library/jest-dom"] + }, + "extends": "@grafana/plugin-configs/tsconfig.json", + "include": ["."] +} diff --git a/public/app/plugins/datasource/elasticsearch/types/store.ts b/public/app/plugins/datasource/elasticsearch/types/store.ts new file mode 100644 index 00000000000..1ff65f1a7af --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/types/store.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-restricted-imports */ +import { + Action, + addListener as addListenerUntyped, + AsyncThunk, + AsyncThunkOptions, + AsyncThunkPayloadCreator, + createAsyncThunk as createAsyncThunkUntyped, + PayloadAction, + TypedAddListener, +} from '@reduxjs/toolkit'; +import { + TypedUseSelectorHook, + useDispatch as useDispatchUntyped, + useSelector as useSelectorUntyped, +} from 'react-redux'; +import { ThunkDispatch as GenericThunkDispatch, ThunkAction } from 'redux-thunk'; + +import type { createRootReducer } from '../reducers/root'; +import { AppDispatch, RootState } from '../store/configureStore'; +import { dispatch as storeDispatch } from '../store/store'; + +export type StoreState = ReturnType>; + +/* + * Utility type to get strongly types thunks + */ +export type ThunkResult = ThunkAction>; + +export type ThunkDispatch = GenericThunkDispatch; + +// Typed useDispatch & useSelector hooks +export const useDispatch: () => AppDispatch = useDispatchUntyped; +export const useSelector: TypedUseSelectorHook = useSelectorUntyped; + +type DefaultThunkApiConfig = { dispatch: AppDispatch; state: StoreState }; +export const createAsyncThunk = ( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator, + options?: AsyncThunkOptions +): AsyncThunk => + createAsyncThunkUntyped(typePrefix, payloadCreator, options); + +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions +export const addListener = addListenerUntyped as TypedAddListener; +export const dispatch: AppDispatch = storeDispatch; diff --git a/public/app/plugins/datasource/elasticsearch/webpack.config.ts b/public/app/plugins/datasource/elasticsearch/webpack.config.ts new file mode 100644 index 00000000000..f64bb95e3c0 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/webpack.config.ts @@ -0,0 +1,9 @@ +import type { Configuration } from 'webpack'; + +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; + +const config = async (env: Env): Promise => { + return await grafanaConfig(env); +}; + +export default config; diff --git a/yarn.lock b/yarn.lock index d16b10ef5f3..5559a4bed77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2610,6 +2610,53 @@ __metadata: languageName: node linkType: hard +"@grafana-plugins/elasticsearch@workspace:public/app/plugins/datasource/elasticsearch": + version: 0.0.0-use.local + resolution: "@grafana-plugins/elasticsearch@workspace:public/app/plugins/datasource/elasticsearch" + dependencies: + "@emotion/css": "npm:11.13.5" + "@grafana/aws-sdk": "npm:0.8.3" + "@grafana/data": "npm:12.4.0-pre" + "@grafana/e2e-selectors": "npm:12.4.0-pre" + "@grafana/plugin-configs": "npm:12.4.0-pre" + "@grafana/plugin-ui": "npm:^0.11.1" + "@grafana/runtime": "npm:12.4.0-pre" + "@grafana/schema": "npm:12.4.0-pre" + "@grafana/ui": "npm:12.4.0-pre" + "@reduxjs/toolkit": "npm:2.10.1" + "@testing-library/dom": "npm:10.4.1" + "@testing-library/jest-dom": "npm:6.6.4" + "@testing-library/react": "npm:16.3.0" + "@testing-library/user-event": "npm:14.6.1" + "@types/jest": "npm:29.5.14" + "@types/lodash": "npm:4.17.20" + "@types/lucene": "npm:^2" + "@types/node": "npm:24.10.1" + "@types/react": "npm:18.3.18" + "@types/react-dom": "npm:18.3.5" + "@types/semver": "npm:7.7.1" + jest: "npm:29.7.0" + lodash: "npm:4.17.21" + lucene: "npm:^2.1.1" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + react-redux: "npm:9.2.0" + react-select: "npm:5.10.2" + react-select-event: "npm:5.5.1" + react-use: "npm:17.6.0" + redux: "npm:5.0.1" + redux-thunk: "npm:3.1.0" + rxjs: "npm:7.8.2" + semver: "npm:7.7.3" + ts-node: "npm:10.9.2" + tslib: "npm:2.8.1" + typescript: "npm:5.9.2" + webpack: "npm:5.101.0" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor": version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor"