From 649e9aa8ca9e8f7f16e9198b3858aaf12714a6a8 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Mon, 25 Aug 2025 18:22:09 -0300 Subject: [PATCH 1/5] SavedQueries: Create feature flag (#109944) --- .../src/types/featureToggles.gen.ts | 6 +++++- pkg/services/featuremgmt/registry.go | 10 ++++++++- pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 6 +++++- pkg/services/featuremgmt/toggles_gen.json | 21 ++++++++++++++++--- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 4cd790e7148..9ab694d9d58 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -527,10 +527,14 @@ export interface FeatureToggles { */ grafanaManagedRecordingRules?: boolean; /** - * Enables Query Library feature in Explore + * Renamed feature toggle, enables Saved queries feature */ queryLibrary?: boolean; /** + * Enables Saved Queries feature + */ + savedQueries?: boolean; + /** * Sets the logs table as default visualisation in logs explore */ logsExploreTableDefaultVisualization?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5b94492d18c..7807eaee089 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -905,7 +905,15 @@ var ( }, { Name: "queryLibrary", - Description: "Enables Query Library feature in Explore", + Description: "Renamed feature toggle, enables Saved queries feature", + Stage: FeatureStagePrivatePreview, + Owner: grafanaSharingSquad, + FrontendOnly: false, + AllowSelfServe: false, + }, + { + Name: "savedQueries", + Description: "Enables Saved Queries feature", Stage: FeatureStagePrivatePreview, Owner: grafanaSharingSquad, FrontendOnly: false, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 46303a7f960..a59feb73671 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -118,6 +118,7 @@ cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false queryLibrary,privatePreview,@grafana/sharing-squad,false,false,false +savedQueries,privatePreview,@grafana/sharing-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ee14a3e8f6c..da56b91fa61 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -480,9 +480,13 @@ const ( FlagGrafanaManagedRecordingRules = "grafanaManagedRecordingRules" // FlagQueryLibrary - // Enables Query Library feature in Explore + // Renamed feature toggle, enables Saved queries feature FlagQueryLibrary = "queryLibrary" + // FlagSavedQueries + // Enables Saved Queries feature + FlagSavedQueries = "savedQueries" + // FlagLogsExploreTableDefaultVisualization // Sets the logs table as default visualisation in logs explore FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 21d57f33c52..367fcdf165a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2667,12 +2667,15 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1753448760331", + "resourceVersion": "1755721444487", "creationTimestamp": "2022-10-07T18:31:45Z", - "deletionTimestamp": "2023-03-20T16:00:14Z" + "deletionTimestamp": "2023-03-20T16:00:14Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-08-20 20:24:04.487598 +0000 UTC" + } }, "spec": { - "description": "Enables Query Library feature in Explore", + "description": "Renamed feature toggle, enables Saved queries feature", "stage": "privatePreview", "codeowner": "@grafana/sharing-squad" } @@ -2849,6 +2852,18 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "savedQueries", + "resourceVersion": "1755721444487", + "creationTimestamp": "2025-08-20T20:24:04Z" + }, + "spec": { + "description": "Enables Saved Queries feature", + "stage": "privatePreview", + "codeowner": "@grafana/sharing-squad" + } + }, { "metadata": { "name": "scanRowInvalidDashboardParseFallbackEnabled", From e87652060ce6764b17b46c6ad0dbbea2fbe8b42e Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 25 Aug 2025 19:14:04 -0500 Subject: [PATCH 2/5] Transformations: Handle special case with nested dataframes/JSON (#109876) * Fix crashes * Remove inline snapshot --- .../groupingToMatrix.test.ts.snap | 33 +++++++++++++++++++ .../transformers/groupingToMatrix.test.ts | 27 +++++++++++++++ .../transformers/groupingToMatrix.ts | 7 ++-- .../components/Table/Cells/JSONViewCell.tsx | 7 +++- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 packages/grafana-data/src/transformations/transformers/__snapshots__/groupingToMatrix.test.ts.snap diff --git a/packages/grafana-data/src/transformations/transformers/__snapshots__/groupingToMatrix.test.ts.snap b/packages/grafana-data/src/transformations/transformers/__snapshots__/groupingToMatrix.test.ts.snap new file mode 100644 index 00000000000..97177391a02 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/__snapshots__/groupingToMatrix.test.ts.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Grouping to Matrix generates Matrix ignoring special value when value type is frame 1`] = ` +[ + { + "config": {}, + "name": "Row\\Column", + "type": "string", + "values": [ + "R1", + "R2", + ], + }, + { + "config": {}, + "name": "C1", + "type": "frame", + "values": [ + {}, + undefined, + ], + }, + { + "config": {}, + "name": "C2", + "type": "frame", + "values": [ + {}, + undefined, + ], + }, +] +`; diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts index 9b3956a12f2..6b5ab80911f 100644 --- a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.test.ts @@ -333,4 +333,31 @@ describe('Grouping to Matrix', () => { `); }); }); + + it('generates Matrix ignoring special value when value type is frame', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.groupingToMatrix, + options: { + columnField: 'Column', + rowField: 'Row', + valueField: 'Temp', + emptyValue: SpecialValue.Zero, + }, + }; + + const seriesA = toDataFrame({ + name: 'C', + fields: [ + { name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] }, + { name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] }, + { name: 'Temp', type: FieldType.frame, values: [{}, null, {}] }, + ], + }); + + await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => { + const processed = received[0]; + + expect(processed[0].fields).toMatchSnapshot(); + }); + }); }); diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts index 39614c52042..4e388bf3257 100644 --- a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -1,7 +1,7 @@ import { map } from 'rxjs/operators'; import { getFieldDisplayName } from '../../field/fieldState'; -import { DataFrame, Field } from '../../types/dataFrame'; +import { DataFrame, Field, FieldType } from '../../types/dataFrame'; import { SpecialValue, DataTransformerInfo, @@ -114,7 +114,10 @@ export const groupingToMatrixTransformer: DataTransformerInfo Date: Mon, 25 Aug 2025 20:06:24 -0600 Subject: [PATCH 3/5] Unified Storage: Fix yield statement (#110134) fix yield --- pkg/storage/unified/sql/backend.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index a4df140d541..2560943c92b 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -711,7 +711,9 @@ func (b *backend) ListModifiedSince(ctx context.Context, key resource.Namespaced if mr.Key.Name <= lastSeen { // resource names should be sorted alphabetically. So if not, the query is not correct. - yield(nil, fmt.Errorf("listModifiedSince: resources are not sorted by name ASC, lastSeen: %q, current: %q", lastSeen, mr.Key.Name)) + if !yield(nil, fmt.Errorf("listModifiedSince: resources are not sorted by name ASC, lastSeen: %q, current: %q", lastSeen, mr.Key.Name)) { + return + } } lastSeen = mr.Key.Name From 6b67b394fd4799d3e50cbbc17fc99540982ed25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 26 Aug 2025 06:56:51 +0200 Subject: [PATCH 4/5] Chore: fixes React hook betterer for EditDataSource.test.tsx (#110104) --- .betterer.results | 4 ---- .../components/EditDataSource.test.tsx | 16 ++++++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.betterer.results b/.betterer.results index ada77c000ef..38ab3e6a0d5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2358,10 +2358,6 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] ], - "public/app/features/datasources/components/EditDataSource.test.tsx:5381": [ - [0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"], - [0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"] - ], "public/app/features/datasources/components/picker/DataSourceCard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], diff --git a/public/app/features/datasources/components/EditDataSource.test.tsx b/public/app/features/datasources/components/EditDataSource.test.tsx index 6a1f6832485..c11627ad514 100644 --- a/public/app/features/datasources/components/EditDataSource.test.tsx +++ b/public/app/features/datasources/components/EditDataSource.test.tsx @@ -326,7 +326,7 @@ describe('', () => { it('should pass a context prop to the rendered UI extension component', () => { const message = "I'm a UI extension component!"; - const component = jest.fn().mockReturnValue(
{message}
); + const Component = jest.fn().mockReturnValue(
{message}
); setPluginComponentsHook( jest.fn().mockReturnValue({ @@ -337,7 +337,7 @@ describe('', () => { pluginId: 'grafana-pdc-app', title: 'Example component', description: 'Example description', - component, + component: Component, }, '1' ), @@ -353,9 +353,9 @@ describe('', () => { }, }); - expect(component).toHaveBeenCalled(); + expect(Component).toHaveBeenCalled(); - const props = component.mock.calls[0][0]; + const props = Component.mock.calls[0][0]; expect(props.context).toBeDefined(); expect(props.context.dataSource).toBeDefined(); @@ -368,7 +368,7 @@ describe('', () => { it('should be possible to update the `jsonData` first and `secureJsonData` directly afterwards from the extension component', () => { const message = "I'm a UI extension component!"; - const component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => { + const Component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => { useEffect(() => { context.setJsonData({ test: 'test' } as unknown as DataSourceJsonData); context.setSecureJsonData({ test: 'test' }); @@ -387,7 +387,7 @@ describe('', () => { pluginId: 'grafana-pdc-app', title: 'Example component', description: 'Example description', - component: component as unknown as React.ComponentType<{}>, + component: Component as unknown as React.ComponentType<{}>, }, '1' ), @@ -413,7 +413,7 @@ describe('', () => { it('should be possible to update the `secureJsonData` first and `jsonData` directly afterwards from the extension component', () => { const message = "I'm a UI extension component!"; - const component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => { + const Component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => { useEffect(() => { context.setSecureJsonData({ test: 'test' }); context.setJsonData({ test: 'test' } as unknown as DataSourceJsonData); @@ -432,7 +432,7 @@ describe('', () => { pluginId: 'grafana-pdc-app', title: 'Example component', description: 'Example description', - component: component as unknown as React.ComponentType<{}>, + component: Component as unknown as React.ComponentType<{}>, }, '1' ), From 6a050ff5514dd5dcc49a05c0ded828c2a8423fcb Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 26 Aug 2025 08:39:36 +0200 Subject: [PATCH 5/5] Alerting: Fix permission checks for the Import to GMA (#109950) Fix permission checks for the Import rules buttons and import to GMA page --- public/app/features/alerting/routes.tsx | 5 ++++- .../unified/components/rules/CloudRules.tsx | 9 +++++++-- .../unified/rule-list/RuleList.v2.test.tsx | 20 ++++++++++++------- .../unified/rule-list/RuleList.v2.tsx | 9 +++++++-- public/app/types/accessControl.ts | 5 +++++ 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index e51fd7cc27e..96bf0a4b421 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -226,7 +226,10 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { }, { path: '/alerting/import-datasource-managed-rules', - roles: () => ['Admin'], + roles: evaluateAccess([ + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingProvisioningSetStatus, + ]), component: config.featureToggles.alertingMigrationUI ? importAlertingComponent( () => diff --git a/public/app/features/alerting/unified/components/rules/CloudRules.tsx b/public/app/features/alerting/unified/components/rules/CloudRules.tsx index e92d40c2969..efb102eb7ea 100644 --- a/public/app/features/alerting/unified/components/rules/CloudRules.tsx +++ b/public/app/features/alerting/unified/components/rules/CloudRules.tsx @@ -6,6 +6,8 @@ import { GrafanaTheme2, urlUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Badge, LinkButton, LoadingPlaceholder, Pagination, Spinner, Stack, Text, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants'; @@ -14,7 +16,6 @@ import { usePagination } from '../../hooks/usePagination'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { getPaginationStyles } from '../../styles/pagination'; import { getRulesDataSources, getRulesSourceUid } from '../../utils/datasource'; -import { isAdmin } from '../../utils/misc'; import { isAsyncRequestStatePending } from '../../utils/redux'; import { createRelativeUrl } from '../../utils/url'; @@ -49,7 +50,11 @@ export const CloudRules = ({ namespaces, expandAll }: Props) => { DEFAULT_PER_PAGE_PAGINATION ); - const canMigrateToGMA = hasDataSourcesConfigured && isAdmin() && config.featureToggles.alertingMigrationUI; + const canMigrateToGMA = + hasDataSourcesConfigured && + config.featureToggles.alertingMigrationUI && + contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate) && + contextSrv.hasPermission(AccessControlAction.AlertingProvisioningSetStatus); return (
diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index 3b150a698df..33024dd123a 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -255,9 +255,12 @@ describe('RuleListActions', () => { describe('Import Alert Rules', () => { testWithFeatureToggles(['alertingMigrationUI']); - it('should show "Import alert rules" option when user is admin and feature toggle is enabled', async () => { - grantUserRole(OrgRole.Admin); - grantUserPermissions([AccessControlAction.AlertingRuleRead]); + it('should show "Import alert rules" option when user has required permissions and feature toggle is enabled', async () => { + grantUserPermissions([ + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingProvisioningSetStatus, + ]); const { user } = render(); @@ -267,8 +270,8 @@ describe('RuleListActions', () => { expect(ui.menuOptions.importAlertRules.query(menu)).toBeInTheDocument(); }); - it('should not show "Import alert rules" option when user is not admin', async () => { - // Keep default Viewer role + it('should not show "Import alert rules" option when user lacks required permissions', async () => { + // Keep default Viewer role and only read permissions grantUserPermissions([AccessControlAction.AlertingRuleRead]); const { user } = render(); @@ -280,8 +283,11 @@ describe('RuleListActions', () => { }); it('should have correct URL for "Import alert rules" menu item', async () => { - grantUserRole(OrgRole.Admin); - grantUserPermissions([AccessControlAction.AlertingRuleRead]); + grantUserPermissions([ + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingProvisioningSetStatus, + ]); const { user } = render(); diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index 7018bc3f9c9..e828b00fa16 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -4,6 +4,8 @@ import { useToggle } from 'react-use'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import { GrafanaRulesExporter } from '../components/export/GrafanaRulesExporter'; @@ -11,7 +13,6 @@ import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelecto import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton'; import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; import { useRulesFilter } from '../hooks/useFilteredRules'; -import { isAdmin } from '../utils/misc'; import { FilterView } from './FilterView'; import { GroupedView } from './GroupedView'; @@ -44,7 +45,11 @@ export function RuleListActions() { const canExportRules = exportRulesSupported && exportRulesAllowed; const canCreateRules = canCreateGrafanaRules || canCreateCloudRules; - const canImportRulesToGMA = isAdmin() && config.featureToggles.alertingMigrationUI; + // Align import UI permission with convert endpoint requirements: rule create + provisioning set status + const canImportRulesToGMA = + config.featureToggles.alertingMigrationUI && + contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate) && + contextSrv.hasPermission(AccessControlAction.AlertingProvisioningSetStatus); const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index 1f86e39ea34..7ef86382edc 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -129,6 +129,11 @@ export enum AccessControlAction { AlertingProvisioningReadSecrets = 'alert.provisioning.secrets:read', AlertingProvisioningRead = 'alert.provisioning:read', AlertingProvisioningWrite = 'alert.provisioning:write', + AlertingRulesProvisioningRead = 'alert.rules.provisioning:read', + AlertingRulesProvisioningWrite = 'alert.rules.provisioning:write', + AlertingNotificationsProvisioningRead = 'alert.notifications.provisioning:read', + AlertingNotificationsProvisioningWrite = 'alert.notifications.provisioning:write', + AlertingProvisioningSetStatus = 'alert.provisioning.provenance:write', // Alerting receivers actions AlertingReceiversPermissionsRead = 'receivers.permissions:read',