Merge remote-tracking branch 'origin/main' into alerting/llm-experiment

pull/107367/head
Sonia Aguilar 2 weeks ago
commit 9e441a5139
  1. 164
      .betterer.results
  2. 4
      e2e/cloud-plugins-suite/azure-monitor.spec.ts
  3. 4
      package.json
  4. 2
      packages/grafana-data/src/types/featureToggles.gen.ts
  5. 3
      packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
  6. 2
      pkg/login/social/connectors/gitlab_oauth.go
  7. 35
      pkg/login/social/connectors/gitlab_oauth_test.go
  8. 4
      pkg/services/featuremgmt/registry.go
  9. 2
      pkg/services/featuremgmt/toggles_gen.csv
  10. 11
      pkg/services/featuremgmt/toggles_gen.json
  11. 2
      public/app/core/components/NestedFolderPicker/NestedFolderList.tsx
  12. 77
      public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
  13. 5
      public/app/features/alerting/unified/components/rules/RuleDetails.test.tsx
  14. 108
      public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
  15. 172
      public/app/features/alerting/unified/hooks/useAbilities.ts
  16. 8
      public/app/features/alerting/unified/rule-list/FilterView.tsx
  17. 182
      public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx
  18. 98
      public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx
  19. 71
      public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx
  20. 150
      public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx
  21. 74
      public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx
  22. 8
      public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts
  23. 4
      public/app/features/alerting/unified/utils/rules.ts
  24. 3
      public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
  25. 3
      public/app/features/browse-dashboards/RecentlyDeletedPage.tsx
  26. 2
      public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
  27. 3
      public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
  28. 8
      public/app/features/browse-dashboards/components/BrowseView.tsx
  29. 2
      public/app/features/browse-dashboards/components/NameCell.tsx
  30. 3
      public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx
  31. 3
      public/app/features/browse-dashboards/components/SearchView.tsx
  32. 3
      public/app/features/browse-dashboards/state/index.ts
  33. 2
      public/app/features/canvas/element.ts
  34. 2
      public/app/features/canvas/elements/button.tsx
  35. 2
      public/app/features/canvas/elements/cloud.tsx
  36. 4
      public/app/features/canvas/elements/droneFront.tsx
  37. 4
      public/app/features/canvas/elements/droneSide.tsx
  38. 4
      public/app/features/canvas/elements/droneTop.tsx
  39. 5
      public/app/features/canvas/elements/icon.tsx
  40. 2
      public/app/features/canvas/elements/parallelogram.tsx
  41. 5
      public/app/features/canvas/elements/server/server.tsx
  42. 2
      public/app/features/canvas/elements/triangle.tsx
  43. 4
      public/app/features/canvas/elements/windTurbine.tsx
  44. 2
      public/app/features/canvas/runtime/element.tsx
  45. 2
      public/app/features/canvas/runtime/frame.tsx
  46. 2
      public/app/features/canvas/runtime/scene.tsx
  47. 14
      public/app/features/connections/Connections.tsx
  48. 2
      public/app/features/connections/pages/DataSourcesListPage.tsx
  49. 6
      public/app/features/connections/pages/index.tsx
  50. 10
      public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx
  51. 2
      public/app/features/correlations/Forms/AddCorrelationForm.tsx
  52. 2
      public/app/features/correlations/Forms/EditCorrelationForm.tsx
  53. 2
      public/app/features/correlations/components/Wizard/index.ts
  54. 2
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  55. 40
      public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx
  56. 3
      public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
  57. 2
      public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx
  58. 13
      public/app/features/dashboard-scene/settings/VersionsEditView.tsx
  59. 2
      public/app/features/dashboard-scene/settings/annotations/index.tsx
  60. 5
      public/app/features/dashboard-scene/settings/version-history/index.ts
  61. 2
      public/app/features/dashboard/components/AnnotationSettings/index.tsx
  62. 3
      public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx
  63. 3
      public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx
  64. 9
      public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
  65. 2
      public/app/features/dashboard/components/LinksSettings/index.tsx
  66. 2
      public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx
  67. 2
      public/app/features/dataframe-import/index.ts
  68. 2
      public/app/features/datasources/components/DataSourceDashboards.tsx
  69. 3
      public/app/features/datasources/components/DataSourcesList.tsx
  70. 3
      public/app/features/datasources/components/DataSourcesListHeader.tsx
  71. 6
      public/app/features/datasources/components/EditDataSource.tsx
  72. 2
      public/app/features/datasources/components/EditDataSourceActions.tsx
  73. 9
      public/app/features/datasources/components/NewDataSource.tsx
  74. 6
      public/app/features/datasources/components/picker/DataSourceModal.tsx
  75. 6
      public/app/features/datasources/state/index.ts
  76. 4
      public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
  77. 6
      public/app/features/dimensions/editors/index.ts
  78. 9
      public/app/features/dimensions/index.ts
  79. 12
      public/app/features/dimensions/utils.ts
  80. 14
      public/app/features/explore/TraceView/TraceView.tsx
  81. 2
      public/app/features/explore/TraceView/components/CriticalPath/index.tsx
  82. 4
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts
  83. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts
  84. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts
  85. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts
  86. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts
  87. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts
  88. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts
  89. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts
  90. 3
      public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts
  91. 2
      public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx
  92. 2
      public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx
  93. 2
      public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts
  94. 2
      public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx
  95. 2
      public/app/features/explore/TraceView/components/ScrollManager.tsx
  96. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx
  97. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx
  98. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
  99. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx
  100. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1496,22 +1496,9 @@ exports[`better eslint`] = {
"public/app/features/browse-dashboards/components/NewFolderForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/browse-dashboards/state/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"]
],
"public/app/features/connections/components/ConnectionsRedirectNotice/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"]
],
"public/app/features/connections/pages/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./AddNewConnectionPage\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./DataSourceDashboardsPage\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./DataSourceDetailsPage\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./DataSourcesListPage\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./EditDataSourcePage\`)", "4"],
[0, 0, 0, "Do not re-export imported variable (\`./NewDataSourcePage\`)", "5"]
],
"public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.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"]
],
@ -1565,10 +1552,6 @@ exports[`better eslint`] = {
"public/app/features/correlations/components/EmptyCorrelationsCTA.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"]
],
"public/app/features/correlations/components/Wizard/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"]
],
"public/app/features/correlations/mocks/useCorrelations.mocks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -1727,10 +1710,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "4"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"]
],
"public/app/features/dashboard-scene/settings/annotations/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsEdit\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsList\`)", "1"]
],
"public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field 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 Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"],
@ -1778,13 +1757,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard-scene/settings/version-history/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./HistorySrv\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryButtons\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryComparison\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryHeader\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryTable\`)", "4"]
],
"public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
@ -1867,10 +1839,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"]
],
"public/app/features/dashboard/components/AnnotationSettings/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsEdit\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./AnnotationSettingsList\`)", "1"]
],
"public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -1953,10 +1921,6 @@ exports[`better eslint`] = {
"public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard/components/LinksSettings/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./LinkSettingsEdit\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./LinkSettingsList\`)", "1"]
],
"public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx:5381": [
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
@ -2225,10 +2189,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/features/dataframe-import/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"]
],
"public/app/features/datasources/components/DataSourceTypeCard.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"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
@ -2248,14 +2208,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/datasources/state/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"]
],
"public/app/features/datasources/state/navModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@ -2296,24 +2248,6 @@ exports[`better eslint`] = {
"public/app/features/dimensions/editors/ValueMappingsEditor/ValueMappingsEditor.tsx:5381": [
[0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
],
"public/app/features/dimensions/editors/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"]
],
"public/app/features/dimensions/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"]
],
"public/app/features/dimensions/scale.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -2359,17 +2293,6 @@ exports[`better eslint`] = {
"public/app/features/explore/TraceView/components/demo/trace-generators.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/TraceView/components/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./TracePageHeader\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./TraceTimelineViewer/SpanDetail/DetailState\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./TraceTimelineViewer\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./model/transform-trace-data\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./settings/SpanBarSettings\`)", "4"],
[0, 0, 0, "Do not re-export imported variable (\`./utils/filter-spans\`)", "5"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "8"]
],
"public/app/features/explore/TraceView/components/model/ddg/types.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./PathElem\`)", "0"]
],
@ -2390,21 +2313,9 @@ exports[`better eslint`] = {
"public/app/features/explore/TraceView/components/model/transform-trace-data.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/TraceView/components/types/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`../settings/SpanBarSettings\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./TNil\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./TTraceTimeline\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./links\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "4"]
],
"public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./DraggableManagerDemo\`)", "0"]
],
"public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./DraggableManager\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./EUpdateTypes\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"]
],
"public/app/features/explore/TraceView/createSpanLink.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
@ -2599,23 +2510,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"]
],
"public/app/features/plugins/admin/components/Badges/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./PluginDeprecatedBadge\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./PluginDisabledBadge\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./PluginEnterpriseBadge\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./PluginInstallBadge\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./PluginUpdateAvailableBadge\`)", "4"]
],
"public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/plugins/admin/components/GetStartedWithPlugin/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./GetStartedWithPlugin\`)", "0"]
],
"public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./InstallControlsButton\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./InstallControlsWarning\`)", "1"]
],
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -3139,10 +3039,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/azuremonitor/azureMetadata/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"]
],
"public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./ArgQueryEditor\`)", "0"]
],
@ -3224,11 +3120,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/azuremonitor/mocks/variables.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/azuremonitor/types/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"]
],
"public/app/plugins/datasource/azuremonitor/types/query.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "1"],
@ -3260,23 +3151,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloud-monitoring/components/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./Aggregation\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./AliasBy\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./AlignmentFunction\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./Alignment\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./AnnotationsHelp\`)", "4"],
[0, 0, 0, "Do not re-export imported variable (\`./Fields\`)", "5"],
[0, 0, 0, "Do not re-export imported variable (\`./GroupBy\`)", "6"],
[0, 0, 0, "Do not re-export imported variable (\`./LabelFilter\`)", "7"],
[0, 0, 0, "Do not re-export imported variable (\`./MQLQueryEditor\`)", "8"],
[0, 0, 0, "Do not re-export imported variable (\`./MetricQueryEditor\`)", "9"],
[0, 0, 0, "Do not re-export imported variable (\`./PeriodSelect\`)", "10"],
[0, 0, 0, "Do not re-export imported variable (\`./Preprocessor\`)", "11"],
[0, 0, 0, "Do not re-export imported variable (\`./Project\`)", "12"],
[0, 0, 0, "Do not re-export imported variable (\`./SLOQueryEditor\`)", "13"],
[0, 0, 0, "Do not re-export imported variable (\`./VisualMetricQueryEditor\`)", "14"]
],
"public/app/plugins/datasource/cloud-monitoring/datasource.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@ -3374,16 +3248,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./commentOnlyQuery\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./empty\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./filterQuery\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./multiLineFullQuery\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./newCommandQuery\`)", "4"],
[0, 0, 0, "Do not re-export imported variable (\`./singleLineFullQuery\`)", "5"],
[0, 0, 0, "Do not re-export imported variable (\`./sortQuery\`)", "6"],
[0, 0, 0, "Do not re-export imported variable (\`./whitespaceQuery\`)", "7"]
],
"public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -3426,13 +3290,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "16"],
[0, 0, 0, "Do not use any type assertions.", "17"]
],
"public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./multiLineFullQuery\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./multiLineIncompleteQueryWithoutNamespace\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./singleLineEmptyQuery\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./singleLineFullQuery\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./singleLineTwoQueries\`)", "4"]
],
"public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -3454,24 +3311,12 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./afterLabelValue\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./insideLabelValue\`)", "1"]
],
"public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/afterFunctionQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./afterFunctionQuery\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./secondArgAfterSearchQuery\`)", "1"],
[0, 0, 0, "Do not re-export imported variable (\`./secondArgQuery\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`./singleLineEmptyQuery\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./thirdArgAfterSearchQuery\`)", "4"],
[0, 0, 0, "Do not re-export imported variable (\`./withinStringQuery\`)", "5"]
],
"public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/secondArgAfterSearchQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -3579,11 +3424,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/elasticsearch/test-helpers/render.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/elasticsearch/types.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./dataquery.gen\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`ElasticsearchQuery\`)", "1"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"]
],
"public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field 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 Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"],
@ -3624,10 +3464,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/grafana-testdata-datasource/components/SimulationSchemaForm.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./RandomWalkEditor\`)", "0"],
[0, 0, 0, "Do not re-export imported variable (\`./StreamingClientEditor\`)", "1"]
],
"public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]

@ -5,11 +5,11 @@ import { v4 as uuidv4 } from 'uuid';
import { selectors as rawSelectors } from '@grafana/e2e-selectors';
import { selectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors';
import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/types/query';
import {
AzureMonitorDataSourceJsonData,
AzureMonitorDataSourceSecureJsonData,
AzureQueryType,
} from '../../public/app/plugins/datasource/azuremonitor/types';
} from '../../public/app/plugins/datasource/azuremonitor/types/types';
import { e2e } from '../utils';
const provisioningPath = `provisioning/datasources/azmonitor-ds.yaml`;

@ -380,7 +380,7 @@
"react": "18.3.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "18.3.1",
"react-draggable": "4.4.6",
"react-draggable": "4.5.0",
"react-dropzone": "^14.2.3",
"react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch",
"react-highlight-words": "0.21.0",
@ -416,7 +416,7 @@
"slate": "0.47.9",
"slate-plain-serializer": "0.7.13",
"slate-react": "0.22.10",
"swagger-ui-react": "5.25.2",
"swagger-ui-react": "5.26.1",
"symbol-observable": "4.0.0",
"systemjs": "6.15.1",
"tinycolor2": "1.6.0",

@ -979,7 +979,7 @@ export interface FeatureToggles {
restoreDashboards?: boolean;
/**
* Skip token rotation if it was already rotated less than 5 seconds ago
* @default false
* @default true
*/
skipTokenRotationIfRecent?: boolean;
/**

@ -226,9 +226,6 @@ class DataSourceWithBackend<
if (!(config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs)) {
console.warn('feature toggle queryServiceFromUI also requires the queryService to be running');
} else {
if (!hasExpr && dsUIDs.size === 1) {
// TODO? can we talk directly to the apiserver?
}
url = `/apis/query.grafana.app/v0alpha1/namespaces/${config.namespace}/query?ds_type=${this.type}`;
}
}

@ -302,6 +302,8 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client
data.Groups = userInfo.Groups
}
data.raw = rawJSON
s.log.Debug("Resolved user data", "data", fmt.Sprintf("%+v", data))
return &data, nil
}

@ -37,6 +37,8 @@ const (
rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":true,"namespace_id":1}`
editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":false,"namespace_id":1}`
editorUserIDToken = `{"sub":"3","preferred_username":"gitlab-editor","name":"Gitlab Editor","email":"gitlab-editor@example.org","email_verified":true,"groups_direct":["editors", "viewers"]}` // #nosec G101 not a hardcoded credential
adminGroup = `{"id":4,"web_url":"http://grafana-gitlab.local/groups/admins","name":"Admins","path":"admins","project_creation_level":"developer","full_name":"Admins","full_path":"admins","created_at":"2022-09-13T19:38:04.891Z"}`
editorGroup = `{"id":5,"web_url":"http://grafana-gitlab.local/groups/editors","name":"Editors","path":"editors","project_creation_level":"developer","full_name":"Editors","full_path":"editors","created_at":"2022-09-13T19:38:15.074Z"}`
viewerGroup = `{"id":6,"web_url":"http://grafana-gitlab.local/groups/viewers","name":"Viewers","path":"viewers","project_creation_level":"developer","full_name":"Viewers","full_path":"viewers","created_at":"2022-09-13T19:38:25.777Z"}`
@ -61,6 +63,7 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
GroupsRespBody string
GroupHeaders map[string]string
RoleAttributePath string
IDToken string
ExpectedLogin string
ExpectedEmail string
ExpectedRoles map[int64]org.RoleType
@ -180,6 +183,24 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{4: "Editor", 5: "Viewer"},
},
{
Name: "Maps roles from ID token attributes if available",
RoleAttributePath: `email=='gitlab-editor@example.org' && 'Editor' || 'Viewer'`,
IDToken: editorUserIDToken,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{1: "Editor"},
ExpectedGrafanaAdmin: nilPointer,
},
{
Name: "Maps groups from ID token groups if available",
RoleAttributePath: gitlabAttrPath,
IDToken: editorUserIDToken,
ExpectedLogin: "gitlab-editor",
ExpectedEmail: "gitlab-editor@example.org",
ExpectedRoles: map[int64]org.RoleType{1: "Editor"},
ExpectedGrafanaAdmin: nilPointer,
},
{
Name: "Should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled",
Cfg: conf{RoleAttributeStrict: true, OrgMapping: []string{"other:Org4:Editor"}},
@ -230,8 +251,17 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
require.Fail(t, "unexpected request URI: "+r.RequestURI)
}
}))
token := &oauth2.Token{}
if tt.IDToken != "" {
emptyJWTHeader := base64.RawURLEncoding.EncodeToString([]byte("{}"))
JWTBody := base64.RawURLEncoding.EncodeToString([]byte(tt.IDToken))
idToken := fmt.Sprintf("%s.%s.signature", emptyJWTHeader, JWTBody)
token = token.WithExtra(map[string]any{"id_token": idToken})
}
provider.info.ApiUrl = ts.URL + apiURI
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), &oauth2.Token{})
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token)
if tt.ExpectedError != nil {
require.ErrorIs(t, err, tt.ExpectedError)
return
@ -382,6 +412,9 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
}
for _, tc := range testCases {
if tc.wantUser != nil {
tc.wantUser.raw = []byte(tc.payload)
}
t.Run(tc.name, func(t *testing.T) {
// Create a test client with a dummy token
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})

@ -1678,11 +1678,11 @@ var (
{
Name: "skipTokenRotationIfRecent",
Description: "Skip token rotation if it was already rotated less than 5 seconds ago",
Stage: FeatureStagePrivatePreview,
Stage: FeatureStageGeneralAvailability,
Owner: identityAccessTeam,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
Expression: "true", // enabled by default
},
{
Name: "alertEnrichment",

@ -219,7 +219,7 @@ alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false
restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false
skipTokenRotationIfRecent,GA,@grafana/identity-access-team,false,false,false
alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
alertingAIGenAlertRules,experimental,@grafana/alerting-squad,false,false,false
alertingAIImproveAlertRules,experimental,@grafana/alerting-squad,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
219 alertingBulkActionsInUI GA @grafana/alerting-squad false false true
220 kubernetesAuthzApis experimental @grafana/identity-access-team false false false
221 restoreDashboards experimental @grafana/grafana-frontend-platform false false false
222 skipTokenRotationIfRecent privatePreview GA @grafana/identity-access-team false false false
223 alertEnrichment experimental @grafana/alerting-squad false false false
224 alertingAIGenAlertRules experimental @grafana/alerting-squad false false false
225 alertingAIImproveAlertRules experimental @grafana/alerting-squad false false false

@ -2865,16 +2865,19 @@
{
"metadata": {
"name": "skipTokenRotationIfRecent",
"resourceVersion": "1750434297879",
"creationTimestamp": "2025-06-03T06:59:40Z"
"resourceVersion": "1751872762065",
"creationTimestamp": "2025-06-03T06:59:40Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-07 07:19:22.065046 +0000 UTC"
}
},
"spec": {
"description": "Skip token rotation if it was already rotated less than 5 seconds ago",
"stage": "privatePreview",
"stage": "GA",
"codeowner": "@grafana/identity-access-team",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
"expression": "true"
}
},
{

@ -9,7 +9,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { IconButton, useStyles2, Text } from '@grafana/ui';
import { Indent } from 'app/core/components/Indent/Indent';
import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state';
import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state/hooks';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { DashboardViewItem } from 'app/features/search/types';
import { useSelector } from 'app/types';

@ -10,7 +10,12 @@ import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugin
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
import {
AlertRuleAction,
skipToken,
useGrafanaPromRuleAbilities,
useRulerRuleAbilities,
} from '../../hooks/useAbilities';
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { prometheusRuleType, rulerRuleType } from '../../utils/rules';
@ -33,6 +38,8 @@ interface Props {
/**
* Get a list of menu items + divider elements for rendering in an alert rule's
* dropdown menu
* If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions,
* as we have removed all requests to the ruler API in the list view.
*/
const AlertRuleMenu = ({
promRule,
@ -46,29 +53,51 @@ const AlertRuleMenu = ({
buttonSize,
fill,
}: Props) => {
// check all abilities and permissions
const [pauseSupported, pauseAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Pause);
const canPause = pauseSupported && pauseAllowed;
const [deleteSupported, deleteAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Delete);
const canDelete = deleteSupported && deleteAllowed;
const [duplicateSupported, duplicateAllowed] = useRulerRuleAbility(
rulerRule,
groupIdentifier,
AlertRuleAction.Duplicate
);
const canDuplicate = duplicateSupported && duplicateAllowed;
const [silenceSupported, silenceAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Silence);
const canSilence = silenceSupported && silenceAllowed;
const [exportSupported, exportAllowed] = useRulerRuleAbility(
rulerRule,
groupIdentifier,
AlertRuleAction.ModifyExport
);
const canExport = exportSupported && exportAllowed;
// check all abilities and permissions using rulerRule
const [rulerPauseAbility, rulerDeleteAbility, rulerDuplicateAbility, rulerSilenceAbility, rulerExportAbility] =
useRulerRuleAbilities(rulerRule, groupIdentifier, [
AlertRuleAction.Pause,
AlertRuleAction.Delete,
AlertRuleAction.Duplicate,
AlertRuleAction.Silence,
AlertRuleAction.ModifyExport,
]);
// check all abilities and permissions using promRule
const [
grafanaPauseAbility,
grafanaDeleteAbility,
grafanaDuplicateAbility,
grafanaSilenceAbility,
grafanaExportAbility,
] = useGrafanaPromRuleAbilities(prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken, [
AlertRuleAction.Pause,
AlertRuleAction.Delete,
AlertRuleAction.Duplicate,
AlertRuleAction.Silence,
AlertRuleAction.ModifyExport,
]);
const [pauseSupported, pauseAllowed] = rulerPauseAbility;
const [grafanaPauseSupported, grafanaPauseAllowed] = grafanaPauseAbility;
const canPause = (pauseSupported && pauseAllowed) || (grafanaPauseSupported && grafanaPauseAllowed);
const [deleteSupported, deleteAllowed] = rulerDeleteAbility;
const [grafanaDeleteSupported, grafanaDeleteAllowed] = grafanaDeleteAbility;
const canDelete = (deleteSupported && deleteAllowed) || (grafanaDeleteSupported && grafanaDeleteAllowed);
const [duplicateSupported, duplicateAllowed] = rulerDuplicateAbility;
const [grafanaDuplicateSupported, grafanaDuplicateAllowed] = grafanaDuplicateAbility;
const canDuplicate =
(duplicateSupported && duplicateAllowed) || (grafanaDuplicateSupported && grafanaDuplicateAllowed);
const [silenceSupported, silenceAllowed] = rulerSilenceAbility;
const [grafanaSilenceSupported, grafanaSilenceAllowed] = grafanaSilenceAbility;
const canSilence = (silenceSupported && silenceAllowed) || (grafanaSilenceSupported && grafanaSilenceAllowed);
const [exportSupported, exportAllowed] = rulerExportAbility;
const [grafanaExportSupported, grafanaExportAllowed] = grafanaExportAbility;
const canExport = (exportSupported && exportAllowed) || (grafanaExportSupported && grafanaExportAllowed);
const ruleExtensionLinks = useRulePluginLinkExtension(promRule, groupIdentifier);

@ -7,6 +7,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { getCloudRule, getGrafanaRule } from '../../mocks';
import { mimirDataSource } from '../../mocks/server/configure';
import { RuleDetails } from './RuleDetails';
@ -32,6 +33,8 @@ const ui = {
setupMswServer();
const { dataSource: mimirDs } = mimirDataSource();
beforeAll(() => {
jest.clearAllMocks();
});
@ -81,7 +84,7 @@ describe('RuleDetails RBAC', () => {
});
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });
const cloudRule = getCloudRule({ name: 'Cloud' }, { rulesSource: mimirDs });
it('Should not render Edit button for users with the update permission', async () => {
// Arrange

@ -4,7 +4,14 @@ import { byRole } from 'testing-library-selector';
import { setPluginLinksHook } from '@grafana/runtime';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { AlertRuleAction, useAlertRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
import {
AlertRuleAction,
useAlertRuleAbility,
useGrafanaPromRuleAbilities,
useGrafanaPromRuleAbility,
useRulerRuleAbilities,
useRulerRuleAbility,
} from '../../hooks/useAbilities';
import { getCloudRule, getGrafanaRule } from '../../mocks';
import { mimirDataSource } from '../../mocks/server/configure';
@ -13,11 +20,15 @@ import { RulesTable } from './RulesTable';
jest.mock('../../hooks/useAbilities');
const mocks = {
// This is a bit unfortunate, but we need to mock both abilities
// RuleActionButtons still needs to use the useAlertRuleAbility hook
// whereas AlertRuleMenu has already been refactored to use useRulerRuleAbility
// Mock the hooks that are actually used by the components:
// RuleActionsButtons uses: useAlertRuleAbility (singular)
// AlertRuleMenu uses: useRulerRuleAbilities and useGrafanaPromRuleAbilities (plural)
// We can also use useGrafanaPromRuleAbility (singular) for simpler mocking
useRulerRuleAbility: jest.mocked(useRulerRuleAbility),
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
useGrafanaPromRuleAbility: jest.mocked(useGrafanaPromRuleAbility),
useRulerRuleAbilities: jest.mocked(useRulerRuleAbilities),
useGrafanaPromRuleAbilities: jest.mocked(useGrafanaPromRuleAbilities),
};
setPluginLinksHook(() => ({
@ -46,18 +57,40 @@ describe('RulesTable RBAC', () => {
jest.clearAllMocks();
jest.restoreAllMocks();
jest.resetAllMocks();
// Set up default neutral mocks for all hooks
// Singular hooks (used by RuleActionsButtons and can simplify mocking)
mocks.useAlertRuleAbility.mockReturnValue([false, false]);
mocks.useRulerRuleAbility.mockReturnValue([false, false]);
mocks.useGrafanaPromRuleAbility.mockReturnValue([false, false]);
// Plural hooks (used by AlertRuleMenu) - need to return arrays based on input actions
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map(() => [false, false]);
});
mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => {
return actions.map(() => [false, false]);
});
});
describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
it('Should not render Edit button for users without the update permission', async () => {
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
// Mock the specific hooks needed for Grafana rules
// Using singular hook for simpler mocking
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
mocks.useGrafanaPromRuleAbility.mockImplementation((rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
// Still need plural hook for AlertRuleMenu component
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
});
render(<RulesTable rules={[grafanaRule]} />);
@ -65,11 +98,14 @@ describe('RulesTable RBAC', () => {
});
it('Should not render Delete button for users without the delete permission', async () => {
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
// Mock the specific hooks needed for Grafana rules
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
});
render(<RulesTable rules={[grafanaRule]} />);
@ -80,11 +116,14 @@ describe('RulesTable RBAC', () => {
});
it('Should render Edit button for users with the update permission', async () => {
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
// Mock the specific hooks needed for Grafana rules
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
});
render(<RulesTable rules={[grafanaRule]} />);
@ -93,11 +132,14 @@ describe('RulesTable RBAC', () => {
});
it('Should render Delete button for users with the delete permission', async () => {
mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => {
// Mock the specific hooks needed for Grafana rules
mocks.useAlertRuleAbility.mockImplementation((rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
});
render(<RulesTable rules={[grafanaRule]} />);
@ -123,11 +165,15 @@ describe('RulesTable RBAC', () => {
};
beforeEach(() => {
mocks.useRulerRuleAbility.mockImplementation(() => {
return [true, true];
// Mock all hooks needed for the creating/deleting state tests
mocks.useRulerRuleAbility.mockImplementation(() => [true, true]);
mocks.useAlertRuleAbility.mockImplementation(() => [true, true]);
// Mock plural hooks for AlertRuleMenu
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map(() => [true, true]);
});
mocks.useAlertRuleAbility.mockImplementation(() => {
return [true, true];
mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => {
return actions.map(() => [true, true]);
});
});
@ -164,6 +210,12 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
});
render(<RulesTable rules={[cloudRule]} />);
@ -177,6 +229,12 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
});
render(<RulesTable rules={[cloudRule]} />);
@ -191,6 +249,12 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
});
render(<RulesTable rules={[cloudRule]} />);
@ -204,6 +268,12 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
// Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken)
mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => {
return actions.map((action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
});
render(<RulesTable rules={[cloudRule]} />);

@ -14,15 +14,20 @@ import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { GrafanaPromRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
import { getRulesSourceName } from '../utils/datasource';
import { getGroupOriginName } from '../utils/groupIdentifier';
import { getGroupOriginName, groupIdentifier } from '../utils/groupIdentifier';
import { isAdmin } from '../utils/misc';
import { isFederatedRuleGroup, isPluginProvidedRule, rulerRuleType } from '../utils/rules';
import {
isPluginProvidedRule,
isProvisionedPromRule,
isProvisionedRule,
prometheusRuleType,
rulerRuleType,
} from '../utils/rules';
import { useIsRuleEditable } from './useIsRuleEditable';
@ -200,7 +205,7 @@ export function useRulerRuleAbility(
}
export function useRulerRuleAbilities(
rule: RulerRuleDTO,
rule: RulerRuleDTO | undefined,
groupIdentifier: RuleGroupIdentifierV2,
actions: AlertRuleAction[]
): Ability[] {
@ -211,28 +216,35 @@ export function useRulerRuleAbilities(
}, [abilities, actions]);
}
// This hook is being called a lot in different places
// In some cases multiple times for ~80 rules (e.g. on the list page)
// We need to investigate further if some of these calls are redundant
// In the meantime, memoizing the result helps
/**
* @deprecated Use {@link useAllRulerRuleAbilities} instead
*/
export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRuleAction> {
const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource);
// This hook is being called a lot in different places
// In some cases multiple times for ~80 rules (e.g. on the list page)
// We need to investigate further if some of these calls are redundant
// In the meantime, memoizing the result helps
const groupIdentifierV2 = useMemo(() => groupIdentifier.fromCombinedRule(rule), [rule]);
return useAllRulerRuleAbilities(rule.rulerRule, groupIdentifierV2);
}
const {
isEditable,
isRemovable,
isRulerAvailable = false,
loading,
} = useIsRuleEditable(rulesSourceName, rule.rulerRule);
export function useAllRulerRuleAbilities(
rule: RulerRuleDTO | undefined,
groupIdentifier: RuleGroupIdentifierV2
): Abilities<AlertRuleAction> {
const rulesSourceName = getGroupOriginName(groupIdentifier);
const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule);
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const canSilence = useCanSilence(rule.rulerRule);
const canSilence = useCanSilence(rule);
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
const isProvisioned =
rulerRuleType.grafana.rule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const isFederated = isFederatedRuleGroup(rule.group);
const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule.rulerRule);
const isPluginProvided = isPluginProvidedRule(rule.rulerRule);
const isProvisioned = rule ? isProvisionedRule(rule) : false;
// TODO: Add support for federated rules
// const isFederated = isFederatedRuleGroup();
const isFederated = false;
const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule);
const isPluginProvided = isPluginProvidedRule(rule);
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
const immutableRule = isProvisioned || isFederated || isPluginProvided;
@ -263,39 +275,44 @@ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRul
};
return abilities;
}, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]);
}, [rule, loading, isRulerAvailable, rulesSourceName, isEditable, isRemovable, canSilence, exportAllowed]);
return abilities;
}
export function useAllRulerRuleAbilities(
rule: RulerRuleDTO | undefined,
groupIdentifier: RuleGroupIdentifierV2
): Abilities<AlertRuleAction> {
const rulesSourceName = getGroupOriginName(groupIdentifier);
const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule);
/**
* Hook for checking abilities on Grafana Prometheus rules (GrafanaPromRuleDTO)
* This is the next version of useAllRulerRuleAbilities designed to work with GrafanaPromRuleDTO
*/
export function useAllGrafanaPromRuleAbilities(rule: GrafanaPromRuleDTO | undefined): Abilities<AlertRuleAction> {
// For GrafanaPromRuleDTO, we use useIsGrafanaPromRuleEditable instead
const { isEditable, isRemovable, loading } = useIsGrafanaPromRuleEditable(rule); // duplicate
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const canSilence = useCanSilence(rule);
const silenceSupported = useGrafanaRulesSilenceSupport();
const canSilenceInFolder = useCanSilenceInFolder(rule?.folderUid);
const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
const isProvisioned = rulerRuleType.grafana.rule(rule) && Boolean(rule.grafana_alert.provenance);
// const isFederated = isFederatedRuleGroup();
const isProvisioned = rule ? isProvisionedPromRule(rule) : false;
// Note: Grafana managed rules can't be federated - this is strictly a Mimir feature
// See: https://grafana.com/docs/mimir/latest/references/architecture/components/ruler/#federated-rule-groups
const isFederated = false;
const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule);
// All GrafanaPromRuleDTO rules are Grafana-managed by definition
const isAlertingRule = prometheusRuleType.grafana.alertingRule(rule);
const isPluginProvided = isPluginProvidedRule(rule);
// if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
const immutableRule = isProvisioned || isFederated || isPluginProvided;
// while we gather info, pretend it's not supported
const MaybeSupported = loading ? NotSupported : isRulerAvailable;
// GrafanaPromRuleDTO rules are always supported (no loading state for ruler availability)
const MaybeSupported = loading ? NotSupported : AlwaysSupported;
const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;
// Creating duplicates of plugin-provided rules does not seem to make a lot of sense
const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;
const rulesPermissions = getRulesPermissions(rulesSourceName);
const rulesPermissions = getRulesPermissions('grafana');
const abilities: Abilities<AlertRuleAction> = {
[AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
@ -303,22 +320,91 @@ export function useAllRulerRuleAbilities(
[AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
[AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
[AlertRuleAction.Silence]: canSilence,
[AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
[AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
[AlertRuleAction.Silence]: [silenceSupported, canSilenceInFolder && isAlertingRule],
[AlertRuleAction.ModifyExport]: [isAlertingRule, exportAllowed],
[AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false],
[AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false],
[AlertRuleAction.DeletePermanently]: [
MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule,
MaybeSupportedUnlessImmutable && isAlertingRule,
(isRemovable && isAdmin()) ?? false,
],
};
return abilities;
}, [rule, loading, isRulerAvailable, rulesSourceName, isEditable, isRemovable, canSilence, exportAllowed]);
}, [rule, loading, isEditable, isRemovable, canSilenceInFolder, exportAllowed, silenceSupported]);
return abilities;
}
interface IsGrafanaPromRuleEditableResult {
isEditable: boolean;
isRemovable: boolean;
loading: boolean;
}
/**
* Hook for checking if a GrafanaPromRuleDTO is editable
* Adapted version of useIsRuleEditable for GrafanaPromRuleDTO
*/
function useIsGrafanaPromRuleEditable(rule?: GrafanaPromRuleDTO): IsGrafanaPromRuleEditableResult {
const folderUID = rule?.folderUid;
const { folder, loading } = useFolder(folderUID);
return useMemo(() => {
if (!rule || !folderUID) {
return { isEditable: false, isRemovable: false, loading: false };
}
if (!folder) {
// Loading or invalid folder UID
return {
isEditable: false,
isRemovable: false,
loading,
};
}
// For Grafana-managed rules, check folder permissions
const rulesPermissions = getRulesPermissions('grafana');
const canEditGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.update, folder);
const canRemoveGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.delete, folder);
return {
isEditable: canEditGrafanaRules,
isRemovable: canRemoveGrafanaRules,
loading,
};
}, [rule, folderUID, folder, loading]);
}
export const skipToken = Symbol('ability-skip-token');
type SkipToken = typeof skipToken;
/**
* Hook for checking a single ability on a GrafanaPromRuleDTO
*/
export function useGrafanaPromRuleAbility(rule: GrafanaPromRuleDTO | SkipToken, action: AlertRuleAction): Ability {
const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule);
return useMemo(() => {
return abilities[action];
}, [abilities, action]);
}
/**
* Hook for checking multiple abilities on a GrafanaPromRuleDTO
*/
export function useGrafanaPromRuleAbilities(
rule: GrafanaPromRuleDTO | SkipToken,
actions: AlertRuleAction[]
): Ability[] {
const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule);
return useMemo(() => {
return actions.map((action) => abilities[action]);
}, [abilities, actions]);
}
export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
const {
selectedAlertmanager,

@ -12,7 +12,7 @@ import { hashRule } from '../utils/rule-id';
import { DataSourceRuleLoader } from './DataSourceRuleLoader';
import { FilterProgressState, FilterStatus } from './FilterViewStatus';
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
import { GrafanaRuleListItem } from './GrafanaRuleListItem';
import LoadMoreHelper from './LoadMoreHelper';
import { UnknownRuleListItem } from './components/AlertRuleListItem';
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
@ -154,11 +154,11 @@ function FilterViewResults({ filterState }: FilterViewProps) {
switch (origin) {
case 'grafana':
return (
<GrafanaRuleLoader
key={key}
ruleIdentifier={{ ruleSourceName: 'grafana', uid: rule.uid }}
<GrafanaRuleListItem
rule={rule}
groupIdentifier={groupIdentifier}
namespaceName={ruleWithOrigin.namespaceName}
showLocation={true}
/>
);
case 'datasource':

@ -2,6 +2,7 @@ import { render } from 'test/test-utils';
import { byRole, byTitle } from 'testing-library-selector';
import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
import { AccessControlAction } from 'app/types';
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
import {
GrafanaPromRuleDTO,
@ -13,13 +14,13 @@ import {
} from 'app/types/unified-alerting-dto';
import { setupMswServer } from '../mockApi';
import { mockGrafanaPromAlertingRule, mockGrafanaRulerRule } from '../mocks';
import { grantUserPermissions } from '../mocks';
import { grafanaRulerGroup, grafanaRulerNamespace } from '../mocks/grafanaRulerApi';
import { setGrafanaPromRules } from '../mocks/server/configure';
import { setFolderAccessControl, setGrafanaPromRules } from '../mocks/server/configure';
import { rulerRuleType } from '../utils/rules';
import { intervalToSeconds } from '../utils/time';
import { GrafanaGroupLoader, matchRules } from './GrafanaGroupLoader';
import { GrafanaGroupLoader } from './GrafanaGroupLoader';
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
@ -32,9 +33,35 @@ const ui = {
ruleLink: (ruleName: string) => byRole('link', { name: ruleName }),
editButton: () => byRole('link', { name: 'Edit' }),
moreButton: () => byRole('button', { name: 'More' }),
// Menu items that appear when More button is clicked
menuItems: {
silence: () => byRole('menuitem', { name: /silence/i }),
duplicate: () => byRole('menuitem', { name: /duplicate/i }),
copyLink: () => byRole('menuitem', { name: /copy link/i }),
export: () => byRole('menuitem', { name: /export/i }),
delete: () => byRole('menuitem', { name: /delete/i }),
},
};
describe('GrafanaGroupLoader', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleDelete,
AccessControlAction.AlertingSilenceCreate,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
// Grant necessary permissions for editing rules
setFolderAccessControl({
[AccessControlAction.AlertingRuleUpdate]: true,
[AccessControlAction.AlertingRuleDelete]: true,
[AccessControlAction.AlertingSilenceCreate]: true,
[AccessControlAction.AlertingRuleCreate]: true, // For duplicate action
[AccessControlAction.AlertingRuleRead]: true, // For export action
});
});
it('should render rule with url when ruler and prom rule exist', async () => {
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
@ -55,8 +82,8 @@ describe('GrafanaGroupLoader', () => {
);
});
it('should render rule with url and creating state when only ruler rule exists', async () => {
setGrafanaPromRules([]);
it('should render More button with action menu options', async () => {
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
@ -65,92 +92,119 @@ describe('GrafanaGroupLoader', () => {
const [rule1] = grafanaRulerGroup.rules;
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
const creatingIcon = ui.ruleStatus('Creating').get(ruleListItem);
expect(creatingIcon).toBeInTheDocument();
// Check that More button is present
const moreButton = ui.moreButton().get(ruleListItem);
expect(moreButton).toBeInTheDocument();
const ruleLink = ui.ruleLink(rule1.grafana_alert.title).get(ruleListItem);
expect(ruleLink).toHaveAttribute(
'href',
expect.stringContaining(`/alerting/grafana/${rule1.grafana_alert.uid}/view`)
);
// Verify More button accessibility
expect(moreButton).toHaveAttribute('aria-label', 'More');
expect(moreButton).toHaveTextContent('More');
});
it('should render delete rule operation list item when only prom rule exists', async () => {
const promOnlyGroup: GrafanaPromRuleGroupDTO = {
...rulerGroupToPromGroup(grafanaRulerGroup),
name: 'prom-only-group',
it('should render multiple rules with their own action buttons', async () => {
// Create a group with multiple rules
const multiRuleGroup = {
...grafanaRulerGroup,
rules: [
grafanaRulerGroup.rules[0],
{
...grafanaRulerGroup.rules[0],
grafana_alert: {
...grafanaRulerGroup.rules[0].grafana_alert,
uid: 'second-rule-uid',
title: 'Second Rule',
},
},
],
};
setGrafanaPromRules([promOnlyGroup]);
setGrafanaPromRules([rulerGroupToPromGroup(multiRuleGroup)]);
const groupIdentifier = getGroupIdentifier(promOnlyGroup);
const groupIdentifier = getGroupIdentifier(multiRuleGroup);
render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />);
const [rule1] = promOnlyGroup.rules;
const promRule = await ui.ruleItem(rule1.name).find();
// Check first rule
const [rule1, rule2] = multiRuleGroup.rules;
const ruleListItem1 = await ui.ruleItem(rule1.grafana_alert.title).find();
const ruleListItem2 = await ui.ruleItem(rule2.grafana_alert.title).find();
// Each rule should have its own More button
expect(ui.moreButton().get(ruleListItem1)).toBeInTheDocument();
expect(ui.moreButton().get(ruleListItem2)).toBeInTheDocument();
const deletingIcon = ui.ruleStatus('Deleting').get(promRule);
expect(deletingIcon).toBeInTheDocument();
// Check that edit buttons are present and have correct URLs
const editButton1 = ui.editButton().get(ruleListItem1);
const editButton2 = ui.editButton().get(ruleListItem2);
expect(ui.editButton().query(promRule)).not.toBeInTheDocument();
expect(ui.moreButton().query(promRule)).not.toBeInTheDocument();
expect(editButton1).toBeInTheDocument();
expect(editButton2).toBeInTheDocument();
// Check that edit buttons have correct URLs (the actual format is simpler)
expect(editButton1).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule1.grafana_alert.uid}/edit`));
expect(editButton2).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule2.grafana_alert.uid}/edit`));
});
});
describe('matchRules', () => {
it('should return matches for all items and have empty promOnlyRules if all rules are matched by uid', () => {
const rulerRules = [
mockGrafanaRulerRule({ uid: '1' }),
mockGrafanaRulerRule({ uid: '2' }),
mockGrafanaRulerRule({ uid: '3' }),
];
it('should not render edit button when user lacks edit permissions', async () => {
// Override permissions to deny editing
setFolderAccessControl({
[AccessControlAction.AlertingRuleUpdate]: false,
[AccessControlAction.AlertingRuleDelete]: false,
});
const promRules = rulerRules.map(rulerRuleToPromRule);
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
const { matches, promOnlyRules } = matchRules(promRules, rulerRules);
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
expect(matches.size).toBe(rulerRules.length);
expect(promOnlyRules).toHaveLength(0);
render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />);
for (const [rulerRule, promRule] of matches) {
expect(rulerRule.grafana_alert.uid).toBe(promRule.uid);
}
const [rule1] = grafanaRulerGroup.rules;
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
// Edit button should not be present
expect(ui.editButton().query(ruleListItem)).not.toBeInTheDocument();
// More button should still be present (for other actions like viewing)
expect(ui.moreButton().get(ruleListItem)).toBeInTheDocument();
});
it('should return unmatched prometheus rules in promOnlyRules array', () => {
const rulerRules = [mockGrafanaRulerRule({ uid: '1' }), mockGrafanaRulerRule({ uid: '2' })];
it('should render correct menu actions when More button is clicked', async () => {
setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
const matchingPromRules = rulerRules.map(rulerRuleToPromRule);
const unmatchedPromRules = [mockGrafanaPromAlertingRule({ uid: '3' }), mockGrafanaPromAlertingRule({ uid: '4' })];
const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
const allPromRules = [...matchingPromRules, ...unmatchedPromRules];
const { matches, promOnlyRules } = matchRules(allPromRules, rulerRules);
const { user } = render(
<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />
);
expect(matches.size).toBe(rulerRules.length);
expect(promOnlyRules).toHaveLength(unmatchedPromRules.length);
expect(promOnlyRules).toEqual(expect.arrayContaining(unmatchedPromRules));
});
const [rule1] = grafanaRulerGroup.rules;
const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
// Find and click the More button
const moreButton = ui.moreButton().get(ruleListItem);
await user.click(moreButton);
// Check that the dropdown menu appears
const menu = byRole('menu').get();
expect(menu).toBeInTheDocument();
// With proper permissions, all 4 menu actions should be available:
it('should not include ruler rules in matches if they have no prometheus counterpart', () => {
const rulerRules = [
mockGrafanaRulerRule({ uid: '1' }),
mockGrafanaRulerRule({ uid: '2' }),
mockGrafanaRulerRule({ uid: '3' }),
];
// 1. Silence notifications - available for alerting rules (AlertingSilenceCreate permission)
expect(ui.menuItems.silence().get()).toBeInTheDocument();
// Only create prom rule for the second ruler rule
const promRules = [rulerRuleToPromRule(rulerRules[1])];
// 2. Copy link - always available
expect(ui.menuItems.copyLink().get()).toBeInTheDocument();
const { matches, promOnlyRules } = matchRules(promRules, rulerRules);
// 3. Duplicate - should be available with create permissions (AlertingRuleCreate permission)
expect(ui.menuItems.duplicate().get()).toBeInTheDocument();
expect(matches.size).toBe(1);
expect(promOnlyRules).toHaveLength(0);
// 4. Export - should be available for Grafana alerting rules (AlertingRuleRead permission)
expect(ui.menuItems.export().get()).toBeInTheDocument();
// Verify that only the second ruler rule is in matches
expect(matches.has(rulerRules[0])).toBe(false);
expect(matches.get(rulerRules[1])).toBe(promRules[0]);
expect(matches.has(rulerRules[2])).toBe(false);
// Verify that the menu contains all 4 expected menu items
const menuItems = byRole('menuitem').getAll();
expect(menuItems.length).toBe(4);
});
});

@ -1,22 +1,13 @@
import { useMemo } from 'react';
import { t } from '@grafana/i18n';
import { Alert } from '@grafana/ui';
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
import { GrafanaPromRuleDTO, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
import { logWarning } from '../Analytics';
import { alertRuleApi } from '../api/alertRuleApi';
import { prometheusApi } from '../api/prometheusApi';
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
import { GrafanaRulesSource } from '../utils/datasource';
import { GrafanaRuleListItem } from './GrafanaRuleLoader';
import { RuleOperationListItem } from './components/AlertRuleListItem';
import { GrafanaRuleListItem } from './GrafanaRuleListItem';
import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader';
import { RuleOperation } from './components/RuleListIcon';
const { useGetGrafanaRulerGroupQuery } = alertRuleApi;
const { useGetGrafanaGroupsQuery } = prometheusApi;
export interface GrafanaGroupLoaderProps {
@ -48,20 +39,8 @@ export function GrafanaGroupLoader({
},
{ pollingInterval: RULE_LIST_POLL_INTERVAL_MS }
);
const { data: rulerResponse, isLoading: isRulerGroupLoading } = useGetGrafanaRulerGroupQuery({
folderUid: groupIdentifier.namespace.uid,
groupName: groupIdentifier.groupName,
});
const { matches, promOnlyRules } = useMemo(() => {
const promRules = promResponse?.data.groups.at(0)?.rules ?? [];
const rulerRules = rulerResponse?.rules ?? [];
return matchRules(promRules, rulerRules);
}, [promResponse, rulerResponse]);
const isLoading = isPromResponseLoading || isRulerGroupLoading;
if (isLoading) {
if (isPromResponseLoading) {
return (
<>
{Array.from({ length: expectedRulesCount }).map((_, index) => (
@ -71,7 +50,7 @@ export function GrafanaGroupLoader({
);
}
if (!rulerResponse && !promResponse) {
if (!promResponse) {
return (
<Alert
title={t(
@ -86,28 +65,11 @@ export function GrafanaGroupLoader({
return (
<>
{rulerResponse?.rules.map((rulerRule) => {
const promRule = matches.get(rulerRule);
if (!promRule) {
return (
<GrafanaRuleListItem
key={rulerRule.grafana_alert.uid}
rule={promRule}
rulerRule={rulerRule}
groupIdentifier={groupIdentifier}
namespaceName={namespaceName}
operation={RuleOperation.Creating}
showLocation={false}
/>
);
}
{promResponse.data.groups.at(0)?.rules.map((promRule) => {
return (
<GrafanaRuleListItem
key={promRule.uid}
rule={promRule}
rulerRule={rulerRule}
groupIdentifier={groupIdentifier}
namespaceName={namespaceName}
// we don't show the location again for rules, it's redundant because they are shown in a folder > group hierarchy
@ -115,58 +77,6 @@ export function GrafanaGroupLoader({
/>
);
})}
{promOnlyRules.map((rule) => (
<RuleOperationListItem
key={rule.uid}
name={rule.name}
namespace={namespaceName}
group={groupIdentifier.groupName}
rulesSource={GrafanaRulesSource}
application="grafana"
operation={RuleOperation.Deleting}
showLocation={false}
/>
))}
</>
);
}
interface MatchingResult {
matches: Map<RulerGrafanaRuleDTO, GrafanaPromRuleDTO>;
/**
* Rules that were already removed from the Ruler but the changes has not been yet propagated to Prometheus
*/
promOnlyRules: GrafanaPromRuleDTO[];
}
export function matchRules(
promRules: GrafanaPromRuleDTO[],
rulerRules: RulerGrafanaRuleDTO[]
): Readonly<MatchingResult> {
const promRulesMap = new Map(promRules.map((rule) => [rule.uid, rule]));
const matchingResult = rulerRules.reduce<MatchingResult>(
(acc, rulerRule) => {
const { matches } = acc;
const promRule = promRulesMap.get(rulerRule.grafana_alert.uid);
if (promRule) {
matches.set(rulerRule, promRule);
promRulesMap.delete(rulerRule.grafana_alert.uid);
}
return acc;
},
{ matches: new Map(), promOnlyRules: [] }
);
matchingResult.promOnlyRules.push(...promRulesMap.values());
if (matchingResult.promOnlyRules.length > 0) {
// Grafana Prometheus rules should be strongly consistent now so each Ruler rule should have a matching Prometheus rule
// If not, log it as a warning
logWarning('Grafana Managed Rules: No matching Prometheus rule found for Ruler rule', {
promOnlyRulesCount: matchingResult.promOnlyRules.length.toString(),
});
}
return matchingResult;
}

@ -0,0 +1,71 @@
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto';
import { GrafanaRulesSource } from '../utils/datasource';
import { totalFromStats } from '../utils/ruleStats';
import { prometheusRuleType } from '../utils/rules';
import { createRelativeUrl } from '../utils/url';
import {
AlertRuleListItem,
RecordingRuleListItem,
RuleListItemCommonProps,
UnknownRuleListItem,
} from './components/AlertRuleListItem';
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
import { RuleOperation } from './components/RuleListIcon';
interface GrafanaRuleListItemProps {
rule: GrafanaPromRuleDTO;
groupIdentifier: GrafanaRuleGroupIdentifier;
namespaceName: string;
operation?: RuleOperation;
showLocation?: boolean;
}
export function GrafanaRuleListItem({
rule,
groupIdentifier,
namespaceName,
operation,
showLocation = true,
}: GrafanaRuleListItemProps) {
const { name, uid, labels, provenance } = rule;
const commonProps: RuleListItemCommonProps = {
name,
rulesSource: GrafanaRulesSource,
group: groupIdentifier.groupName,
namespace: namespaceName,
href: createRelativeUrl(`/alerting/grafana/${uid}/view`),
health: rule?.health,
error: rule?.lastError,
labels: labels,
isProvisioned: Boolean(provenance),
isPaused: rule?.isPaused,
application: 'grafana' as const,
actions: <RuleActionsButtons promRule={rule} groupIdentifier={groupIdentifier} compact />,
};
if (prometheusRuleType.grafana.alertingRule(rule)) {
const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined;
const instancesCount = totalFromStats(promAlertingRule?.totals ?? {});
return (
<AlertRuleListItem
{...commonProps}
summary={rule.annotations?.summary}
state={promAlertingRule?.state}
instancesCount={instancesCount}
operation={operation}
showLocation={showLocation}
/>
);
}
if (prometheusRuleType.grafana.recordingRule(rule)) {
return <RecordingRuleListItem {...commonProps} showLocation={showLocation} />;
}
return <UnknownRuleListItem ruleName={name} groupIdentifier={groupIdentifier} ruleDefinition={rule} />;
}

@ -1,150 +0,0 @@
import { Trans, t } from '@grafana/i18n';
import { Alert } from '@grafana/ui';
import { GrafanaRuleGroupIdentifier, GrafanaRuleIdentifier } from 'app/types/unified-alerting';
import { GrafanaPromRuleDTO, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../api/alertRuleApi';
import { prometheusApi } from '../api/prometheusApi';
import { createReturnTo } from '../hooks/useReturnTo';
import { GrafanaRulesSource } from '../utils/datasource';
import { totalFromStats } from '../utils/ruleStats';
import { rulerRuleType } from '../utils/rules';
import { createRelativeUrl } from '../utils/url';
import {
AlertRuleListItem,
RecordingRuleListItem,
RuleListItemCommonProps,
UnknownRuleListItem,
} from './components/AlertRuleListItem';
import { AlertRuleListItemSkeleton, RulerRuleLoadingError } from './components/AlertRuleListItemLoader';
import { RuleActionsButtons } from './components/RuleActionsButtons.V2';
import { RuleOperation } from './components/RuleListIcon';
const { useGetGrafanaRulerGroupQuery } = alertRuleApi;
const { useGetGrafanaGroupsQuery } = prometheusApi;
interface GrafanaRuleLoaderProps {
ruleIdentifier: GrafanaRuleIdentifier;
groupIdentifier: GrafanaRuleGroupIdentifier;
namespaceName: string;
}
export function GrafanaRuleLoader({ ruleIdentifier, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) {
const {
data: rulerRuleGroup,
error: rulerRuleGroupError,
isLoading: isRulerRuleGroupLoading,
} = useGetGrafanaRulerGroupQuery({
folderUid: groupIdentifier.namespace.uid,
groupName: groupIdentifier.groupName,
});
const {
data: promRuleGroup,
error: promRuleGroupError,
isLoading: isPromRuleGroupLoading,
} = useGetGrafanaGroupsQuery({
folderUid: groupIdentifier.namespace.uid,
groupName: groupIdentifier.groupName,
});
const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === ruleIdentifier.uid);
const promRule = promRuleGroup?.data.groups
.flatMap((group) => group.rules)
.find((promRule) => promRule.uid === ruleIdentifier.uid);
if (rulerRuleGroupError || promRuleGroupError) {
return <RulerRuleLoadingError ruleIdentifier={ruleIdentifier} error={rulerRuleGroupError || promRuleGroupError} />;
}
if (isRulerRuleGroupLoading || isPromRuleGroupLoading) {
return <AlertRuleListItemSkeleton />;
}
if (!rulerRule) {
return (
<Alert
title={t('alerting.rule-list.cannot-load-rule-details-for', 'Cannot load rule details for UID {{uid}}', {
uid: ruleIdentifier.uid,
})}
severity="error"
>
<Trans i18nKey="alerting.rule-list.cannot-find-rule-details-for">
Cannot find rule details for UID {{ uid: ruleIdentifier.uid ?? '<empty uid>' }}
</Trans>
</Alert>
);
}
return (
<GrafanaRuleListItem
rule={promRule}
rulerRule={rulerRule}
groupIdentifier={groupIdentifier}
namespaceName={namespaceName}
/>
);
}
interface GrafanaRuleListItemProps {
rule?: GrafanaPromRuleDTO;
rulerRule: RulerGrafanaRuleDTO;
groupIdentifier: GrafanaRuleGroupIdentifier;
namespaceName: string;
operation?: RuleOperation;
showLocation?: boolean;
}
export function GrafanaRuleListItem({
rule,
rulerRule,
groupIdentifier,
namespaceName,
operation,
showLocation = true,
}: GrafanaRuleListItemProps) {
const returnTo = createReturnTo();
const {
grafana_alert: { uid, title, provenance, is_paused },
annotations = {},
labels = {},
} = rulerRule;
const commonProps: RuleListItemCommonProps = {
name: title,
rulesSource: GrafanaRulesSource,
group: groupIdentifier.groupName,
namespace: namespaceName,
href: createRelativeUrl(`/alerting/grafana/${uid}/view`, { returnTo }),
health: rule?.health,
error: rule?.lastError,
labels: labels,
isProvisioned: Boolean(provenance),
isPaused: rule?.isPaused ?? is_paused,
application: 'grafana' as const,
actions: <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />,
showLocation,
};
if (rulerRuleType.grafana.alertingRule(rulerRule)) {
const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined;
const instancesCount = totalFromStats(promAlertingRule?.totals ?? {});
return (
<AlertRuleListItem
{...commonProps}
summary={annotations.summary}
state={promAlertingRule?.state}
instancesCount={instancesCount}
operation={operation}
/>
);
}
if (rulerRuleType.grafana.recordingRule(rulerRule)) {
return <RecordingRuleListItem {...commonProps} />;
}
return <UnknownRuleListItem ruleName={title} groupIdentifier={groupIdentifier} ruleDefinition={rulerRule} />;
}

@ -1,4 +1,5 @@
import { useState } from 'react';
import { RequireAtLeastOne } from 'type-fest';
import { Trans, t } from '@grafana/i18n';
import { LinkButton, Stack } from '@grafana/ui';
@ -6,24 +7,34 @@ import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule';
import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer';
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
import {
EditableRuleIdentifier,
GrafanaRuleIdentifier,
Rule,
RuleGroupIdentifierV2,
RuleIdentifier,
} from 'app/types/unified-alerting';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities';
import { logWarning } from '../../Analytics';
import { AlertRuleAction, skipToken, useGrafanaPromRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
import * as ruleId from '../../utils/rule-id';
import { isProvisionedRule, rulerRuleType } from '../../utils/rules';
import { isProvisionedPromRule, isProvisionedRule, prometheusRuleType, rulerRuleType } from '../../utils/rules';
import { createRelativeUrl } from '../../utils/url';
interface Props {
rule: RulerRuleDTO;
type RuleProps = RequireAtLeastOne<{
rule?: RulerRuleDTO;
promRule?: Rule;
}>;
type Props = RuleProps & {
groupIdentifier: RuleGroupIdentifierV2;
/**
* Should we show the buttons in a "compact" state?
* i.e. without text and using smaller button sizes
*/
compact?: boolean;
}
};
// For now this is just a copy of RuleActionsButtons.tsx but with the View button removed.
// This is only done to keep the new list behind a feature flag and limit changes in the existing components
@ -37,16 +48,26 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
>(undefined);
const isProvisioned = isProvisionedRule(rule);
const isProvisioned = getIsProvisioned(rule, promRule);
const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update);
// If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions,
// as we have removed all requests to the ruler API in the list view.
const [grafanaEditRuleSupported, grafanaEditRuleAllowed] = useGrafanaPromRuleAbility(
prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken,
AlertRuleAction.Update
);
const canEditRule = editRuleSupported && editRuleAllowed;
const canEditRule = (editRuleSupported && editRuleAllowed) || (grafanaEditRuleSupported && grafanaEditRuleAllowed);
const buttons: JSX.Element[] = [];
const buttonSize = compact ? 'sm' : 'md';
const identifier = ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule);
const identifier = getEditableIdentifier(groupIdentifier, rule, promRule);
if (!identifier) {
return null;
}
if (canEditRule) {
const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`);
@ -93,3 +114,38 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
</Stack>
);
}
function getIsProvisioned(rule?: RulerRuleDTO, promRule?: Rule): boolean {
if (rule) {
return isProvisionedRule(rule);
}
if (promRule) {
return isProvisionedPromRule(promRule);
}
return false;
}
function getEditableIdentifier(
groupIdentifier: RuleGroupIdentifierV2,
rule?: RulerRuleDTO,
promRule?: Rule
): EditableRuleIdentifier | undefined {
if (rule) {
return ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule);
}
if (prometheusRuleType.grafana.rule(promRule)) {
return {
ruleSourceName: 'grafana',
uid: promRule.uid,
} satisfies GrafanaRuleIdentifier;
}
logWarning('Unable to construct an editable rule identifier');
// Returning undefined is safer than throwing here as it allows the component to gracefully handle
// the error by returning null instead of crashing the entire component tree
return undefined;
}

@ -4,7 +4,6 @@ import { useDispatch } from 'app/types/store';
import { DataSourceRulesSourceIdentifier, RuleHealth } from 'app/types/unified-alerting';
import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../../api/alertRuleApi';
import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi';
const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi;
@ -83,13 +82,6 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions =
// Because the user waits a bit longer for the initial load but doesn't need to wait for each group to be loaded
if (hookOptions.populateCache) {
const cacheAndRulerPreload = response.data.groups.map(async (group) => {
dispatch(
alertRuleApi.util.prefetch(
'getGrafanaRulerGroup',
{ folderUid: group.folderUid, groupName: group.name },
{ force: true }
)
);
await dispatch(
prometheusApi.util.upsertQueryData(
'getGrafanaGroups',

@ -168,6 +168,10 @@ export function isProvisionedRule(rulerRule: RulerRuleDTO): boolean {
return isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
}
export function isProvisionedPromRule(promRule: PromRuleDTO): boolean {
return prometheusRuleType.grafana.rule(promRule) && Boolean(promRule.provenance);
}
export function isProvisionedRuleGroup(group: RulerRuleGroupDTO): boolean {
return group.rules.some((rule) => isProvisionedRule(rule));
}

@ -27,7 +27,8 @@ import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
import { setAllSelection, useHasSelection } from './state';
import { useHasSelection } from './state/hooks';
import { setAllSelection } from './state/slice';
// New Browse/Manage/Search Dashboards views for nested folders
const BrowseDashboardsPage = memo(() => {

@ -16,7 +16,8 @@ import { RecentlyDeletedActions } from './components/RecentlyDeletedActions';
import { RecentlyDeletedEmptyState } from './components/RecentlyDeletedEmptyState';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
import { setAllSelection, useHasSelection } from './state';
import { useHasSelection } from './state/hooks';
import { setAllSelection } from './state/slice';
const RecentlyDeletedPage = memo(() => {
const dispatch = useDispatch();

@ -25,7 +25,7 @@ import {
SaveDashboardResponseDTO,
} from 'app/types';
import { refetchChildren, refreshParents } from '../state';
import { refetchChildren, refreshParents } from '../state/actions';
import { DashboardTreeSelection } from '../types';
import { isProvisionedDashboard, isProvisionedFolder } from './isProvisioned';

@ -9,7 +9,8 @@ import { useDispatch } from 'app/types';
import { ShowModalReactEvent } from 'app/types/events';
import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI';
import { setAllSelection, useActionSelectionState } from '../../state';
import { useActionSelectionState } from '../../state/hooks';
import { setAllSelection } from '../../state/slice';
import { DashboardTreeSelection } from '../../types';
import { DeleteModal } from './DeleteModal';

@ -6,17 +6,15 @@ import { DashboardViewItem } from 'app/features/search/types';
import { useDispatch } from 'app/types';
import { PAGE_SIZE } from '../api/services';
import { fetchNextChildrenPage } from '../state/actions';
import {
useFlatTreeState,
useCheckboxSelectionState,
setFolderOpenState,
setItemSelectionState,
useChildrenByParentUIDState,
setAllSelection,
useBrowseLoadingStatus,
useLoadNextChildrenPage,
fetchNextChildrenPage,
} from '../state';
} from '../state/hooks';
import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice';
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';
import { DashboardsTree } from './DashboardsTree';

@ -10,7 +10,7 @@ import { getIconForItem } from 'app/features/search/service/utils';
import { Indent } from '../../../core/components/Indent/Indent';
import { FolderRepo } from '../../../core/components/NestedFolderPicker/FolderRepo';
import { useChildrenByParentUIDState } from '../state';
import { useChildrenByParentUIDState } from '../state/hooks';
import { DashboardsTreeCellProps } from '../types';
import { makeRowID } from './utils';

@ -12,7 +12,8 @@ import { ShowModalReactEvent } from 'app/types/events';
import { deletedDashboardsCache } from '../../search/service/deletedDashboardsCache';
import { useListDeletedDashboardsQuery, useRestoreDashboardMutation } from '../api/browseDashboardsAPI';
import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager';
import { clearFolders, setAllSelection, useActionSelectionState } from '../state';
import { useActionSelectionState } from '../state/hooks';
import { clearFolders, setAllSelection } from '../state/slice';
import { RestoreModal } from './RestoreModal';

@ -9,7 +9,8 @@ import { SearchStateManager } from 'app/features/search/state/SearchStateManager
import { DashboardViewItemKind, SearchState } from 'app/features/search/types';
import { useDispatch, useSelector } from 'app/types';
import { setAllSelection, setItemSelectionState, useHasSelection } from '../state';
import { useHasSelection } from '../state/hooks';
import { setAllSelection, setItemSelectionState } from '../state/slice';
interface SearchViewProps {
height: number;

@ -1,3 +0,0 @@
export * from './slice';
export * from './actions';
export * from './hooks';

@ -7,7 +7,7 @@ import { config } from 'app/core/config';
import { BackgroundConfig, Constraint, LineConfig, Placement } from 'app/plugins/panel/canvas/panelcfg.gen';
import { LineStyleConfig } from '../../plugins/panel/canvas/editor/LineStyleEditor';
import { DimensionContext } from '../dimensions';
import { DimensionContext } from '../dimensions/context';
import { StandardEditorConfig } from './types';

@ -6,7 +6,7 @@ import { t } from '@grafana/i18n';
import { TextDimensionMode } from '@grafana/schema';
import { Button, Spinner, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
import { APIEditor, APIEditorConfig } from 'app/plugins/panel/canvas/editor/element/APIEditor';
import { ButtonStyleConfig, ButtonStyleEditor } from 'app/plugins/panel/canvas/editor/element/ButtonStyleEditor';

@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';

@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors';
import { DimensionContext } from 'app/features/dimensions/context';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element';

@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors';
import { DimensionContext } from 'app/features/dimensions/context';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element';

@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors';
import { DimensionContext } from 'app/features/dimensions/context';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element';

@ -6,9 +6,10 @@ import { LinkModel } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { ResourceDimensionEditor } from 'app/features/dimensions/editors/ResourceDimensionEditor';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions/resource';
import { LineConfig } from 'app/plugins/panel/canvas/panelcfg.gen';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element';

@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';

@ -4,8 +4,9 @@ import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
import config from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor, ScalarDimensionEditor } from 'app/features/dimensions/editors';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps } from '../../element';

@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';

@ -4,8 +4,8 @@ import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors';
import { DimensionContext } from 'app/features/dimensions/context';
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element';

@ -15,7 +15,7 @@ import { t } from '@grafana/i18n';
import { ConfirmModal } from '@grafana/ui';
import { LayerElement } from 'app/core/components/Layers/types';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import {
BackgroundImageSize,
Constraint,

@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import { HorizontalConstraint, Placement, VerticalConstraint } from 'app/plugins/panel/canvas/panelcfg.gen';
import { LayerActionID } from 'app/plugins/panel/canvas/types';

@ -16,7 +16,7 @@ import {
} from '@grafana/schema';
import { Portal } from '@grafana/ui';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { DimensionContext } from 'app/features/dimensions/context';
import {
getColorDimensionFromData,
getResourceDimensionFromData,

@ -3,14 +3,12 @@ import { Navigate, Routes, Route, useLocation } from 'react-router-dom-v5-compat
import { StoreState, useSelector } from 'app/types';
import { ROUTES } from './constants';
import {
AddNewConnectionPage,
DataSourceDashboardsPage,
DataSourceDetailsPage,
DataSourcesListPage,
EditDataSourcePage,
NewDataSourcePage,
} from './pages';
import { AddNewConnectionPage } from './pages/AddNewConnectionPage';
import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage';
import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage';
import { DataSourcesListPage } from './pages/DataSourcesListPage';
import { EditDataSourcePage } from './pages/EditDataSourcePage';
import { NewDataSourcePage } from './pages/NewDataSourcePage';
function RedirectToAddNewConnection() {
const { search } = useLocation();

@ -2,7 +2,7 @@ import { Page } from 'app/core/components/Page/Page';
import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice';
import { DataSourceAddButton } from 'app/features/datasources/components/DataSourceAddButton';
import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList';
import { getDataSourcesCount } from 'app/features/datasources/state';
import { getDataSourcesCount } from 'app/features/datasources/state/selectors';
import { StoreState, useSelector } from 'app/types';
export function DataSourcesListPage() {

@ -1,6 +0,0 @@
export { AddNewConnectionPage } from './AddNewConnectionPage';
export { DataSourceDetailsPage } from './DataSourceDetailsPage';
export { DataSourcesListPage } from './DataSourcesListPage';
export { DataSourceDashboardsPage } from './DataSourceDashboardsPage';
export { EditDataSourcePage } from './EditDataSourcePage';
export { NewDataSourcePage } from './NewDataSourcePage';

@ -5,12 +5,10 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { featureEnabled } from '@grafana/runtime';
import { Card, Grid, useStyles2, Stack, Badge } from '@grafana/ui';
import {
PluginDeprecatedBadge,
PluginDisabledBadge,
PluginInstalledBadge,
PluginUpdateAvailableBadge,
} from 'app/features/plugins/admin/components/Badges';
import { PluginDeprecatedBadge } from 'app/features/plugins/admin/components/Badges/PluginDeprecatedBadge';
import { PluginDisabledBadge } from 'app/features/plugins/admin/components/Badges/PluginDisabledBadge';
import { PluginInstalledBadge } from 'app/features/plugins/admin/components/Badges/PluginInstallBadge';
import { PluginUpdateAvailableBadge } from 'app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge';
import { getBadgeColor } from 'app/features/plugins/admin/components/Badges/sharedStyles';
import { isPluginUpdatable } from 'app/features/plugins/admin/helpers';
import { CatalogPlugin } from 'app/features/plugins/admin/types';

@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { PanelContainer, useStyles2 } from '@grafana/ui';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { Wizard } from '../components/Wizard';
import { Wizard } from '../components/Wizard/Wizard';
import { useCorrelations } from '../useCorrelations';
import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm';

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { Wizard } from '../components/Wizard';
import { Wizard } from '../components/Wizard/Wizard';
import { Correlation } from '../types';
import { useCorrelations } from '../useCorrelations';

@ -1,2 +0,0 @@
export * from './Wizard';
export * from './types';

@ -59,7 +59,7 @@ import { buildGridItemForPanel, transformSaveModelToScene } from '../serializati
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { DashboardEditView } from '../settings/utils';
import { historySrv } from '../settings/version-history';
import { historySrv } from '../settings/version-history/HistorySrv';
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
import { isInCloneChain } from '../utils/clone';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';

@ -57,17 +57,38 @@ export class DashboardGridItem
public constructor(state: DashboardGridItemState) {
super(state);
this.addActivationHandler(() => this.handleVariableName());
this.addActivationHandler(() => this._activationHandler());
}
private _handleGridResize(newState: DashboardGridItemState, prevState: DashboardGridItemState) {
const itemCount = this.state.repeatedPanels?.length ?? 1;
const stateChange: Partial<DashboardGridItemState> = {};
private _activationHandler() {
this.handleVariableName();
return () => {
this._handleGridSizeUnsubscribe();
};
}
private _handleGridSizeSubscribe() {
if (!this._gridSizeSub) {
this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState));
}
}
private _handleGridSizeUnsubscribe() {
if (this._gridSizeSub) {
this._gridSizeSub.unsubscribe();
this._gridSizeSub = undefined;
}
}
private _handleGridResize(newState: DashboardGridItemState, prevState: DashboardGridItemState) {
if (newState.height === prevState.height) {
return;
}
const itemCount = this.state.repeatedPanels?.length ?? 1;
const stateChange: Partial<DashboardGridItemState> = {};
if (this.getRepeatDirection() === 'v') {
stateChange.itemHeight = Math.ceil(newState.height! / itemCount);
} else {
@ -203,16 +224,9 @@ export class DashboardGridItem
public handleVariableName() {
if (this.state.variableName) {
if (!this._gridSizeSub) {
this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState));
this._subs.add(this._gridSizeSub);
}
this._handleGridSizeSubscribe();
} else {
if (this._gridSizeSub) {
this._gridSizeSub.unsubscribe();
this._subs.remove(this._gridSizeSub);
this._gridSizeSub = undefined;
}
this._handleGridSizeUnsubscribe();
}
this.performRepeat();

@ -11,7 +11,8 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../utils/utils';
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from './annotations';
import { AnnotationSettingsEdit, newAnnotationName } from './annotations/AnnotationSettingsEdit';
import { AnnotationSettingsList } from './annotations/AnnotationSettingsList';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export enum MoveDirection {

@ -5,7 +5,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { VERSIONS_FETCH_LIMIT, VersionsEditView } from './VersionsEditView';
import { historySrv } from './version-history';
import { historySrv } from './version-history/HistorySrv';
jest.mock('./version-history/HistorySrv');

@ -11,14 +11,11 @@ import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
import {
RevisionsModel,
VersionHistoryComparison,
VersionHistoryHeader,
VersionHistoryTable,
VersionsHistoryButtons,
historySrv,
} from './version-history';
import { RevisionsModel, historySrv } from './version-history/HistorySrv';
import { VersionsHistoryButtons } from './version-history/VersionHistoryButtons';
import { VersionHistoryComparison } from './version-history/VersionHistoryComparison';
import { VersionHistoryHeader } from './version-history/VersionHistoryHeader';
import { VersionHistoryTable } from './version-history/VersionHistoryTable';
export const VERSIONS_FETCH_LIMIT = 10;

@ -1,2 +0,0 @@
export { AnnotationSettingsEdit, newAnnotationName } from './AnnotationSettingsEdit';
export { AnnotationSettingsList } from './AnnotationSettingsList';

@ -1,5 +0,0 @@
export { HistorySrv, historySrv, type RevisionsModel } from './HistorySrv';
export { VersionHistoryTable } from './VersionHistoryTable';
export { VersionHistoryHeader } from './VersionHistoryHeader';
export { VersionsHistoryButtons } from './VersionHistoryButtons';
export { VersionHistoryComparison } from './VersionHistoryComparison';

@ -1,2 +0,0 @@
export { AnnotationSettingsEdit, newAnnotationName } from './AnnotationSettingsEdit';
export { AnnotationSettingsList } from './AnnotationSettingsList';

@ -3,7 +3,8 @@ import { getDataSourceSrv, locationService } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import { DashboardModel } from '../../state/DashboardModel';
import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from '../AnnotationSettings';
import { AnnotationSettingsEdit, newAnnotationName } from '../AnnotationSettings/AnnotationSettingsEdit';
import { AnnotationSettingsList } from '../AnnotationSettings/AnnotationSettingsList';
import { SettingsPageProps } from './types';

@ -4,7 +4,8 @@ import { locationService } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import { NEW_LINK } from 'app/features/dashboard-scene/settings/links/utils';
import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings';
import { LinkSettingsEdit } from '../LinksSettings/LinkSettingsEdit';
import { LinkSettingsList } from '../LinksSettings/LinkSettingsList';
import { SettingsPageProps } from './types';

@ -4,12 +4,9 @@ import * as React from 'react';
import { config } from '@grafana/runtime';
import { Spinner, HorizontalGroup } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import {
historySrv,
RevisionsModel,
VersionHistoryHeader,
VersionsHistoryButtons,
} from 'app/features/dashboard-scene/settings/version-history';
import { historySrv, RevisionsModel } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
import { VersionsHistoryButtons } from 'app/features/dashboard-scene/settings/version-history/VersionHistoryButtons';
import { VersionHistoryHeader } from 'app/features/dashboard-scene/settings/version-history/VersionHistoryHeader';
import { VersionHistoryComparison } from '../VersionHistory/VersionHistoryComparison';
import { VersionHistoryTable } from '../VersionHistory/VersionHistoryTable';

@ -1,2 +0,0 @@
export { LinkSettingsEdit } from './LinkSettingsEdit';
export { LinkSettingsList } from './LinkSettingsList';

@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
import { locationUtil } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
import { historySrv } from 'app/features/dashboard-scene/settings/version-history';
import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
import { useSelector } from 'app/types';
import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher';

@ -1,2 +0,0 @@
export * from './utils';
export * from './constants';

@ -6,7 +6,7 @@ import { loadPluginDashboards } from 'app/features/plugins/admin/state/actions';
import { PluginDashboard, StoreState, useDispatch, useSelector } from 'app/types';
import DashboardTable from '../components/DashboardsTable';
import { useInitDataSourceSettings } from '../state';
import { useInitDataSourceSettings } from '../state/hooks';
export type Props = {
// The UID of the data source

@ -10,7 +10,8 @@ import { contextSrv } from 'app/core/core';
import { StoreState, AccessControlAction, useSelector } from 'app/types';
import { ROUTES } from '../../connections/constants';
import { getDataSources, getDataSourcesCount, useLoadDataSources } from '../state';
import { useLoadDataSources } from '../state/hooks';
import { getDataSources, getDataSourcesCount } from '../state/selectors';
import { trackDataSourcesListViewed } from '../tracking';
import { DataSourcesListCard } from './DataSourcesListCard';

@ -5,7 +5,8 @@ import { SelectableValue } from '@grafana/data';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { StoreState, useSelector, useDispatch } from 'app/types';
import { getDataSourcesSearchQuery, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state';
import { setDataSourcesSearchQuery, setIsSortAscending } from '../state/reducers';
import { getDataSourcesSearchQuery, getDataSourcesSort } from '../state/selectors';
import { trackDsSearched } from '../tracking';
const ascendingSortValue = 'alpha-asc';

@ -16,9 +16,6 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { DataSourceSettingsState, useDispatch } from 'app/types';
import {
dataSourceLoaded,
setDataSourceName,
setIsDefault,
useDataSource,
useDataSourceExploreUrl,
useDataSourceMeta,
@ -28,7 +25,8 @@ import {
useInitDataSourceSettings,
useTestDataSource,
useUpdateDatasource,
} from '../state';
} from '../state/hooks';
import { setIsDefault, setDataSourceName, dataSourceLoaded } from '../state/reducers';
import { trackDsConfigClicked, trackDsConfigUpdated } from '../tracking';
import { DataSourceRights } from '../types';

@ -3,7 +3,7 @@ import { config } from '@grafana/runtime';
import { LinkButton } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { useDataSource } from '../state';
import { useDataSource } from '../state/hooks';
import { trackCreateDashboardClicked, trackDsConfigClicked, trackExploreClicked } from '../tracking';
import { constructDataSourceExploreUrl } from '../utils';

@ -10,12 +10,9 @@ import { DataSourcePluginCategory, StoreState, useDispatch, useSelector } from '
import { ROUTES } from '../../connections/constants';
import { DataSourceCategories } from '../components/DataSourceCategories';
import { DataSourceTypeCardList } from '../components/DataSourceTypeCardList';
import {
useAddDatasource,
useLoadDataSourcePlugins,
getFilteredDataSourcePlugins,
setDataSourceTypeSearchQuery,
} from '../state';
import { useAddDatasource, useLoadDataSourcePlugins } from '../state/hooks';
import { setDataSourceTypeSearchQuery } from '../state/reducers';
import { getFilteredDataSourcePlugins } from '../state/selectors';
export function NewDataSource() {
useLoadDataSourcePlugins();

@ -15,7 +15,7 @@ import {
Icon,
ScrollContainer,
} from '@grafana/ui';
import * as DFImport from 'app/features/dataframe-import';
import { acceptedFiles, maxFileSize } from 'app/features/dataframe-import/constants';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { getFileDropToQueryHandler } from 'app/plugins/datasource/grafana/utils';
@ -203,9 +203,9 @@ export function DataSourceModal({
readAs="readAsArrayBuffer"
fileListRenderer={() => undefined}
options={{
maxSize: DFImport.maxFileSize,
maxSize: maxFileSize,
multiple: false,
accept: DFImport.acceptedFiles,
accept: acceptedFiles,
onDrop: onFileDrop,
}}
>

@ -1,6 +0,0 @@
export * from './actions';
export * from './buildCategories';
export * from './hooks';
export * from './navModel';
export * from './reducers';
export * from './selectors';

@ -7,8 +7,8 @@ import { ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema'
import { InlineField, InlineFieldRow, RadioButtonGroup } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/internal';
import { getPublicOrAbsoluteUrl, ResourceFolderName } from '..';
import { MediaType, ResourceDimensionOptions, ResourcePickerSize } from '../types';
import { getPublicOrAbsoluteUrl } from '../resource';
import { MediaType, ResourceDimensionOptions, ResourceFolderName, ResourcePickerSize } from '../types';
import { ResourcePicker } from './ResourcePicker';

@ -1,6 +0,0 @@
export * from './ColorDimensionEditor';
export * from './IconSelector';
export * from './ResourceDimensionEditor';
export * from './ScaleDimensionEditor';
export * from './ScalarDimensionEditor';
export * from './TextDimensionEditor';

@ -1,9 +0,0 @@
export * from './types';
export * from './color';
export * from './scale';
export * from './text';
export * from './utils';
export * from './resource';
export * from './context';
export * from './scalar';

@ -7,15 +7,13 @@ import {
ColorDimensionConfig,
ScalarDimensionConfig,
} from '@grafana/schema';
import {
getColorDimension,
getScaledDimension,
getTextDimension,
getResourceDimension,
DimensionSupplier,
} from 'app/features/dimensions';
import { getColorDimension } from './color';
import { getResourceDimension } from './resource';
import { getScalarDimension } from './scalar';
import { getScaledDimension } from './scale';
import { getTextDimension } from './text';
import { DimensionSupplier } from './types';
export function getColorDimensionFromData(
data: PanelData | undefined,

@ -27,17 +27,15 @@ import { useDispatch, useSelector } from 'app/types';
import { changePanelState } from '../state/explorePane';
import {
SpanBarOptionsData,
SpanLinkFunc,
Trace,
TracePageHeader,
TraceTimelineViewer,
TTraceTimeline,
} from './components';
import memoizedTraceCriticalPath from './components/CriticalPath';
import { TracePageHeader } from './components/TracePageHeader';
import SpanGraph from './components/TracePageHeader/SpanGraph';
import TraceTimelineViewer from './components/TraceTimelineViewer';
import { TraceFlameGraphs } from './components/TraceTimelineViewer/SpanDetail';
import { SpanBarOptionsData } from './components/settings/SpanBarSettings';
import TTraceTimeline from './components/types/TTraceTimeline';
import { SpanLinkFunc } from './components/types/links';
import { Trace } from './components/types/trace';
import { createSpanLinkFactory } from './createSpanLink';
import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';

@ -14,7 +14,7 @@
import memoizeOne from 'memoize-one';
import { CriticalPathSection, Trace, TraceSpan } from '../types';
import { TraceSpan, CriticalPathSection, Trace } from '../types/trace';
import findLastFinishingChildSpan from './utils/findLastFinishingChildSpan';
import getChildOfSpans from './utils/getChildOfSpans';

@ -27,7 +27,9 @@
Here +++++ are critical path sections
*/
import { Trace, TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { Trace, TraceResponse } from '../../types/trace';
const testTrace: TraceResponse = {
traceID: 'test1-trace',

@ -29,7 +29,8 @@
|
Here ++++++ is critical path |
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const happyTrace: TraceResponse = {
traceID: 'trace-123',

@ -26,7 +26,8 @@ Span B will be dropped. |
span A is on critical path(+++++) |
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const trace: TraceResponse = {
traceID: '006c3cf93508f205',

@ -29,7 +29,8 @@ Both spanB and spanC will be dropped. |
span A is on critical path(+++++) |
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const trace: TraceResponse = {
traceID: 'trace-abc',

@ -28,7 +28,8 @@
Here span B is ref-type is 'FOLLOWS_FROM' |
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const trace: TraceResponse = {
traceID: 'trace-abc',

@ -26,7 +26,8 @@
| (parent-child tree)
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const trace: TraceResponse = {
traceID: 'trace-abc',

@ -26,7 +26,8 @@
|
*/
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
const trace: TraceResponse = {
traceID: 'trace-abc',

@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
/*
|

@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceResponse, transformTraceData } from '../../index';
import transformTraceData from '../../model/transform-trace-data';
import { TraceResponse } from '../../types/trace';
/*
|

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
import { TraceSpan } from '../../types/trace';
/**
* @returns - Returns the span that finished last among the remaining child spans.

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
import { TraceSpan } from '../../types/trace';
/**
* Removes child spans whose refType is FOLLOWS_FROM and their descendants.

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
import { TraceSpan } from '../../types/trace';
import test3 from '../testCases/test3';
import test4 from '../testCases/test4';
import test6 from '../testCases/test6';

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
import { TraceSpan } from '../../types/trace';
/**
* This function resolves overflowing child spans for each span.

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TNil } from './types';
import TNil from './types/TNil';
import { TraceSpan, TraceSpanReference, Trace } from './types/trace';
/**

@ -23,7 +23,7 @@ import { config, reportInteraction } from '@grafana/runtime';
import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/internal';
import { Trace } from '../../types';
import { Trace } from '../../types/trace';
export type NextPrevResultProps = {
trace: Trace;

@ -21,7 +21,7 @@ import { Button, Switch, useStyles2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/internal';
import { SearchProps } from '../../../useSearch';
import { Trace } from '../../types';
import { Trace } from '../../types/trace';
import { convertTimeFilter } from '../../utils/filter-spans';
import NextPrevResult from './NextPrevResult';

@ -23,7 +23,7 @@ import { Collapse, Icon, InlineField, InlineFieldRow, Select, Stack, Tooltip, us
import { defaultFilters, SearchProps } from '../../../useSearch';
import { getTraceServiceNames, getTraceSpanNames } from '../../../utils/tags';
import SearchBarInput from '../../common/SearchBarInput';
import { Trace } from '../../types';
import { Trace } from '../../types/trace';
import NextPrevResult from '../SearchBar/NextPrevResult';
import TracePageSearchBar from '../SearchBar/TracePageSearchBar';

@ -9,7 +9,7 @@ import { Input, Select, Stack, useStyles2 } from '@grafana/ui';
import { randomId, SearchProps, Tag } from '../../../useSearch';
import { getTraceTagKeys, getTraceTagValues } from '../../../utils/tags';
import { Trace } from '../../types';
import { Trace } from '../../types/trace';
interface Props {
search: SearchProps;

@ -19,7 +19,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { withTheme2, stylesFactory } from '@grafana/ui';
import { autoColor } from '../../Theme';
import { TNil } from '../../types';
import TNil from '../../types/TNil';
import { getRgbColorByKey } from '../../utils/color-generator';
import renderIntoCanvas from './render-into-canvas';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save