From 5cb4c311dc65c9b42fcc0fd14d1183a097aeba46 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Mon, 12 Jan 2026 12:13:11 +0100 Subject: [PATCH 01/88] Chore: Eslint ignore webpack.config barrel files (#116115) chore(eslint): ignore decoupled plugins webpack configs barrel files --- eslint-suppressions.json | 5 ----- eslint.config.js | 2 ++ .../plugins/datasource/cloud-monitoring/webpack.config.ts | 1 - .../grafana-postgresql-datasource/webpack.config.ts | 1 - .../datasource/grafana-testdata-datasource/webpack.config.ts | 1 - public/app/plugins/datasource/graphite/webpack.config.ts | 1 - public/app/plugins/datasource/loki/webpack.config.ts | 1 - public/app/plugins/datasource/mysql/webpack.config.ts | 1 - public/app/plugins/datasource/opentsdb/webpack.config.ts | 1 - public/app/plugins/datasource/tempo/webpack.config.ts | 1 - public/app/plugins/datasource/zipkin/webpack.config.ts | 1 - 11 files changed, 2 insertions(+), 14 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index c92d2939837..250dbd20348 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -4020,11 +4020,6 @@ "count": 1 } }, - "public/app/plugins/datasource/parca/webpack.config.ts": { - "no-barrel-files/no-barrel-files": { - "count": 1 - } - }, "public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/eslint.config.js b/eslint.config.js index bd1be26465a..5e44ffebaf4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -585,6 +585,8 @@ module.exports = [ // FIXME: Remove once all enterprise issues are fixed - // we don't have a suppressions file/approach for enterprise code yet ...enterpriseIgnores, + // Ignore decoupled plugin webpack configs + 'public/app/**/webpack.config.ts', ], rules: { 'no-barrel-files/no-barrel-files': 'error', diff --git a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts +++ b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/graphite/webpack.config.ts b/public/app/plugins/datasource/graphite/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/graphite/webpack.config.ts +++ b/public/app/plugins/datasource/graphite/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/loki/webpack.config.ts b/public/app/plugins/datasource/loki/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/loki/webpack.config.ts +++ b/public/app/plugins/datasource/loki/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/mysql/webpack.config.ts b/public/app/plugins/datasource/mysql/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/mysql/webpack.config.ts +++ b/public/app/plugins/datasource/mysql/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/opentsdb/webpack.config.ts b/public/app/plugins/datasource/opentsdb/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/opentsdb/webpack.config.ts +++ b/public/app/plugins/datasource/opentsdb/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/tempo/webpack.config.ts b/public/app/plugins/datasource/tempo/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/tempo/webpack.config.ts +++ b/public/app/plugins/datasource/tempo/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/zipkin/webpack.config.ts b/public/app/plugins/datasource/zipkin/webpack.config.ts index 7931eb9cb5c..31247bb11af 100644 --- a/public/app/plugins/datasource/zipkin/webpack.config.ts +++ b/public/app/plugins/datasource/zipkin/webpack.config.ts @@ -1,4 +1,3 @@ import config from '@grafana/plugin-configs/webpack.config.ts'; -// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; From e61e40644034876022ab15ee6f6e77fc57fcad10 Mon Sep 17 00:00:00 2001 From: Naimesh Patel <70344829+naimeshpatel5295@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:49:47 +0530 Subject: [PATCH 02/88] Explore: Add keyboard shortcut to run queries (#111675) (#115811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Explore: add keyboard shortcut to run queries (#111675) * Update mock * Fix linting --------- Co-authored-by: Piotr Jamróz --- .../explore/hooks/useKeyboardShortcuts.ts | 28 ++++++++++++++++++- public/app/types/events.ts | 4 +++ public/test/mocks/getGrafanaContextMock.ts | 2 ++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/hooks/useKeyboardShortcuts.ts b/public/app/features/explore/hooks/useKeyboardShortcuts.ts index 9ff1e32e5ed..47487690878 100644 --- a/public/app/features/explore/hooks/useKeyboardShortcuts.ts +++ b/public/app/features/explore/hooks/useKeyboardShortcuts.ts @@ -3,9 +3,19 @@ import { Unsubscribable } from 'rxjs'; import { getAppEvents } from '@grafana/runtime'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import { AbsoluteTimeEvent, CopyTimeEvent, PasteTimeEvent, ShiftTimeEvent, ZoomOutEvent } from 'app/types/events'; +import { getState } from 'app/store/store'; +import { + AbsoluteTimeEvent, + CopyTimeEvent, + PasteTimeEvent, + RunQueriesEvent, + ShiftTimeEvent, + ZoomOutEvent, +} from 'app/types/events'; import { useDispatch } from 'app/types/store'; +import { runQueries } from '../state/query'; +import { selectPanesEntries } from '../state/selectors'; import { copyTimeRangeToClipboard, makeAbsoluteTime, @@ -21,8 +31,23 @@ export function useKeyboardShortcuts() { useEffect(() => { keybindings.setupTimeRangeBindings(false); + // Explore-specific: run queries shortcut + keybindings.bind('e r', () => { + getAppEvents().publish(new RunQueriesEvent()); + }); + const tearDown: Unsubscribable[] = []; + tearDown.push( + getAppEvents().subscribe(RunQueriesEvent, () => { + // Read panes at event time to avoid re-subscribing when panes change + const panes = selectPanesEntries(getState()); + panes.forEach(([exploreId]) => { + dispatch(runQueries({ exploreId })); + }); + }) + ); + tearDown.push( getAppEvents().subscribe(AbsoluteTimeEvent, () => { dispatch(makeAbsoluteTime()); @@ -54,6 +79,7 @@ export function useKeyboardShortcuts() { ); return () => { + keybindings.unbind('e r'); tearDown.forEach((u) => u.unsubscribe()); }; }, [dispatch, keybindings]); diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 6fb728bb94c..95fbecc1e2d 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -159,6 +159,10 @@ export class AbsoluteTimeEvent extends BusEventWithPayload { static type = 'remove-panel'; } diff --git a/public/test/mocks/getGrafanaContextMock.ts b/public/test/mocks/getGrafanaContextMock.ts index 64e704986b7..aa1d061f7fe 100644 --- a/public/test/mocks/getGrafanaContextMock.ts +++ b/public/test/mocks/getGrafanaContextMock.ts @@ -19,6 +19,8 @@ export function getGrafanaContextMock(overrides: Partial = { clearAndInitGlobalBindings: jest.fn(), setupDashboardBindings: jest.fn(), setupTimeRangeBindings: jest.fn(), + bind: jest.fn(), + unbind: jest.fn(), } as unknown as KeybindingSrv, newAssetsChecker: { start: jest.fn(), From 1b52718c23f4e7542a3e74c5e9801ed11cb36974 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 12 Jan 2026 15:21:03 +0300 Subject: [PATCH 03/88] Search: include panel titles and types in index (#115742) --- .../rtkq/dashboard/v0alpha1/endpoints.gen.ts | 6 + pkg/registry/apis/dashboard/search.go | 51 ++++-- .../store/kind/dashboard/ds_lookup.go | 11 +- .../panel-with-library-panel-field-info.json | 8 +- pkg/services/store/kind/dashboard/types.go | 19 ++ pkg/storage/unified/search/bleve.go | 6 +- pkg/storage/unified/search/bleve_test.go | 1 - .../unified/search/builders/dashboard.go | 35 +++- .../testdata/doc/dashboard-aaa-out.json | 14 +- .../builders/testdata/doc/dashboard-aaa.json | 14 +- .../search/testdata/manual-dashboard.json | 12 +- pkg/tests/apis/dashboard/search_test.go | 168 ++++++++++++++++++ .../dashboard/testdata/searchV0/t00-all.json | 165 +++++++++++++++++ .../testdata/searchV0/t01-simple-query.json | 28 +++ .../searchV0/t02-with-text-panel.json | 18 ++ .../dashboard.grafana.app-v0alpha1.json | 16 ++ .../admin/components/PluginDetailsBody.tsx | 2 +- .../plugins/admin/components/PluginUsage.tsx | 5 +- .../admin/hooks/usePluginDetailsTabs.tsx | 2 +- public/app/features/search/service/types.ts | 1 - public/app/features/search/service/unified.ts | 8 + 21 files changed, 557 insertions(+), 33 deletions(-) create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t00-all.json create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json create mode 100644 pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 326b53ccedd..9605b8e9355 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -246,6 +246,8 @@ const injectedRtkApi = api facetLimit: queryArg.facetLimit, tags: queryArg.tags, libraryPanel: queryArg.libraryPanel, + panelType: queryArg.panelType, + dataSourceType: queryArg.dataSourceType, permission: queryArg.permission, sort: queryArg.sort, limit: queryArg.limit, @@ -674,6 +676,10 @@ export type SearchDashboardsAndFoldersApiArg = { tags?: string[]; /** find dashboards that reference a given libraryPanel */ libraryPanel?: string; + /** find dashboards using panels of a given plugin type */ + panelType?: string; + /** find dashboards using datasources of a given plugin type */ + dataSourceType?: string; /** permission needed for the resource (view, edit, admin) */ permission?: 'view' | 'edit' | 'admin'; /** sortable field */ diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 08a943b8da6..8572dae8295 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -142,6 +142,24 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * Schema: spec.StringProperty(), }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "panelType", + In: "query", + Description: "find dashboards using panels of a given plugin type", + Required: false, + Schema: spec.StringProperty(), + }, + }, + { + ParameterProps: spec3.ParameterProps{ + Name: "dataSourceType", + In: "query", + Description: "find dashboards using datasources of a given plugin type", + Required: false, + Schema: spec.StringProperty(), + }, + }, { ParameterProps: spec3.ParameterProps{ Name: "permission", @@ -430,14 +448,11 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use } } - // The facet term fields + // Apply facet terms if facets, ok := queryParams["facet"]; ok { if queryParams.Has("facetLimit") { if parsed, err := strconv.Atoi(queryParams.Get("facetLimit")); err == nil && parsed > 0 { - facetLimit = parsed - if facetLimit > 1000 { - facetLimit = 1000 - } + facetLimit = min(parsed, 1000) } } searchRequest.Facet = make(map[string]*resourcepb.ResourceSearchRequest_Facet) @@ -449,21 +464,35 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use } } - // The tags filter - if tags, ok := queryParams["tag"]; ok { + if v, ok := queryParams["tag"]; ok { searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: "tags", Operator: "=", - Values: tags, + Values: v, }) } - // The libraryPanel filter - if libraryPanel, ok := queryParams["libraryPanel"]; ok { + if v, ok := queryParams["panelType"]; ok { + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ + Key: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_PANEL_TYPES, + Operator: "=", + Values: v, + }) + } + + if v, ok := queryParams["dataSourceType"]; ok { + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ + Key: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_DS_TYPES, + Operator: "=", + Values: v, + }) + } + + if v, ok := queryParams["libraryPanel"]; ok { searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", - Values: libraryPanel, + Values: v, }) } diff --git a/pkg/services/store/kind/dashboard/ds_lookup.go b/pkg/services/store/kind/dashboard/ds_lookup.go index 5d132d569be..7330e598409 100644 --- a/pkg/services/store/kind/dashboard/ds_lookup.go +++ b/pkg/services/store/kind/dashboard/ds_lookup.go @@ -100,6 +100,9 @@ func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef { if ref == nil { return d.defaultDS } + if ref.UID == "default" && ref.Type == "" { + return d.defaultDS + } key := "" if ref.UID != "" { @@ -117,7 +120,13 @@ func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef { return ds } - return d.byName[key] + ds, ok = d.byName[key] + if ok { + return ds + } + + // With nothing was found (or configured), use the original reference + return ref } func (d *DsLookup) ByType(dsType string) []DataSourceRef { diff --git a/pkg/services/store/kind/dashboard/testdata/panel-with-library-panel-field-info.json b/pkg/services/store/kind/dashboard/testdata/panel-with-library-panel-field-info.json index 1ffaecb605b..2a687ad8865 100644 --- a/pkg/services/store/kind/dashboard/testdata/panel-with-library-panel-field-info.json +++ b/pkg/services/store/kind/dashboard/testdata/panel-with-library-panel-field-info.json @@ -4,8 +4,8 @@ "tags": null, "datasource": [ { - "uid": "default.uid", - "type": "default.type" + "uid": "000000001", + "type": "graphite" } ], "panels": [ @@ -16,8 +16,8 @@ "libraryPanel": "dfkljg98345dkf", "datasource": [ { - "uid": "default.uid", - "type": "default.type" + "uid": "000000001", + "type": "graphite" } ] } diff --git a/pkg/services/store/kind/dashboard/types.go b/pkg/services/store/kind/dashboard/types.go index 51aa00a79fd..c1dea30abb7 100644 --- a/pkg/services/store/kind/dashboard/types.go +++ b/pkg/services/store/kind/dashboard/types.go @@ -1,5 +1,7 @@ package dashboard +import "iter" + type PanelSummaryInfo struct { ID int64 `json:"id"` Title string `json:"title"` @@ -30,3 +32,20 @@ type DashboardSummaryInfo struct { Refresh string `json:"refresh,omitempty"` ReadOnly bool `json:"readOnly,omitempty"` // editable = false } + +func (d *DashboardSummaryInfo) PanelIterator() iter.Seq[PanelSummaryInfo] { + return func(yield func(PanelSummaryInfo) bool) { + for _, p := range d.Panels { + if len(p.Collapsed) > 0 { + for _, c := range p.Collapsed { + if !yield(c) { // NOTE, rows can only be one level deep! + return + } + } + } + if !yield(p) { + return + } + } + } +} diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 254c1080653..eec7290633b 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -1253,21 +1253,23 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R 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) // 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) - // At least one of the queries must match - searchQuery := bleve.NewDisjunctionQuery(queryExact, queryAnalyzed, queryPhrase) queries = append(queries, searchQuery) } diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index d80e35f90e5..c9c3967cd58 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -23,7 +23,6 @@ import ( "go.uber.org/goleak" authlib "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/storage/unified/search/builders/dashboard.go b/pkg/storage/unified/search/builders/dashboard.go index 4f8d55111a9..a2963d71186 100644 --- a/pkg/storage/unified/search/builders/dashboard.go +++ b/pkg/storage/unified/search/builders/dashboard.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "slices" "sort" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,6 +19,7 @@ import ( const DASHBOARD_SCHEMA_VERSION = "schema_version" const DASHBOARD_LINK_COUNT = "link_count" const DASHBOARD_PANEL_TYPES = "panel_types" +const DASHBOARD_PANEL_TITLE = "panel_title" const DASHBOARD_DS_TYPES = "ds_types" const DASHBOARD_TRANSFORMATIONS = "transformation" const DASHBOARD_LIBRARY_PANEL_REFERENCE = "reference.LibraryPanel" @@ -53,11 +55,21 @@ func DashboardBuilder(namespaced resource.NamespacedDocumentSupplier) (resource. Type: resourcepb.ResourceTableColumnDefinition_INT32, Description: "How many links appear on the page", }, + { + Name: DASHBOARD_PANEL_TITLE, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + IsArray: true, + Description: "The panel title text", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: false, // full text + FreeText: true, + }, + }, { Name: DASHBOARD_PANEL_TYPES, Type: resourcepb.ResourceTableColumnDefinition_STRING, IsArray: true, - Description: "How many links appear on the page", + Description: "The panel types used in this dashboard", Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ Filterable: true, }, @@ -269,14 +281,22 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou doc.Description = summary.Description doc.Tags = summary.Tags + panelTitles := []string{} panelTypes := []string{} transformations := []string{} dsTypes := []string{} - for _, p := range summary.Panels { - if p.Type != "" { + for p := range summary.PanelIterator() { + switch p.Type { + case "": // ignore + case "row": // row should map to a layout type when we support v2 constructs + default: panelTypes = append(panelTypes, p.Type) } + + if len(p.Title) > 0 { + panelTitles = append(panelTitles, p.Title) + } if len(p.Transformer) > 0 { transformations = append(transformations, p.Transformer...) } @@ -309,17 +329,20 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou resource.SEARCH_FIELD_LEGACY_ID: summary.ID, } + if len(panelTitles) > 0 { + doc.Fields[DASHBOARD_PANEL_TITLE] = panelTitles + } if len(panelTypes) > 0 { sort.Strings(panelTypes) - doc.Fields[DASHBOARD_PANEL_TYPES] = panelTypes + doc.Fields[DASHBOARD_PANEL_TYPES] = slices.Compact(panelTypes) // distinct values } if len(dsTypes) > 0 { sort.Strings(dsTypes) - doc.Fields[DASHBOARD_DS_TYPES] = dsTypes + doc.Fields[DASHBOARD_DS_TYPES] = slices.Compact(dsTypes) // distinct values } if len(transformations) > 0 { sort.Strings(transformations) - doc.Fields[DASHBOARD_TRANSFORMATIONS] = transformations + doc.Fields[DASHBOARD_TRANSFORMATIONS] = slices.Compact(transformations) // distinct values } for k, v := range s.Stats[summary.UID] { diff --git a/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json index a77725e2cc2..fdb77b02c3b 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json +++ b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json @@ -32,10 +32,16 @@ "errors_last_7_days": 1, "grafana.app/deprecatedInternalID": 141, "link_count": 0, + "panel_title": [ + "green pie", + "red pie", + "blue pie", + "collapsed row" + ], "panel_types": [ "barchart", "graph", - "row" + "pie" ], "schema_version": 38 }, @@ -46,6 +52,12 @@ "kind": "DataSource", "name": "DSUID" }, + { + "relation": "depends-on", + "group": "dashboards.grafana.app", + "kind": "LibraryPanel", + "name": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, { "relation": "depends-on", "group": "dashboards.grafana.app", diff --git a/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json index d9ecbfc6aec..24360ee929b 100644 --- a/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json +++ b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json @@ -67,7 +67,7 @@ "name": "red pie", "uid": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" }, - "title": "green pie" + "title": "red pie" }, { "id": 7, @@ -78,6 +78,14 @@ "id": 8, "type": "graph" }, + { + "id": 20, + "type": "graph" + }, + { + "id": 30, + "type": "graph" + }, { "collapsed": true, "gridPos": { @@ -101,6 +109,10 @@ "uid": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" }, "title": "blue pie" + }, + { + "id": 40, + "type": "pie" } ], "title": "collapsed row", diff --git a/pkg/storage/unified/search/testdata/manual-dashboard.json b/pkg/storage/unified/search/testdata/manual-dashboard.json index 4208a58dcd3..2ae346d072c 100644 --- a/pkg/storage/unified/search/testdata/manual-dashboard.json +++ b/pkg/storage/unified/search/testdata/manual-dashboard.json @@ -71,11 +71,18 @@ "description": "How many links appear on the page", "priority": 0 }, + { + "name": "panel_title", + "type": "string", + "format": "", + "description": "The panel title text", + "priority": 0 + }, { "name": "panel_types", "type": "string", "format": "", - "description": "How many links appear on the page", + "description": "The panel types used in this dashboard", "priority": 0 }, { @@ -214,6 +221,7 @@ null, null, null, + null, null ], "object": { @@ -239,6 +247,7 @@ "repo", null, null, + null, [ "timeseries" ], @@ -282,6 +291,7 @@ "repo", null, null, + null, [ "timeseries", "table" diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 3fe35c51b3a..2227c67287e 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -4,10 +4,15 @@ import ( "context" "encoding/json" "fmt" + "io/fs" + "math" "net/http" + "os" + "path/filepath" "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -16,12 +21,167 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/util/testutil" ) +func TestIntegrationSearchDevDashboards(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + ctx := context.Background() + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableDataMigrations: true, + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, + "folders.folder.grafana.app": {DualWriterMode: rest.Mode5}, + }, + UnifiedStorageEnableSearch: true, + }) + defer helper.Shutdown() + + // Create devenv dashboards from legacy API + cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig()) + cfg.GroupVersion = &dashboardV0.GroupVersion + adminClient, err := k8srest.RESTClientFor(cfg) + require.NoError(t, err) + adminClient.Get() + + fileCount := 0 + devenv := "../../../../devenv/dev-dashboards/panel-timeseries" + err = filepath.WalkDir(devenv, func(p string, d fs.DirEntry, e error) error { + require.NoError(t, err) + if d.IsDir() || filepath.Ext(d.Name()) != ".json" { + return nil + } + + // use the filename as UID + uid := strings.TrimSuffix(d.Name(), ".json") + if len(uid) > 40 { + uid = uid[:40] // avoid uid too long, max 40 characters + } + + // nolint:gosec + data, err := os.ReadFile(p) + require.NoError(t, err) + + cmd := dashboards.SaveDashboardCommand{ + Dashboard: &simplejson.Json{}, + Overwrite: true, + } + err = cmd.Dashboard.FromDB(data) + require.NoError(t, err) + cmd.Dashboard.Set("id", nil) + cmd.Dashboard.Set("uid", uid) + data, err = json.Marshal(cmd) + require.NoError(t, err) + + var statusCode int + result := adminClient.Post().AbsPath("api", "dashboards", "db"). + Body(data). + SetHeader("Content-type", "application/json"). + Do(ctx). + StatusCode(&statusCode) + require.NoError(t, result.Error(), "file: [%d] %s [status:%d]", fileCount, d.Name(), statusCode) + require.Equal(t, int(http.StatusOK), statusCode) + fileCount++ + return nil + }) + require.NoError(t, err) + require.Equal(t, 16, fileCount, "file count from %s", devenv) + + // Helper to call search + callSearch := func(user apis.User, params string) dashboardV0.SearchResults { + require.NotNil(t, user) + ns := user.Identity.GetNamespace() + cfg := dynamic.ConfigFor(user.NewRestConfig()) + cfg.GroupVersion = &dashboardV0.GroupVersion + restClient, err := k8srest.RESTClientFor(cfg) + require.NoError(t, err) + + var statusCode int + req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search"). + 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]) + } + } + res := req.Do(ctx).StatusCode(&statusCode) + require.NoError(t, res.Error()) + require.Equal(t, int(http.StatusOK), statusCode) + var sr dashboardV0.SearchResults + raw, err := res.Raw() + require.NoError(t, err) + require.NoError(t, json.Unmarshal(raw, &sr)) + + // Normalize scores and query cost for snapshot comparison + sr.QueryCost = 0 // this depends on the hardware + sr.MaxScore = roundTo(sr.MaxScore, 3) + for i := range sr.Hits { + sr.Hits[i].Score = roundTo(sr.Hits[i].Score, 3) // 0.6250571494814442 -> 0.625 + } + return sr + } + + // Compare a results to snapshots + testCases := []struct { + name string + user apis.User + params string + }{ + { + name: "all", + user: helper.Org1.Admin, + params: "", // only dashboards + }, + { + name: "simple-query", + user: helper.Org1.Admin, + params: "query=stacking", + }, + { + name: "with-text-panel", + user: helper.Org1.Admin, + params: "field=panel_types&panelType=text", + }, + } + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res := callSearch(tc.user, tc.params) + jj, err := json.MarshalIndent(res, "", " ") + require.NoError(t, err) + + fname := fmt.Sprintf("testdata/searchV0/t%02d-%s.json", i, tc.name) + // nolint:gosec + snapshot, err := os.ReadFile(fname) + if err != nil { + assert.Failf(t, "Failed to read snapshot", "file: %s", fname) + err = os.WriteFile(fname, jj, 0o644) + require.NoErrorf(t, err, "Failed to write snapshot file %s", fname) + return + } + + if !assert.JSONEq(t, string(snapshot), string(jj)) { + err = os.WriteFile(fname, jj, 0o644) + require.NoErrorf(t, err, "Failed to write snapshot file %s", fname) + } + }) + } +} + func TestIntegrationSearchPermissionFiltering(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -285,3 +445,11 @@ func setFolderPermissions(t *testing.T, helper *apis.K8sTestHelper, actingUser a require.Equal(t, http.StatusOK, resp.Response.StatusCode, "Failed to set permissions for folder %s", folderUID) } + +// roundTo rounds a float64 to a specified number of decimal places. +func roundTo(n float64, decimals uint32) float64 { + // Calculate the power of 10 for the desired number of decimals + scale := math.Pow(10, float64(decimals)) + // Multiply, round to the nearest integer, and then divide back + return math.Round(n*scale) / scale +} diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t00-all.json b/pkg/tests/apis/dashboard/testdata/searchV0/t00-all.json new file mode 100644 index 00000000000..35b7cff0302 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t00-all.json @@ -0,0 +1,165 @@ +{ + "totalHits": 16, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries", + "title": "Panel Tests - Graph NG", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-by-value-color-schemes", + "title": "Panel Tests - Graph NG - By value color schemes", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-nulls", + "title": "Panel Tests - Graph NG - Discrete panels", + "tags": [ + "gdev", + "panel-tests", + "graph-ng", + "timeseries", + "trend", + "state-timeline", + "transform" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-gradient-area", + "title": "Panel Tests - Graph NG - Gradient Area Fills", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-soft-limits", + "title": "Panel Tests - Graph NG - softMin/softMax", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-yaxis-ticks", + "title": "Panel Tests - Graph NG - Y axis ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-hue-gradients", + "title": "Panel Tests - GraphNG - Hue Gradients", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-time", + "title": "Panel Tests - GraphNG - Time Axis", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-thresholds", + "title": "Panel Tests - GraphNG Thresholds", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-shared-tooltip-cursor-positio", + "title": "Panel Tests - shared tooltips cursor positioning", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-bars-high-density", + "title": "Panel Tests - TimeSeries - bars high density (stroke + fill)", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-out-of-rage", + "title": "Panel Tests - Timeseries - Out of range", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-stacking", + "title": "Panel Tests - TimeSeries - stacking", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-formats", + "title": "Panel Tests - Timeseries - Supported input formats" + }, + { + "resource": "dashboards", + "name": "timeseries-stacking2", + "title": "TimeSeries \u0026 BarChart Stacking", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + }, + { + "resource": "dashboards", + "name": "timeseries-y-ticks-zero-decimals", + "title": "Zero Decimals Y Ticks", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ] + } + ], + "maxScore": 1 +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json b/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json new file mode 100644 index 00000000000..6c9a935dfe8 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t01-simple-query.json @@ -0,0 +1,28 @@ +{ + "totalHits": 2, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-stacking", + "title": "Panel Tests - TimeSeries - stacking", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.658 + }, + { + "resource": "dashboards", + "name": "timeseries-stacking2", + "title": "TimeSeries \u0026 BarChart Stacking", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "score": 0.625 + } + ], + "maxScore": 0.658 +} \ 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/t02-with-text-panel.json new file mode 100644 index 00000000000..b38cfb14b40 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/searchV0/t02-with-text-panel.json @@ -0,0 +1,18 @@ +{ + "totalHits": 1, + "hits": [ + { + "resource": "dashboards", + "name": "timeseries-formats", + "title": "Panel Tests - Timeseries - Supported input formats", + "field": { + "panel_types": [ + "table", + "text", + "timeseries" + ] + } + } + ], + "maxScore": 1.778 +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 4634143bd45..3ed64961b1f 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1830,6 +1830,22 @@ "type": "string" } }, + { + "name": "panelType", + "in": "query", + "description": "find dashboards using panels of a given plugin type", + "schema": { + "type": "string" + } + }, + { + "name": "dataSourceType", + "in": "query", + "description": "find dashboards using datasources of a given plugin type", + "schema": { + "type": "string" + } + }, { "name": "permission", "in": "query", diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index d93c5a913b5..b6c5b301f68 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -176,7 +176,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId, info, showDetai export const getStyles = (theme: GrafanaTheme2) => ({ wrap: css({ width: '100%', - height: '50vh', + height: '65vh', }), readme: css({ '& img': { diff --git a/public/app/features/plugins/admin/components/PluginUsage.tsx b/public/app/features/plugins/admin/components/PluginUsage.tsx index 0ad9ebb1738..b156fb39783 100644 --- a/public/app/features/plugins/admin/components/PluginUsage.tsx +++ b/public/app/features/plugins/admin/components/PluginUsage.tsx @@ -62,6 +62,7 @@ export function PluginUsage({ plugin }: Props) { keyboardEvents={of()} onTagSelected={() => {}} trackingSource="PluginDetailsPage_PluginUsage" + onClickItem={() => {}} /> ); }} @@ -74,13 +75,13 @@ export function PluginUsage({ plugin }: Props) { return ; } - if (!config.featureToggles.panelTitleSearch) { + if (!config.featureToggles.unifiedStorageSearchUI) { return ( diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 6cb8ec39af4..be6a6c73973 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -101,7 +101,7 @@ export const usePluginDetailsTabs = ( } if ( - config.featureToggles.panelTitleSearch && + config.featureToggles.unifiedStorageSearchUI && (pluginConfig.meta.type === PluginType.panel || pluginConfig.meta.type === PluginType.datasource) ) { navModelChildren.push({ diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index a670b054979..aaca2595bf0 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -25,7 +25,6 @@ export interface SearchQuery { sort?: string; ds_uid?: string; ds_type?: string; - saved_query_uid?: string; // TODO: not implemented yet tags?: string[]; kind?: string[]; panel_type?: string; diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 146a54d295d..15280602202 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -297,6 +297,14 @@ export class UnifiedSearcher implements GrafanaSearcher { uri += '&' + query.kind.map((kind) => `type=${kind}`).join('&'); } + if (query.ds_type?.length) { + uri += '&dataSourceType=' + query.ds_type; + } + + if (query.panel_type?.length) { + uri += '&panelType=' + query.panel_type; + } + if (query.tags?.length) { uri += '&' + query.tags.map((tag) => `tag=${encodeURIComponent(tag)}`).join('&'); } From 0d7f46c08a36587a482676ddb4f71b8cd8ae9896 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Mon, 12 Jan 2026 13:22:48 +0100 Subject: [PATCH 04/88] chore(unified): remove unifiedStorageSearch feature toggle (#116109) --- packages/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 | 3 ++- pkg/storage/unified/README.md | 1 - pkg/storage/unified/resource/search.go | 2 +- pkg/storage/unified/search/options.go | 2 +- pkg/tests/apis/folder/folders_test.go | 8 ++------ 9 files changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3982bdc33e5..0bd17e2af72 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -657,10 +657,6 @@ export interface FeatureToggles { */ rolePickerDrawer?: boolean; /** - * Enable unified storage search - */ - unifiedStorageSearch?: boolean; - /** * Enable sprinkles on unified storage search */ unifiedStorageSearchSprinkles?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ed591908042..6f2274f22e7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1087,13 +1087,6 @@ var ( Stage: FeatureStageExperimental, Owner: identityAccessTeam, }, - { - Name: "unifiedStorageSearch", - Description: "Enable unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - }, { Name: "unifiedStorageSearchSprinkles", Description: "Enable sprinkles on unified storage search", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 15376f39fc1..c4d2c026d9a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -150,7 +150,6 @@ alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false -unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false managedDualWriter,experimental,@grafana/search-and-storage,false,false,false pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e778a6fc3d1..97ecf868a64 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -455,10 +455,6 @@ const ( // Enables the new role picker drawer design FlagRolePickerDrawer = "rolePickerDrawer" - // FlagUnifiedStorageSearch - // Enable unified storage search - FlagUnifiedStorageSearch = "unifiedStorageSearch" - // FlagUnifiedStorageSearchSprinkles // Enable sprinkles on unified storage search FlagUnifiedStorageSearchSprinkles = "unifiedStorageSearchSprinkles" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 124a4977c07..64d6ff7b5dd 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3697,7 +3697,8 @@ "metadata": { "name": "unifiedStorageSearch", "resourceVersion": "1764664939750", - "creationTimestamp": "2024-09-30T19:46:14Z" + "creationTimestamp": "2024-09-30T19:46:14Z", + "deletionTimestamp": "2026-01-12T10:02:12Z" }, "spec": { "description": "Enable unified storage search", diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index aef27df8e05..9cd0d1fd01d 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -236,7 +236,6 @@ kubernetesDashboards = true kubernetesFolders = true unifiedStorage = true unifiedStorageHistoryPruner = true -unifiedStorageSearch = true unifiedStorageSearchPermissionFiltering = false unifiedStorageSearchSprinkles = false diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index bca5fb98491..ade86760502 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -863,7 +863,7 @@ func newRebuildRequest(key NamespacedResource, minBuildTime, lastImportTime time func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats, key NamespacedResource, reason string) (ResourceIndex, error) { if s == nil || s.search == nil { - return nil, fmt.Errorf("search is not configured properly (missing unifiedStorageSearch feature toggle?)") + return nil, fmt.Errorf("search is not configured properly (missing enable_search config?)") } ctx, span := tracer.Start(ctx, "resource.searchSupport.getOrCreateIndex") diff --git a/pkg/storage/unified/search/options.go b/pkg/storage/unified/search/options.go index 20a0874b598..d450e9ae24b 100644 --- a/pkg/storage/unified/search/options.go +++ b/pkg/storage/unified/search/options.go @@ -19,7 +19,7 @@ func NewSearchOptions( ownsIndexFn func(key resource.NamespacedResource) (bool, error), ) (resource.SearchOptions, error) { //nolint:staticcheck // not yet migrated to OpenFeature - if cfg.EnableSearch || features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) || features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { + if cfg.EnableSearch || features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { root := cfg.IndexPath if root == "" { root = filepath.Join(cfg.DataPath, "unified-search", "bleve") diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index ce7dd5f6795..cdcc798c93a 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -2054,9 +2054,7 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) { DualWriterMode: modeDw, }, }, - EnableFeatureToggles: []string{ - featuremgmt.FlagUnifiedStorageSearch, - }, + UnifiedStorageEnableSearch: true, } setupProvisioningDir(t, &ops) @@ -2163,9 +2161,7 @@ func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T DualWriterMode: mode3, }, }, - EnableFeatureToggles: []string{ - featuremgmt.FlagUnifiedStorageSearch, - }, + UnifiedStorageEnableSearch: true, } setupProvisioningDir(t, &ops) From 2aedbdb76ff606febee27d235118bca28bd4a6ce Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 12 Jan 2026 13:37:17 +0100 Subject: [PATCH 05/88] processing: support duplicated keys when parsing json logs (#116116) * processing: support duplicated keys when parsing json logs * Add regression test * prettier --- .../logs/components/panel/processing.test.ts | 16 ++++++++++++++++ .../features/logs/components/panel/processing.ts | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 5998ff36d21..748c53b1df4 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -190,6 +190,22 @@ describe('preProcessLogs', () => { expect(logListModel.body).not.toBe(entry); }); + test('Prettifies JSON with duplicate keys', () => { + const entry = '{"key": "value", "key": "otherValue"}'; + const logListModel = createLogLine( + { entry }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + prettifyJSON: true, + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).not.toBe(entry); + }); + test('Prettifies and escapes wrapped JSON', () => { const entry = '{"key": "value", "otherKey": "other\\nValue"}'; const logListModel = createLogLine( diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 9132c2b7e6e..814481e87f2 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -135,7 +135,9 @@ export class LogListModel implements LogRowModel { get body(): string { if (this._body === undefined) { try { - const parsed = parse(this.raw); + const parsed = parse(this.raw, undefined, { + onDuplicateKey: ({ newValue }) => newValue, + }); if (typeof parsed === 'object' && parsed !== null && !(parsed instanceof LosslessNumber)) { this._json = true; } From b91ca14f48894cedd18df87c9085a7cfd4638cc3 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:38:18 +0100 Subject: [PATCH 06/88] Icons: Add brain icon (#116023) * Icons: Add brain icon * lint * Add brain to cached icons --- packages/grafana-data/src/types/icon.ts | 1 + public/app/core/icons/cached.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 34e672b66f5..3dc7215a2cf 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -52,6 +52,7 @@ export const availableIconsIndex = { bookmark: true, 'book-open': true, 'brackets-curly': true, + brain: true, 'browser-alt': true, bug: true, building: true, diff --git a/public/app/core/icons/cached.json b/public/app/core/icons/cached.json index 6e35e64dd0c..9987d867738 100644 --- a/public/app/core/icons/cached.json +++ b/public/app/core/icons/cached.json @@ -29,6 +29,7 @@ "unicons/bookmark", "unicons/book-open", "unicons/brackets-curly", + "unicons/brain", "unicons/bug", "unicons/building", "unicons/calculator-alt", From bec4d225b378c81ac95b4d00ca822da0f2eb3beb Mon Sep 17 00:00:00 2001 From: Kristina Demeshchik Date: Mon, 12 Jan 2026 09:29:16 -0500 Subject: [PATCH 07/88] FieldConfig: Fix multiple value mappings in field overrides being overwritten (#116027) * multiple value mappingg overrides * add comment for clarity * remove extra check --- .../src/field/fieldOverrides.test.ts | 40 +++++++++++++++++++ .../grafana-data/src/field/fieldOverrides.ts | 11 ++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 767da439543..b0aa271be3f 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -9,6 +9,7 @@ import { FieldColorModeId } from '../types/fieldColor'; import { FieldConfigPropertyItem, FieldConfigSource } from '../types/fieldOverrides'; import { InterpolateFunction } from '../types/panel'; import { ThresholdsMode } from '../types/thresholds'; +import { MappingType } from '../types/valueMapping'; import { Registry } from '../utils/Registry'; import { locationUtil } from '../utils/location'; import { mockStandardProperties } from '../utils/tests/mockStandardProperties'; @@ -999,6 +1000,45 @@ describe('setDynamicConfigValue', () => { expect(config.custom.property3).toEqual({}); expect(config.displayName).toBeUndefined(); }); + + it('works correctly with multiple value mappings in the same override', () => { + const config: FieldConfig = { + mappings: [{ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } }], + }; + + setDynamicConfigValue( + config, + { + id: 'mappings', + value: [{ type: MappingType.ValueToText, options: { first: { text: 'first' } } }], + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [], + field: { type: FieldType.number } as Field, + dataFrameIndex: 0, + } + ); + + setDynamicConfigValue( + config, + { + id: 'mappings', + value: [{ type: MappingType.ValueToText, options: { second: { text: 'second' } } }], + }, + { + fieldConfigRegistry: customFieldRegistry, + data: [], + field: { type: FieldType.number } as Field, + dataFrameIndex: 0, + } + ); + + expect(config.mappings).toHaveLength(3); + expect(config.mappings![0]).toEqual({ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } }); + expect(config.mappings![1]).toEqual({ type: MappingType.ValueToText, options: { first: { text: 'first' } } }); + expect(config.mappings![2]).toEqual({ type: MappingType.ValueToText, options: { second: { text: 'second' } } }); + }); }); describe('getLinksSupplier', () => { diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 8b345036c64..bf56f811106 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -341,7 +341,7 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV return; } - const val = item.process(value.value, context, item.settings); + let val = item.process(value.value, context, item.settings); const remove = val === undefined || val === null; @@ -352,6 +352,15 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV unset(config, item.path); } } else { + // Merge arrays (e.g. mappings) when multiple overrides target the same field + if (Array.isArray(val)) { + const existingValue = item.isCustom ? get(config.custom, item.path) : get(config, item.path); + + if (Array.isArray(existingValue)) { + val = [...existingValue, ...val]; + } + } + if (item.isCustom) { if (!config.custom) { config.custom = {}; From 77c500dc01385b42d940593e7c2913af8cdb9b78 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 12 Jan 2026 15:46:47 +0100 Subject: [PATCH 08/88] logsExploreTableDefaultVisualization: remove feature flag (#116127) --- packages/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.json | 3 ++- public/app/features/explore/Logs/Logs.tsx | 4 ---- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0bd17e2af72..31359af52cf 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -527,10 +527,6 @@ export interface FeatureToggles { */ dashboardTemplates?: boolean; /** - * Sets the logs table as default visualisation in logs explore - */ - logsExploreTableDefaultVisualization?: boolean; - /** * Enables the new alert list view design */ alertingListViewV2?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6f2274f22e7..f6d9e2da560 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -872,13 +872,6 @@ var ( Owner: grafanaSharingSquad, FrontendOnly: false, }, - { - Name: "logsExploreTableDefaultVisualization", - Description: "Sets the logs table as default visualisation in logs explore", - Stage: FeatureStageExperimental, - Owner: grafanaObservabilityLogsSquad, - FrontendOnly: true, - }, { Name: "alertingListViewV2", Description: "Enables the new alert list view design", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c4d2c026d9a..fdb265c7980 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -120,7 +120,6 @@ queryLibrary,preview,@grafana/sharing-squad,false,false,false dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false dashboardTemplates,preview,@grafana/sharing-squad,false,false,false -logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 64d6ff7b5dd..5747b9bdd20 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2246,7 +2246,8 @@ "metadata": { "name": "logsExploreTableDefaultVisualization", "resourceVersion": "1764664939750", - "creationTimestamp": "2024-05-02T15:28:15Z" + "creationTimestamp": "2024-05-02T15:28:15Z", + "deletionTimestamp": "2026-01-12T14:11:46Z" }, "spec": { "description": "Sets the logs table as default visualisation in logs explore", diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 46930f67552..6d3c0a90739 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -149,9 +149,6 @@ const getDefaultVisualisationType = (): LogsVisualisationType => { if (visualisationType === 'logs') { return 'logs'; } - if (config.featureToggles.logsExploreTableDefaultVisualization) { - return 'table'; - } return 'logs'; }; @@ -447,7 +444,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { reportInteraction('grafana_explore_logs_visualisation_changed', { newVisualizationType: visualisation, datasourceType: props.datasourceType ?? 'unknown', - defaultVisualisationType: config.featureToggles.logsExploreTableDefaultVisualization ? 'table' : 'logs', }); }, [panelState?.logs, props.datasourceType, updatePanelState] From 6dc604c2ea08de488f45ab6639375527e19fb5bb Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Mon, 12 Jan 2026 09:56:15 -0500 Subject: [PATCH 09/88] RecentlyViewedDashboards: Add instrumentations (#116036) RecentlyViewedDashboards: Add instrumentation --- .../components/RecentlyViewedDashboards.tsx | 14 ++++++++++-- .../plugins/panel/dashlist/DashListItem.tsx | 22 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx b/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx index effa0c8009b..f0c2d4d3a13 100644 --- a/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx @@ -4,6 +4,7 @@ import { useAsyncRetry } from 'react-use'; import { GrafanaTheme2, store } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { evaluateBooleanFlag } from '@grafana/runtime/internal'; import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; @@ -35,10 +36,18 @@ export function RecentlyViewedDashboards() { const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0); const handleClearHistory = () => { + reportInteraction('grafana_recently_viewed_dashboards_clear_history'); store.set(recentDashboardsKey, JSON.stringify([])); retry(); }; + const handleSectionToggle = () => { + reportInteraction('grafana_recently_viewed_dashboards_toggle_section', { + expanded: !isOpen, + }); + setIsOpen(!isOpen); + }; + if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) { return null; } @@ -48,7 +57,7 @@ export function RecentlyViewedDashboards() { headerDataTestId="browseDashboardsRecentlyViewedTitle" label={ - setIsOpen(!isOpen)}> + Recently viewed