Compare commits

...

22 Commits

Author SHA1 Message Date
Mihai Doarna f7337c65d1 Merge branch 'main' into dmihai/team-search-missing-fields 2025-12-19 11:13:15 +02:00
Tania 7360194ab9 Chore: Remove unifiedReqeustLog feature flag (#115559)
Chore: Remove unifiedReqeustLog feature flag
2025-12-19 09:55:47 +01:00
Will Assis 99f5f14de7 unified-storage: move rvmanager into its own package (#115445)
* unified-storage: move rvmanager into its own package so it can be reused with sqlkv later
2025-12-18 18:35:32 -05:00
Collin Fingar 606a59584a Saved Queries: Pass editor ref for dynamic dropdown display (#114321)
* Saved Queries: Pass editor ref for dynamic dropdown display

* Updated docs per feedback

* Update docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/explore/get-started-with-explore.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

---------

Co-authored-by: Nathan Marrs <nathanielmarrs@gmail.com>
Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
2025-12-18 18:18:24 -05:00
Nathan Marrs 0ec716a433 Embedded Dashboard Panels: Add Grafana Branding (#115198)
* feat: add Grafana logo to embedded panels

- Add Grafana logo watermark to solo panel view (embedded panels)
- Logo appears in top-right corner with subtle background container
- Logo hides on hover to avoid interfering with panel content
- Uses React state to track hover for reliable behavior across nested elements

* minor formatting

* update changes to match public dashboards styling

* match styles of public dashboards

* feat: add responsive Grafana branding to embedded panels

- Add 'Powered by Grafana' branding with text logo to solo panel view
- Implement responsive scaling based on panel dimensions (0.6x to 1.0x)
- Logo and text scale proportionally with panel size
- Branding hides on hover to avoid interfering with panel content
- Matches public dashboard branding pattern for consistency
- Uses ResizeObserver for efficient responsive updates

* feat: add Grafana branding to embedded solo panels

- Add 'Powered by Grafana' branding with text logo to embedded panels
- Create SoloPanelPageLogo component for reusable branding
- Implement responsive scaling based on panel dimensions
- Add hover-to-hide functionality to avoid content overlap
- Logo scales between 0.6x and 1.0x based on panel size

* refactor: move scale calculation into SoloPanelPageLogo component

- Move responsive scale calculation logic from SoloPanelRenderer to SoloPanelPageLogo
- Logo component now manages its own scaling based on container dimensions
- Improves separation of concerns and component encapsulation

* feat: add hideLogo query parameter to disable embedded panel branding

- Add hideLogo query parameter support to SoloPanelPage
- Logo can be hidden via ?hideLogo, ?hideLogo=true, or ?hideLogo=1
- Useful for customers who want to disable branding and for image rendering scenarios
- Update Props interface to include hideLogo in queryParams type

* feat: hide logo in panel image renderer URLs

- Add hideLogo=true parameter to image renderer URLs in ShareLinkTab
- Ensures logo is hidden when generating panel images through share feature
- Update test to expect hideLogo=true in render URL

* feat: hide logo in old dashboard sharing panel image URLs

- Add hideLogo=true parameter to buildImageUrl in ShareModal utils
- Ensures logo is hidden when generating panel images through old share modal
- Update all ShareLink tests to expect hideLogo=true in render URLs

* test: add comprehensive tests for SoloPanelPage and SoloPanelPageLogo

- Add SoloPanelPageLogo tests covering rendering, hover behavior, theme selection, and scaling
- Add SoloPanelPage tests covering logo visibility based on hideLogo prop
- Test logo hiding functionality (most important behavior)
- Test responsive scaling based on container dimensions
- Test ResizeObserver integration
- All 14 tests passing

* refactor: centralize hideLogo handling in SoloPanelPageLogo

Move hideLogo parsing and decision-making into SoloPanelPageLogo so SoloPanelPage/SoloPanelRenderer only pass through the raw query param value.

* chore: clean up solo logo test and share link params

Remove a duplicate SVG mock in SoloPanelPageLogo.test, and simplify ShareLinkTab image URL building without changing behavior.

* chore: revert ShareLinkTab image query refactor

Restore the previous image URL query-param mutation logic in ShareLinkTab to reduce risk.

* chore: set hideLogo once for ShareLinkTab image URLs

Avoid passing hideLogo twice when building the rendered image URL.

* fix: handle boolean hideLogo query param in SoloPanelPageLogo

Handle query params that are represented as booleans (e.g., ?hideLogo) and arrays, and avoid calling trim() on non-strings.

* fix i18n

* fix(dashboard-scene): address SoloPanelPageLogo review feedback

Avoid double-scaling logo margin, clarify scaling comments, and extend tests for null/array values and ResizeObserver cleanup.

* update margin left on logo to better match text spacing
2025-12-18 15:01:16 -08:00
Leon Sorokin 72e1f1e546 Heatmap: Support for linear y axis (#113337)
* wip

* boop

* Base factor on data

* Add some basic option control

* Remove old comments

* Add feature flag

* Apply feature flag to axis options

* Turn factor calculation into exported function

* Simplify bucket factor function

* Clarify comments

* Fix cell sizing of pre-bucketed heatmaps with log

* Remove unnecessary category change

* Consolidate editor for calculate from data no

* Update bucket function sanity checks

* Wire up scale config from yBucketScale

* Hide bucket controls for heatmap cells

* Fix splits

* Add test coverage

* Fix failing test

* Add basic util test coverage

* Fix tooltip for legacy in linear

* Fix y bucket option width to be consistent

* Hide tick alignment for explicit scale modes

* Clarify comment

* Make sure units are passed properly for linear

* Remove null assertion operator

* Clean up nested ternary

* Add type protection to scaleLog

* Remove repeated code for ySize calcs

* Remove ternary for scaleDistribution

* Add test coverage for YBucketScaleEditor

* Add isHeatmapSparse function to tooltip utils

* Create calculateYSizeDivisor util function

* Fix y axis min and max options and extend to log

* Add toLogBase test coverage

* Create applyExplicitMinMax function

* Add additional test coverage for scale editor

* Run i18n-extract

* Update eslint suppressions

---------

Co-authored-by: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com>
2025-12-18 14:45:00 -08:00
Haris Rozajac 37c1e3fb02 Dashboard Schema v1beta1 to v2alpha1: Preserve string template variable datasource references in query variables (#115516)
* Dashboard migration: preserve legacy string datasource references

Fix v1beta1 → v2alpha1 conversion to handle legacy string datasource
references in QueryVariable, AdhocVariable, and GroupByVariable.

Previously, string datasource references (both template variables like
"$datasource" and direct names/UIDs like "prometheus") were being
dropped during conversion, causing variable chaining to break.

The frontend's DatasourceSrv.getInstanceSettings() already handles
string references by trying uid → name → id lookup at runtime, so we
preserve the string in the uid field and let the frontend resolve it.

* trigger frontend ci tests when dashboard migration code changes

* v1: if string convert to DS ref

* Update migration testdata to fix template variable datasource references

* update
2025-12-18 15:11:09 -07:00
Denis Vodopianov 39c562a911 Revert: chore: a drop-in replacement for FeatureToggles.IsEnabledGlobally in app settings (#115593)
* Revert "chore: a drop-in replacement for FeatureToggles.IsEnabledGlobally in app settings (#113449)"

This reverts commit 26ce2c09d7.

* Change FeatureToggles.IsEnabledGlobally deprecation message
2025-12-18 16:46:32 -05:00
Haris Rozajac 05fd304dbd Dashboards: AdHoc and GroupBy wrapper (#115124)
* wip; DrilldownControls

* use wrapper so that drilldown controls wrap inline

* keep labels on top when input expands vertically

* add clear all button

* add collapsible prop

* i18n

* Increase maxWidth for adhoc

* bump scenes for testing

* fix

* remove clear all button

* use new feature toggle; pass collapsible in v2

* update variable controls to use new feature flag

* cleanup

* wip (#115441)

* wip

* fix

* update wrapping on smaller screens

---------

Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>

* Filter out variables that are not in inControlsMenu

* filter out inControlsMenu vars, not hidden ones

* canary scenes

* fix

* cleanup

* canary scenes

* pass wideInput to groupby based on ff

* update var name and bump scenes

* bump scenes

* yarn lock

---------

Co-authored-by: Victor Marin <victor.marin@grafana.com>
2025-12-18 11:58:21 -07:00
Laura Fernández 1850163346 Rudderstack: Add new config option for rudderstack v3 url (#115374) 2025-12-18 19:47:04 +01:00
Denis Vodopianov 26ce2c09d7 chore: a drop-in replacement for FeatureToggles.IsEnabledGlobally in app settings (#113449) 2025-12-18 13:10:30 -05:00
Galen Kistler 051cdaad0d Revert "Plugins: Add PluginInsights UI (#111603)" (#115574)
This reverts commit 1f4f2b4d7c.
2025-12-18 17:11:33 +00:00
Mihai Doarna 1862e5dac5 IAM: Fix team search for unistore (#115250)
* fix team search for unistore

* fix search in unistore

* remove field prefix when generating the response

* fix unit test

* address feedback
2025-12-18 18:54:55 +02:00
Renato Costa 19f6dbe1bb unified-storage: add BatchGet support to the sqlkv implementation (#115517)
* unified-storage: add `BatchGet` support to the sqlkv implementation

* address comments

* fix linting
2025-12-18 11:21:36 -05:00
Igor Suleymanov facb25a09c Fix Grafana App SDK logger log level (#115551)
* Fix Grafana App SDK logger log level

What

This commit fixes the hardcoded value of the app SDK logger log level
by properly setting it during the log manager initialization.

Why

To prevent app SDK logging from always logging at DEBUG.

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

* Add missing argument to the logging test

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>

---------

Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
2025-12-18 18:07:48 +02:00
Charandas d0792ebe97 Secrets: Add gRPC client retry with exp. backoff" (#115526)
Provisioning: secrets decrypt client should retry with exponential backoff
2025-12-18 07:44:33 -08:00
Alexa Vargas 98aa6c50dc DashboardLibrary: Force v1 dashboard scene page manager when loading template dashboards (#115488)
Force v1 manager for template dashboards feature
2025-12-18 16:42:37 +01:00
Vardan Torosyan a65aa9d18f SCIM Docs: Replace warning with an information text for SAML identifier (#115353)
* SCIM Docs: Replace warning with an information text for SAML identifier

* Fix externalId warning
2025-12-18 16:26:56 +01:00
Yunwen Zheng 58a026b6a5 RecentlyViewedDashboards: Clear history button (#115519)
* RecentlyViewedDashboards: Clear history button
2025-12-18 10:22:40 -05:00
Kristina Demeshchik 7e5eb46bea Dashboards: Fix text panel content loss during v1 to v2 migration (#115496)
* move content and mode properties to options level

* move to angular section

* Update comments

* handle missing angular text panel

* re-generate test files

* angualr panels tests

* fixing test

* Update output files

* Update output for dev dashboard

* Spread options at the top panel level for migration

* linting issue

---------

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
2025-12-18 09:59:58 -05:00
Kristina Demeshchik 4bcd31b17a Dashboard: change export dropdown placement in sidebar (#115515)
Update export menu placement
2025-12-18 09:59:26 -05:00
Mihai Doarna 375c22c5b0 add the missing fields to the team search results 2025-12-11 17:52:03 +02:00
152 changed files with 11076 additions and 1077 deletions
@@ -95,6 +95,7 @@ runs:
- 'nx.json'
- 'tsconfig.json'
- '.yarn/**'
- 'apps/dashboard/pkg/migration/**'
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
@@ -300,15 +300,9 @@
"y": 0
},
"id": 6,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
},
"options": {},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown",
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -743,7 +743,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -764,7 +766,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -961,8 +961,12 @@
"hide": "dontHide",
"refresh": "onDashboardLoad",
"skipUrlSync": false,
"datasource": {
"type": "",
"uid": "$datasource"
},
"query": {
"kind": "prometheus",
"kind": "",
"spec": {
"__legacyStringValue": "label_values(up, job)"
}
@@ -988,8 +992,12 @@
"hide": "dontHide",
"refresh": "onDashboardLoad",
"skipUrlSync": false,
"datasource": {
"type": "",
"uid": "$datasource"
},
"query": {
"kind": "prometheus",
"kind": "",
"spec": {
"__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)"
}
@@ -978,8 +978,11 @@
"skipUrlSync": false,
"query": {
"kind": "DataQuery",
"group": "prometheus",
"group": "",
"version": "v0",
"datasource": {
"name": "$datasource"
},
"spec": {
"__legacyStringValue": "label_values(up, job)"
}
@@ -1007,8 +1010,11 @@
"skipUrlSync": false,
"query": {
"kind": "DataQuery",
"group": "prometheus",
"group": "",
"version": "v0",
"datasource": {
"name": "$datasource"
},
"spec": {
"__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)"
}
@@ -115,7 +115,14 @@
"kind": "logs",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "logs",
"originalOptions": {
"height": 100
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -120,7 +120,14 @@
"group": "logs",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "logs",
"originalOptions": {
"height": 100
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -182,7 +182,20 @@
"kind": "table",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"grid": {
"max": 100,
"min": 0
},
"legend": true,
"y2_format": "bytes",
"y_format": "short"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -189,7 +189,20 @@
"group": "table",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"grid": {
"max": 100,
"min": 0
},
"legend": true,
"y2_format": "bytes",
"y_format": "short"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -435,7 +435,29 @@
"kind": "table",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"styles": [
{
"colors": [
"red",
"yellow",
"green"
],
"pattern": "/.*/",
"thresholds": [
"10",
"20"
],
"unit": "short"
}
],
"table": "table2"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -449,7 +449,29 @@
"group": "table",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"styles": [
{
"colors": [
"red",
"yellow",
"green"
],
"pattern": "/.*/",
"thresholds": [
"10",
"20"
],
"unit": "short"
}
],
"table": "table2"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -110,7 +110,15 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Angular Text Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -115,7 +115,15 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Angular Text Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -361,7 +361,15 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data link variables overview\n\nThis dashboard presents variables that one can use when creating *data links*. All links redirect to this dashboard and this panel represents the values that were interpolated in the link that was clicked.\n\n\n#### Series variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$seriesName\u003c/span\u003e\n2. **label.datacenter:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenter\u003c/span\u003e\n3. **label.datacenter.region:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenterRegion\u003c/span\u003e\n\n#### Field variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$fieldName\u003c/span\u003e\n\n#### Value variables\n1. **Time:** \u003cspan style=\"color: orange;\"\u003e$valueTime\u003c/span\u003e\n2. **Numeric:** \u003cspan style=\"color: orange;\"\u003e$valueNumeric\u003c/span\u003e\n3. **Text:** \u003cspan style=\"color: orange;\"\u003e$valueText\u003c/span\u003e\n4. **Calc:** \u003cspan style=\"color: orange;\"\u003e$valueCalc\u003c/span\u003e\n\n",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -372,7 +372,15 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data link variables overview\n\nThis dashboard presents variables that one can use when creating *data links*. All links redirect to this dashboard and this panel represents the values that were interpolated in the link that was clicked.\n\n\n#### Series variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$seriesName\u003c/span\u003e\n2. **label.datacenter:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenter\u003c/span\u003e\n3. **label.datacenter.region:** \u003cspan style=\"color: orange;\"\u003e$labelDatacenterRegion\u003c/span\u003e\n\n#### Field variables\n1. **Name:** \u003cspan style=\"color: orange;\"\u003e$fieldName\u003c/span\u003e\n\n#### Value variables\n1. **Time:** \u003cspan style=\"color: orange;\"\u003e$valueTime\u003c/span\u003e\n2. **Numeric:** \u003cspan style=\"color: orange;\"\u003e$valueNumeric\u003c/span\u003e\n3. **Text:** \u003cspan style=\"color: orange;\"\u003e$valueText\u003c/span\u003e\n4. **Calc:** \u003cspan style=\"color: orange;\"\u003e$valueCalc\u003c/span\u003e\n\n",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -167,7 +167,15 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -174,7 +174,15 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -273,7 +273,15 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -282,7 +282,15 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -296,6 +296,7 @@
}
},
{
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"datasource": {
"type": "grafana-testdata-datasource"
},
@@ -306,15 +307,7 @@
"y": 0
},
"id": 6,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
},
"mode": "markdown",
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -1256,13 +1256,13 @@
"spec": {
"pluginVersion": "11.0.0-pre",
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
@@ -1301,13 +1301,13 @@
"version": "11.0.0-pre",
"spec": {
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
@@ -62,6 +62,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -151,6 +157,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -241,6 +253,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -320,6 +338,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -401,6 +425,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -480,6 +510,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -559,6 +595,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -639,6 +681,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -730,6 +778,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -927,6 +981,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1006,6 +1066,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1085,6 +1151,12 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -67,6 +67,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -159,6 +165,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -252,6 +264,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -334,6 +352,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -418,6 +442,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -500,6 +530,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -582,6 +618,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -665,6 +707,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -759,6 +807,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -961,6 +1015,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1043,6 +1103,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1125,6 +1191,12 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -412,7 +412,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -456,7 +466,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Stacking values on top of nulls, should treat the null values as zero. ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -500,7 +520,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Stacking when all values are null should leave a gap in the graph",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -1681,7 +1711,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2061,7 +2101,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Just verify that the tooltip time has millisecond resolution ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2105,7 +2155,17 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Verify that axis labels look ok",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -429,7 +429,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -475,7 +485,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Stacking values on top of nulls, should treat the null values as zero. ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -521,7 +541,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Stacking when all values are null should leave a gap in the graph",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -1779,7 +1809,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2172,7 +2212,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Just verify that the tooltip time has millisecond resolution ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2218,7 +2268,17 @@
"group": "text",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Verify that axis labels look ok",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -74,7 +74,44 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateViridis",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"tooltipDecimals": 4,
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 2,
"format": "areaM2",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -116,7 +153,46 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {
"cardRound": 50
},
"color": {
"cardColor": "#1F60C4",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 1,
"format": "kwatt",
"logBase": 1,
"show": true,
"width": "100"
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -158,7 +234,44 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#1F60C4",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": true,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 1,
"format": "kwatt",
"logBase": 1,
"show": true,
"width": "100"
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -204,7 +317,46 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateViridis",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"tooltipDecimals": 4,
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 2,
"format": "areaM2",
"logBase": 1,
"max": "50",
"min": "20",
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -246,7 +398,44 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateBuGn",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": true,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"xAxis": {
"show": true
},
"xBucketNumber": 10,
"yAxis": {
"format": "short",
"logBase": 2,
"show": true,
"splitFactor": 2
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -288,7 +477,44 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateBuGn",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": true,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"xAxis": {
"show": true
},
"xBucketNumber": 10,
"yAxis": {
"format": "short",
"logBase": 10,
"show": true,
"splitFactor": 5
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -78,7 +78,44 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateViridis",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"tooltipDecimals": 4,
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 2,
"format": "areaM2",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -123,7 +160,46 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {
"cardRound": 50
},
"color": {
"cardColor": "#1F60C4",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 1,
"format": "kwatt",
"logBase": 1,
"show": true,
"width": "100"
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -168,7 +244,44 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#1F60C4",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": true,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 1,
"format": "kwatt",
"logBase": 1,
"show": true,
"width": "100"
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -216,7 +329,46 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateViridis",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"tooltipDecimals": 4,
"xAxis": {
"show": true
},
"yAxis": {
"decimals": 2,
"format": "areaM2",
"logBase": 1,
"max": "50",
"min": "20",
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -261,7 +413,44 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateBuGn",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": true,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"xAxis": {
"show": true
},
"xBucketNumber": 10,
"yAxis": {
"format": "short",
"logBase": 2,
"show": true,
"splitFactor": 2
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -306,7 +495,44 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateBuGn",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": true,
"highlightCards": true,
"legend": {
"show": true
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": true
},
"xAxis": {
"show": true
},
"xBucketNumber": 10,
"yAxis": {
"format": "short",
"logBase": 10,
"show": true,
"splitFactor": 5
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -665,7 +665,42 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": false
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"format": "short",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -691,7 +691,42 @@
"group": "heatmap",
"version": "",
"spec": {
"options": {},
"options": {
"__angularMigration": {
"autoMigrateFrom": "heatmap",
"originalOptions": {
"cards": {},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"exponent": 0.5,
"mode": "spectrum"
},
"dataFormat": "timeseries",
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"legend": {
"show": false
},
"reverseYBuckets": false,
"tooltip": {
"show": true,
"showHistogram": false
},
"xAxis": {
"show": true
},
"yAxis": {
"format": "short",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
}
}
},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -56,6 +56,14 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"panel-tests"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -94,6 +102,15 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -133,6 +150,15 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"templating",
"gdev"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -172,6 +198,15 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"datasource-test"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -211,6 +246,12 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": []
}
},
"maxItems": 100,
"query": "",
"showHeadings": true,
@@ -247,6 +288,15 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -58,6 +58,14 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"panel-tests"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -97,6 +105,15 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -137,6 +154,15 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"templating",
"gdev"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -177,6 +203,15 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"datasource-test"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -217,6 +252,12 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": []
}
},
"maxItems": 100,
"query": "",
"showHeadings": true,
@@ -254,6 +295,15 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -2,6 +2,7 @@ package conversion
import (
"context"
"strings"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apiserver/pkg/endpoints/request"
@@ -79,5 +80,57 @@ func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard,
return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1")
}
// Normalize template variable datasources from string to object format
// This handles legacy dashboards where query variables have datasource: "$datasource" (string)
// instead of datasource: { uid: "$datasource" } (object)
// our migration pipeline in v36 doesn't address because this was not addressed historically
// in DashboardMigrator - see public/app/features/dashboard/state/DashboardMigrator.ts#L607
// Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings
normalizeTemplateVariableDatasources(out.Spec.Object)
return nil
}
// normalizeTemplateVariableDatasources converts template variable string datasources to object format.
// Legacy dashboards may have query variables with datasource: "$datasource" (string).
// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion.
func normalizeTemplateVariableDatasources(dashboard map[string]interface{}) {
templating, ok := dashboard["templating"].(map[string]interface{})
if !ok {
return
}
list, ok := templating["list"].([]interface{})
if !ok {
return
}
for _, variable := range list {
varMap, ok := variable.(map[string]interface{})
if !ok {
continue
}
varType, _ := varMap["type"].(string)
if varType != "query" {
continue
}
ds := varMap["datasource"]
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
// Convert string template variable reference to object format
varMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
}
// isTemplateVariableRef checks if a string is a Grafana template variable reference.
// Template variables can be in the form: $varname or ${varname}
func isTemplateVariableRef(s string) bool {
if s == "" {
return false
}
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
}
@@ -1185,6 +1185,10 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm
// If no UID and no type, use default
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
@@ -1532,6 +1536,10 @@ func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, comm
// If no UID and no type, use default
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
@@ -1709,6 +1717,10 @@ func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, co
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
datasourceUID = resolveGrafanaDatasourceUID(datasourceType, datasourceUID)
} else if dsStr, ok := datasource.(string); ok && isTemplateVariable(dsStr) {
// Handle datasource variable reference (e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
datasourceUID = dsStr
} else {
datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider)
}
@@ -2296,20 +2308,24 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo
// We check two cases:
// 1. Panel already has autoMigrateFrom set (from v0→v1 migration) - panel type already converted
// 2. Panel type is a known Angular panel - need to convert type AND set autoMigrateFrom
// 3. Panel has original options - need to set autoMigrateFrom and originalOptions
autoMigrateFrom, hasAutoMigrateFrom := panelMap["autoMigrateFrom"].(string)
originalOptions := extractAngularOptions(panelMap)
if !hasAutoMigrateFrom || autoMigrateFrom == "" {
// Check if panel type is an Angular type that needs migration
if newType := getAngularPanelMigration(panelType, panelMap); newType != "" {
autoMigrateFrom = panelType // Original Angular type
panelType = newType // New modern type
} else if len(originalOptions) > 0 {
autoMigrateFrom = panelType
}
}
if autoMigrateFrom != "" {
options["__angularMigration"] = map[string]interface{}{
"autoMigrateFrom": autoMigrateFrom,
"originalOptions": extractAngularOptions(panelMap),
"originalOptions": originalOptions,
}
}
@@ -290,6 +290,7 @@
}
},
{
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"datasource": {
"type": "grafana-testdata-datasource"
},
@@ -300,15 +301,7 @@
"y": 0
},
"id": 6,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
},
"mode": "markdown",
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -654,7 +654,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -677,7 +679,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -737,7 +737,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -758,7 +760,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -717,7 +717,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -739,7 +741,9 @@
"text": "prod",
"value": "prod"
},
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"hide": 0,
"includeAll": false,
"label": "namespace",
+10 -5
View File
@@ -2,6 +2,8 @@ package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/services/team"
)
// +k8s:deepcopy-gen=true
@@ -27,9 +29,12 @@ type TeamSearchResults struct {
// +k8s:deepcopy-gen=true
type TeamHit struct {
Name string `json:"name"`
Title string `json:"title"`
Email string `json:"email,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
ExternalUID string `json:"externalUID,omitempty"`
Name string `json:"name"`
Title string `json:"title"`
Email string `json:"email,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
ExternalUID string `json:"externalUID,omitempty"`
MemberCount int64 `json:"memberCount"`
Permission team.PermissionType `json:"permission"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
}
+3
View File
@@ -335,6 +335,9 @@ rudderstack_data_plane_url =
# Rudderstack SDK url, optional, only valid if rudderstack_write_key and rudderstack_data_plane_url is also set
rudderstack_sdk_url =
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
rudderstack_v3_sdk_url =
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
rudderstack_config_url =
+3
View File
@@ -322,6 +322,9 @@
# Rudderstack SDK url, optional, only valid if rudderstack_write_key and rudderstack_data_plane_url is also set
;rudderstack_sdk_url =
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
;rudderstack_v3_sdk_url =
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
;rudderstack_config_url =
@@ -299,15 +299,9 @@
"y": 0
},
"id": 6,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "# Graph panel >> Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
},
"options": {},
"content": "# Graph panel >> Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown",
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -54,18 +54,6 @@ SCIM offers several advantages for managing users and teams in Grafana:
## Authentication and access requirements
{{< admonition type="warning" title="Critical: Aligning SAML Identifier with SCIM externalId" >}}
When using SAML for authentication alongside SCIM provisioning, a critical security measure is to ensure proper alignment between the the SCIM user's `externalId` and the SAML user identifier. The unique identifier used for SCIM provisioning (which becomes the `externalId` in Grafana, often sourced from a stable IdP attribute like Entra ID's `user.objectid`) **must also be sent as a claim in the SAML assertion from your Identity Provider.**
Furthermore, the Grafana SAML configuration must be correctly set up to identify and use this specific claim for linking the authenticated SAML user to their SCIM-provisioned user. This can be achieved by either ensuring the primary SAML login identifier by using the `assertion_attribute_external_uid` setting in Grafana to explicitly set the name of the SAML claim that contains the stable unique identifier attribute.
**Why is this important?**
A mismatch or inconsistent mapping between this SAML login identifier and the SCIM `externalId` creates a critical security vulnerability. If these two identifiers are not reliably and uniquely aligned for each individual user, Grafana may fail to correctly link an authenticated SAML session to the intended SCIM-provisioned user profile and its associated permissions. This can enable a malicious actor to impersonate another user—for instance, by crafting a SAML assertion that, due to the identifier misalignment, incorrectly grants them the access rights of the targeted user.
Grafana relies on this linkage to correctly associate the authenticated user from SAML with the provisioned user from SCIM. Failure to ensure a consistent and unique identifier across both systems can break this linkage, leading to incorrect user mapping and potential unauthorized access.
Always verify that your SAML identity provider is configured to send a stable, unique user identifier that your SCIM configuration maps to `externalId`. Refer to your identity provider's documentation and the specific Grafana SCIM integration guides (e.g., for [Entra ID](configure-scim-with-azuread/) or [Okta](configure-scim-with-okta/)) for detailed instructions on configuring these attributes correctly.
{{< /admonition >}}
When you enable SCIM in Grafana, the following requirements and restrictions apply:
1. **Use the same identity provider for user provisioning and for authentication flow**: You must use the same identity provider for both authentication and user provisioning.
@@ -74,6 +62,12 @@ When you enable SCIM in Grafana, the following requirements and restrictions app
- Configure `userUID` SAML assertion in [Entra ID](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-azuread/#configure-saml-assertions-when-using-scim-provisioning)
- Configure `userUID` SAML assertion in [Okta](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-okta/#configure-saml-assertions-when-using-scim-provisioning)
### Align SAML identifier with SCIM `externalId`
When you use SAML with SCIM provisioning, align the SCIM `externalId` with the SAML user identifier. Use a stable IdP attribute (for example, Entra ID `user.objectid`) as the SCIM `externalId`, and send that same value as a SAML claim. Configure Grafana to read this claim with the `assertion_attribute_external_uid` setting so SAML authentication links to the SCIM-provisioned user and its permissions.
If the SAML identifier and SCIM `externalId` differ, Grafana may not link the authenticated user to the intended SCIM profile, which can result in incorrect access. Verify your IdP sends a stable, unique identifier and that it matches the SCIM `externalId`. Refer to your IdP docs and the Grafana SCIM integration guides for [Entra ID](configure-scim-with-azuread/) and [Okta](configure-scim-with-okta/) for attribute configuration details.
## Configure SCIM using the Grafana user interface
You can configure SCIM in Grafana using the Grafana user interface. To do this, navigate to **Administration > Authentication > SCIM**.
@@ -642,6 +642,12 @@ You must also provide the `rudderstack_write_key` to enable this feature.
Optional.
If tracking with RudderStack is enabled, you can provide a custom URL to load the RudderStack SDK.
#### `rudderstack_v3_sdk_url`
Optional.
This is mirroring the old configuration option, which will be deprecated.
If `rudderstack_sdk_url` and `rudderstack_v3_sdk_url` are both set, the feature toggle `rudderstackUpgrade` will control which one is loaded.
#### `rudderstack_config_url`
Optional.
@@ -31,7 +31,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes |
| `unifiedRequestLog` | Writes error logs to the request logger | Yes |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes |
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |
@@ -163,9 +163,9 @@ To add a new annotation query to a dashboard, follow these steps:
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source. The annotation query options are different for each data source. For information about annotations in a specific data source, refer to the specific [data source](ref:data-source) topic.
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option.
1. (Optional) Click **Test annotation query** to ensure that the query is working properly.
1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and test them as many times as needed.
@@ -125,9 +125,9 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source.
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option.
1. Click **Refresh** to query the data source.
1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and refresh the data source as many times as needed.
@@ -71,8 +71,9 @@ Explore consists of a toolbar, outline, query editor, the ability to add multipl
- **Run query** - Click to run your query.
- **Query editor** - Interface where you construct the query for a specific data source. Query editor elements differ based on data source. In order to run queries across multiple data sources you need to select **Mixed** from the data source picker.
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace with saved query** - Reuse a saved query.
- **Saved queries**:
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace query** - Reuse a saved query.
- **+ Add query** - Add an additional query.
- **+ Add from saved queries** - Add an additional query by reusing a saved query.
@@ -88,8 +88,9 @@ The data section contains tabs where you enter queries, transform your data, and
- **Queries**
- Select your data source. You can also set or update the data source in existing dashboards using the drop-down menu in the **Queries** tab.
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace with saved query** - Reuse a saved query.
- **Saved queries**:
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace query** - Reuse a saved query.
- **+ Add query** - Add an additional query.
- **+ Add from saved queries** - Add an additional query by reusing a saved query.
@@ -156,11 +156,11 @@ In the **Saved queries** drawer, you can:
- Edit a query title, description, tags, or the availability of the query to other users in your organization. By default, saved queries are locked for editing.
- When you access the **Saved queries** drawer from Explore, you can use the **Edit in Explore** option to edit the body of a query.
To access your saved queries, click **+ Add from saved queries** or **Replace with saved query** in the query editor:
To access your saved queries, click **+ Add from saved queries** or open the **Saved queries** drop-down menu and click **Replace query** in the query editor:
{{< figure src="/media/docs/grafana/dashboards/screenshot-use-saved-queries-v12.3.png" max-width="750px" alt="Access saved queries" >}}
Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** updates your existing query.
Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace query** in the **Saved queries** drop-down menu updates your existing query.
{{< admonition type="note" >}}
Users with Admin and Editor roles can create and save queries for reuse.
@@ -172,7 +172,7 @@ Viewers can only reuse queries.
To save a query you've created:
1. From the query editor, click the **Save query** icon:
1. From the query editor, open the **Saved queries** drop-down menu and click the **Save query** option:
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-save-query-v12.2.png" max-width="750px" alt="Save a query" >}}
@@ -227,7 +227,7 @@ To add a query, follow these steps:
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source.
- Click **Replace with saved query** to reuse a saved query.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a saved query.
{{< admonition type="note" >}}
[Saved queries](#saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available.
@@ -235,7 +235,7 @@ To add a query, follow these steps:
This feature is only available on Grafana Enterprise and Grafana Cloud.
{{< /admonition >}}
1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** button (or icon).
1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** option in the **Saved queries** drop-down menu.
1. (Optional) Click **+ Add query** or **Add from saved queries** to add more queries as needed.
1. Click **Run queries**.
+6 -6
View File
@@ -1888,11 +1888,6 @@
"count": 1
}
},
"public/app/features/dashboard-scene/serialization/angularMigration.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
@@ -2873,6 +2868,11 @@
"count": 1
}
},
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/features/plugins/admin/helpers.ts": {
"no-restricted-syntax": {
"count": 2
@@ -4339,7 +4339,7 @@
},
"public/app/plugins/panel/heatmap/utils.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 16
"count": 14
}
},
"public/app/plugins/panel/histogram/Histogram.tsx": {
+2 -2
View File
@@ -295,8 +295,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.51.0",
"@grafana/scenes-react": "^6.51.0",
"@grafana/scenes": "6.52.0",
"@grafana/scenes-react": "6.52.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -289,6 +289,7 @@ export interface GrafanaConfig {
rudderstackWriteKey: string;
rudderstackDataPlaneUrl: string;
rudderstackSdkUrl: string;
rudderstackV3SdkUrl: string;
rudderstackConfigUrl: string;
rudderstackIntegrationsUrl: string;
applicationInsightsConnectionString: string;
+9 -10
View File
@@ -126,11 +126,6 @@ export interface FeatureToggles {
*/
disableSSEDataplane?: boolean;
/**
* Writes error logs to the request logger
* @default true
*/
unifiedRequestLog?: boolean;
/**
* Uses JWT-based auth for rendering instead of relying on remote cache
*/
renderAuthJWT?: boolean;
@@ -499,6 +494,10 @@ export interface FeatureToggles {
*/
newDashboardWithFiltersAndGroupBy?: boolean;
/**
* Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it
*/
dashboardAdHocAndGroupByWrapper?: boolean;
/**
* Updates CloudWatch label parsing to be more accurate
* @default true
*/
@@ -1165,6 +1164,11 @@ export interface FeatureToggles {
*/
externalVizSuggestions?: boolean;
/**
* Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows)
* @default false
*/
heatmapRowsAxisOptions?: boolean;
/**
* Restrict PanelChrome contents with overflow: hidden;
* @default true
*/
@@ -1189,11 +1193,6 @@ export interface FeatureToggles {
*/
onlyStoreActionSets?: boolean;
/**
* Show insights for plugins in the plugin details page
* @default false
*/
pluginInsights?: boolean;
/**
* Enables a new panel time settings drawer
*/
panelTimeSettings?: boolean;
+1
View File
@@ -224,6 +224,7 @@ export class GrafanaBootConfig {
rudderstackWriteKey?: string;
rudderstackDataPlaneUrl?: string;
rudderstackSdkUrl?: string;
rudderstackV3SdkUrl?: string;
rudderstackConfigUrl?: string;
rudderstackIntegrationsUrl?: string;
analyticsConsoleReporting = false;
@@ -185,6 +185,10 @@ export interface RowsHeatmapOptions {
* Sets the name of the cell when not calculating from data
*/
value?: string;
/**
* Controls the scale distribution of the y-axis buckets
*/
yBucketScale?: ui.ScaleDistributionConfig;
}
export interface Options {
+1
View File
@@ -200,6 +200,7 @@ type FrontendSettingsDTO struct {
RudderstackWriteKey string `json:"rudderstackWriteKey"`
RudderstackDataPlaneUrl string `json:"rudderstackDataPlaneUrl"`
RudderstackSdkUrl string `json:"rudderstackSdkUrl"`
RudderstackV3SdkUrl string `json:"rudderstackV3SdkUrl"`
RudderstackConfigUrl string `json:"rudderstackConfigUrl"`
RudderstackIntegrationsUrl string `json:"rudderstackIntegrationsUrl"`
+1
View File
@@ -229,6 +229,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
RudderstackWriteKey: hs.Cfg.RudderstackWriteKey,
RudderstackDataPlaneUrl: hs.Cfg.RudderstackDataPlaneURL,
RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL,
RudderstackV3SdkUrl: hs.Cfg.RudderstackV3SDKURL,
RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL,
RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL,
AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting,
+23 -8
View File
@@ -54,7 +54,8 @@ func init() {
}
logger := level.NewFilter(format(os.Stderr), level.AllowInfo())
root = newManager(logger)
initAppSDKLogger(logger)
// Use default Info level during package initialization before config is loaded
initAppSDKLogger(logger, slog.LevelInfo)
RegisterContextualLogProvider(func(ctx context.Context) ([]any, bool) {
pFromCtx := ctx.Value(logParamsContextKey{})
@@ -80,7 +81,7 @@ func newManager(logger gokitlog.Logger) *logManager {
}
}
func (lm *logManager) initialize(loggers []logWithFilters) {
func (lm *logManager) initialize(loggers []logWithFilters, levelStr string) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
@@ -113,7 +114,7 @@ func (lm *logManager) initialize(loggers []logWithFilters) {
lm.loggersByName[name].Swap(&compositeLogger{loggers: ctxLoggers})
}
initAppSDKLogger(lm.ConcreteLogger)
initAppSDKLogger(lm.ConcreteLogger, stringToSlogLevel(levelStr))
}
func (lm *logManager) New(ctx ...any) *ConcreteLogger {
@@ -514,7 +515,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
configLoggers = append(configLoggers, handler)
}
if len(configLoggers) > 0 {
root.initialize(configLoggers)
root.initialize(configLoggers, defaultLevelName)
}
return nil
@@ -551,8 +552,22 @@ func SetupConsoleLogger(level string) error {
return nil
}
func initAppSDKLogger(gkl gokitlog.Logger) {
// We need to allow Debug logs here. go-kit/log does not support sharing the level we're using.
// TODO: Refactor such that we can pass in a level in a more appropriate manner.
logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, slog.LevelDebug))
// stringToSlogLevel converts a log level string to slog.Level
func stringToSlogLevel(levelStr string) slog.Level {
switch strings.ToLower(levelStr) {
case "trace", "debug":
return slog.LevelDebug
case "info":
return slog.LevelInfo
case "warn", "warning":
return slog.LevelWarn
case "error", "critical":
return slog.LevelError
default:
return slog.LevelInfo
}
}
func initAppSDKLogger(gkl gokitlog.Logger, level slog.Level) {
logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, level))
}
+1 -1
View File
@@ -88,7 +88,7 @@ func TestNew(t *testing.T) {
val: swapLogger,
maxLevel: level.AllowAll(),
},
})
}, "info")
err := log1.Log("msg", "hello 1")
require.NoError(t, err)
+1 -4
View File
@@ -64,10 +64,7 @@ func (l *loggerImpl) Middleware() web.Middleware {
// put the start time on context so we can measure it later.
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
//nolint:staticcheck // not yet migrated to OpenFeature
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) {
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
}
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
rw := web.Rw(w, r)
next.ServeHTTP(rw, r)
+25 -4
View File
@@ -2,10 +2,13 @@ package team
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"log/slog"
"math"
"strconv"
"strings"
"google.golang.org/grpc"
@@ -77,7 +80,7 @@ func (c *LegacyTeamSearchClient) Search(ctx context.Context, req *resourcepb.Res
namespace := signedInUser.GetNamespace()
for _, t := range res.Teams {
cells := createCells(t, req.Fields)
cells := createCells(c.log, t, req.Fields)
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(t, namespace),
Cells: cells,
@@ -102,7 +105,8 @@ func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition {
columns := getDefaultColumns()
for _, field := range fields {
if col, ok := builders.TeamSearchTableColumnDefinitions[field]; ok {
fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX)
if col, ok := builders.TeamSearchTableColumnDefinitions[fieldName]; ok {
columns = append(columns, col)
}
}
@@ -118,16 +122,33 @@ func getDefaultColumns() []*resourcepb.ResourceTableColumnDefinition {
}
}
func createCells(t *team.TeamDTO, fields []string) [][]byte {
func createCells(log *slog.Logger, t *team.TeamDTO, fields []string) [][]byte {
cells := createDefaultCells(t)
for _, field := range fields {
switch field {
fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX)
switch fieldName {
case builders.TEAM_SEARCH_EMAIL:
cells = append(cells, []byte(t.Email))
case builders.TEAM_SEARCH_PROVISIONED:
cells = append(cells, []byte(strconv.FormatBool(t.IsProvisioned)))
case builders.TEAM_SEARCH_EXTERNAL_UID:
cells = append(cells, []byte(t.ExternalUID))
case builders.TEAM_SEARCH_MEMBER_COUNT:
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(t.MemberCount))
cells = append(cells, b)
case builders.TEAM_SEARCH_PERMISSION:
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(t.Permission))
cells = append(cells, b)
case builders.TEAM_SEARCH_ACCESS_CONTROL:
accessControl, err := json.Marshal(t.AccessControl)
if err != nil {
log.Error("error marshalling access control", "error", err)
cells = append(cells, []byte(""))
continue
}
cells = append(cells, accessControl)
}
}
return cells
@@ -2,6 +2,7 @@ package team
import (
"context"
"encoding/binary"
"errors"
"math"
"testing"
@@ -25,7 +26,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
Limit: 10,
Page: 1,
Query: "test",
Fields: []string{"name", "email", "provisioned", "externalUID"},
Fields: []string{"name", "email", "provisioned", "externalUID", "memberCount", "permission", "accessControl"},
}
mockTeamService.ExpectedSearchTeamsResult = team.SearchTeamQueryResult{
@@ -36,6 +37,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
Email: "test@example.com",
IsProvisioned: true,
ExternalUID: "testExternalUID",
MemberCount: 10,
Permission: team.PermissionTypeAdmin,
AccessControl: map[string]bool{"test": true},
},
},
TotalCount: 1,
@@ -48,7 +52,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
require.NoError(t, err)
require.Equal(t, int64(1), resp.TotalHits)
require.Len(t, resp.Results.Rows, 1)
require.Len(t, resp.Results.Columns, 5)
require.Len(t, resp.Results.Columns, 8)
require.Equal(t, "default", resp.Results.Rows[0].Key.Namespace)
require.Equal(t, "iam.grafana.com", resp.Results.Rows[0].Key.Group)
require.Equal(t, "teams", resp.Results.Rows[0].Key.Resource)
@@ -58,6 +62,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
require.Equal(t, "test@example.com", string(resp.Results.Rows[0].Cells[2]))
require.Equal(t, "true", string(resp.Results.Rows[0].Cells[3]))
require.Equal(t, "testExternalUID", string(resp.Results.Rows[0].Cells[4]))
require.Equal(t, 10, int(binary.BigEndian.Uint64(resp.Results.Rows[0].Cells[5])))
require.Equal(t, team.PermissionTypeAdmin, team.PermissionType(binary.BigEndian.Uint32(resp.Results.Rows[0].Cells[6])))
require.Equal(t, "{\"test\":true}", string(resp.Results.Rows[0].Cells[7]))
})
t.Run("returns error if page is negative", func(t *testing.T) {
+22 -4
View File
@@ -2,6 +2,7 @@ package iam
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
@@ -12,6 +13,7 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -137,6 +139,12 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request)
return
}
requester, err := identity.GetRequester(ctx)
if err != nil {
errhttp.Write(ctx, fmt.Errorf("no identity found for request: %w", err), w)
return
}
limit := 50
offset := 0
page := 1
@@ -154,16 +162,26 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request)
}
searchRequest := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{},
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: iamv0alpha1.TeamResourceInfo.GroupResource().Group,
Resource: iamv0alpha1.TeamResourceInfo.GroupResource().Resource,
Namespace: requester.GetNamespace(),
},
},
Query: queryParams.Get("query"),
Limit: int64(limit),
Offset: int64(offset),
Page: int64(page),
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
Fields: []string{
builders.TEAM_SEARCH_EMAIL,
builders.TEAM_SEARCH_PROVISIONED,
builders.TEAM_SEARCH_EXTERNAL_UID,
resource.SEARCH_FIELD_TITLE,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EMAIL,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PROVISIONED,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EXTERNAL_UID,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_MEMBER_COUNT,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PERMISSION,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_ACCESS_CONTROL,
},
}
+1 -1
View File
@@ -89,7 +89,7 @@ func TestTeamSearchHandler(t *testing.T) {
if mockClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
expectedFields := []string{"email", "provisioned", "externalUID"}
expectedFields := []string{"title", "fields.email", "fields.provisioned", "fields.externalUID", "fields.memberCount", "fields.permission", "fields.accessControl"}
if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) {
t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields)
}
@@ -9,10 +9,13 @@ import (
"maps"
"os"
"slices"
"time"
"github.com/fullstorydev/grpchan"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
@@ -72,6 +75,15 @@ func NewGRPCDecryptClientWithTLS(
opts = append(opts, grpc.WithDisableServiceConfig())
}
// Add retry interceptor to retry on transient connection issues.
// Retries on ResourceExhausted (per-RPC limits reached) and Unavailable (system unavailable).
retryInterceptor := grpc_retry.UnaryClientInterceptor(
grpc_retry.WithMax(3),
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)),
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
)
opts = append(opts, grpc.WithUnaryInterceptor(retryInterceptor))
conn, err := grpc.NewClient(address, opts...)
if err != nil {
return nil, fmt.Errorf("failed to connect to grpc decrypt server at %s: %w", address, err)
+1 -3
View File
@@ -23,9 +23,7 @@ type FeatureToggles interface {
// a full server restart for a change to take place.
//
// Deprecated: FeatureToggles.IsEnabledGlobally is deprecated and will be removed in a future release.
// Toggles that must be reliably evaluated at the service startup should be
// changed to settings (see setting.StartupSettings), and/or removed entirely.
// For app registration please use `grafana-apiserver.runtime_config` in settings.ini
// Toggles that must be reliably evaluated at the service startup should be changed to settings and/or removed entirely.
IsEnabledGlobally(flag string) bool
// Get the enabled flags -- this *may* also include disabled flags (with value false)
+15 -15
View File
@@ -185,13 +185,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaDatasourcesCoreServicesSquad,
},
{
Name: "unifiedRequestLog",
Description: "Writes error logs to the request logger",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaBackendGroup,
Expression: "true",
},
{
Name: "renderAuthJWT",
Description: "Uses JWT-based auth for rendering instead of relying on remote cache",
@@ -820,6 +813,13 @@ var (
Owner: grafanaDashboardsSquad,
HideFromDocs: true,
},
{
Name: "dashboardAdHocAndGroupByWrapper",
Description: "Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it",
Stage: FeatureStageExperimental,
Owner: grafanaDashboardsSquad,
HideFromDocs: true,
},
{
Name: "cloudWatchNewLabelParsing",
Description: "Updates CloudWatch label parsing to be more accurate",
@@ -1921,6 +1921,14 @@ var (
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "heatmapRowsAxisOptions",
Description: "Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows)",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "preventPanelChromeOverflow",
Description: "Restrict PanelChrome contents with overflow: hidden;",
@@ -1960,14 +1968,6 @@ var (
Owner: identityAccessTeam,
Expression: "true",
},
{
Name: "pluginInsights",
Description: "Show insights for plugins in the plugin details page",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaPluginsPlatformSquad,
Expression: "false",
},
{
Name: "panelTimeSettings",
Description: "Enables a new panel time settings drawer",
+2 -2
View File
@@ -24,7 +24,6 @@ influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,fa
influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false
lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false
disableSSEDataplane,experimental,@grafana/grafana-datasources-core-services,false,false,false
unifiedRequestLog,GA,@grafana/grafana-backend-group,false,false,false
renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false
refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false
faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true
@@ -113,6 +112,7 @@ scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
refreshTokenRequired,experimental,@grafana/identity-access-team,false,false,false
newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,false,false
dashboardAdHocAndGroupByWrapper,experimental,@grafana/dashboards-squad,false,false,false
cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
@@ -261,12 +261,12 @@ pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false,
newGauge,experimental,@grafana/dataviz-squad,false,false,true
newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true
externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true
heatmapRowsAxisOptions,experimental,@grafana/dataviz-squad,false,false,true
preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
newPanelPadding,preview,@grafana/dashboards-squad,false,false,true
onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
pluginInsights,experimental,@grafana/plugins-platform-backend,false,false,true
panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
elasticsearchRawDSLQuery,experimental,@grafana/partner-datasources,false,false,false
kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
24 influxdbRunQueriesInParallel privatePreview @grafana/partner-datasources false false false
25 lokiLogsDataplane experimental @grafana/observability-logs false false false
26 disableSSEDataplane experimental @grafana/grafana-datasources-core-services false false false
unifiedRequestLog GA @grafana/grafana-backend-group false false false
27 renderAuthJWT preview @grafana/grafana-operator-experience-squad false false false
28 refactorVariablesTimeRange preview @grafana/dashboards-squad false false false
29 faroDatasourceSelector preview @grafana/app-o11y false false true
112 oauthRequireSubClaim experimental @grafana/identity-access-team false false false
113 refreshTokenRequired experimental @grafana/identity-access-team false false false
114 newDashboardWithFiltersAndGroupBy experimental @grafana/dashboards-squad false false false
115 dashboardAdHocAndGroupByWrapper experimental @grafana/dashboards-squad false false false
116 cloudWatchNewLabelParsing GA @grafana/aws-datasources false false false
117 disableNumericMetricsSortingInExpressions experimental @grafana/oss-big-tent false true false
118 grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
261 newGauge experimental @grafana/dataviz-squad false false true
262 newVizSuggestions preview @grafana/dataviz-squad false false true
263 externalVizSuggestions experimental @grafana/dataviz-squad false false true
264 heatmapRowsAxisOptions experimental @grafana/dataviz-squad false false true
265 preventPanelChromeOverflow preview @grafana/grafana-frontend-platform false false true
266 jaegerEnableGrpcEndpoint experimental @grafana/oss-big-tent false false false
267 pluginStoreServiceLoading experimental @grafana/plugins-platform-backend false false false
268 newPanelPadding preview @grafana/dashboards-squad false false true
269 onlyStoreActionSets GA @grafana/identity-access-team false false false
pluginInsights experimental @grafana/plugins-platform-backend false false true
270 panelTimeSettings experimental @grafana/dashboards-squad false false false
271 elasticsearchRawDSLQuery experimental @grafana/partner-datasources false false false
272 kubernetesAnnotations experimental @grafana/grafana-backend-services-squad false false false
+4 -4
View File
@@ -79,10 +79,6 @@ const (
// Disables dataplane specific processing in server side expressions.
FlagDisableSSEDataplane = "disableSSEDataplane"
// FlagUnifiedRequestLog
// Writes error logs to the request logger
FlagUnifiedRequestLog = "unifiedRequestLog"
// FlagRenderAuthJWT
// Uses JWT-based auth for rendering instead of relying on remote cache
FlagRenderAuthJWT = "renderAuthJWT"
@@ -339,6 +335,10 @@ const (
// Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.
FlagNewDashboardWithFiltersAndGroupBy = "newDashboardWithFiltersAndGroupBy"
// FlagDashboardAdHocAndGroupByWrapper
// Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it
FlagDashboardAdHocAndGroupByWrapper = "dashboardAdHocAndGroupByWrapper"
// FlagCloudWatchNewLabelParsing
// Updates CloudWatch label parsing to be more accurate
FlagCloudWatchNewLabelParsing = "cloudWatchNewLabelParsing"
+29 -15
View File
@@ -922,6 +922,19 @@
"frontend": true
}
},
{
"metadata": {
"name": "dashboardAdHocAndGroupByWrapper",
"resourceVersion": "1765841806645",
"creationTimestamp": "2025-12-15T23:36:46Z"
},
"spec": {
"description": "Wraps the ad hoc and group by variables in a single wrapper, with all other variables below it",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"hideFromDocs": true
}
},
{
"metadata": {
"name": "dashboardDisableSchemaValidationV1",
@@ -1634,6 +1647,20 @@
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "heatmapRowsAxisOptions",
"resourceVersion": "1765353244400",
"creationTimestamp": "2025-12-10T07:54:04Z"
},
"spec": {
"description": "Enable Y-axis scale configuration options for pre-bucketed heatmap data (heatmap-rows)",
"stage": "experimental",
"codeowner": "@grafana/dataviz-squad",
"frontend": true,
"expression": "false"
}
},
{
"metadata": {
"name": "improvedExternalSessionHandling",
@@ -2693,20 +2720,6 @@
"expression": "false"
}
},
{
"metadata": {
"name": "pluginInsights",
"resourceVersion": "1761300628147",
"creationTimestamp": "2025-10-24T10:10:28Z"
},
"spec": {
"description": "Show insights for plugins in the plugin details page",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend",
"frontend": true,
"expression": "false"
}
},
{
"metadata": {
"name": "pluginInstallAPISync",
@@ -3502,7 +3515,8 @@
"metadata": {
"name": "unifiedRequestLog",
"resourceVersion": "1764664939750",
"creationTimestamp": "2023-03-31T13:38:09Z"
"creationTimestamp": "2023-03-31T13:38:09Z",
"deletionTimestamp": "2025-12-18T14:21:02Z"
},
"spec": {
"description": "Writes error logs to the request logger",
@@ -32,6 +32,7 @@ type FSFrontendSettings struct {
RudderstackWriteKey string `json:"rudderstackWriteKey,omitempty"`
RudderstackDataPlaneUrl string `json:"rudderstackDataPlaneUrl,omitempty"`
RudderstackSdkUrl string `json:"rudderstackSdkUrl,omitempty"`
RudderstackV3SdkUrl string `json:"rudderstackV3SdkUrl,omitempty"`
RudderstackConfigUrl string `json:"rudderstackConfigUrl,omitempty"`
RudderstackIntegrationsUrl string `json:"rudderstackIntegrationsUrl,omitempty"`
+1
View File
@@ -94,6 +94,7 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li
RudderstackDataPlaneUrl: cfg.RudderstackDataPlaneURL,
RudderstackIntegrationsUrl: cfg.RudderstackIntegrationsURL,
RudderstackSdkUrl: cfg.RudderstackSDKURL,
RudderstackV3SdkUrl: cfg.RudderstackV3SDKURL,
RudderstackWriteKey: cfg.RudderstackWriteKey,
TrustedTypesDefaultPolicyEnabled: (cfg.CSPEnabled && strings.Contains(cfg.CSPTemplate, "require-trusted-types-for")) || (cfg.CSPReportOnlyEnabled && strings.Contains(cfg.CSPReportOnlyTemplate, "require-trusted-types-for")),
VerifyEmailEnabled: cfg.VerifyEmailEnabled,
+31
View File
@@ -1,9 +1,12 @@
package search
import (
"encoding/binary"
"encoding/json"
"fmt"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search/builders"
@@ -22,6 +25,9 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
emailIDX := -1
provisionedIDX := -1
externalUIDIDX := -1
memberCountIDX := -1
permissionIDX := -1
accessControlIDX := -1
for i, v := range result.Results.Columns {
if v == nil {
@@ -37,6 +43,12 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
provisionedIDX = i
case builders.TEAM_SEARCH_EXTERNAL_UID:
externalUIDIDX = i
case builders.TEAM_SEARCH_MEMBER_COUNT:
memberCountIDX = i
case builders.TEAM_SEARCH_PERMISSION:
permissionIDX = i
case builders.TEAM_SEARCH_ACCESS_CONTROL:
accessControlIDX = i
}
}
@@ -75,6 +87,25 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
hit.ExternalUID = string(row.Cells[externalUIDIDX])
}
if memberCountIDX >= 0 && row.Cells[memberCountIDX] != nil {
memberCount := binary.BigEndian.Uint64(row.Cells[memberCountIDX])
hit.MemberCount = int64(memberCount)
}
if permissionIDX >= 0 && row.Cells[permissionIDX] != nil {
permission := binary.BigEndian.Uint32(row.Cells[permissionIDX])
hit.Permission = team.PermissionType(permission)
}
if accessControlIDX >= 0 && row.Cells[accessControlIDX] != nil {
var accessControl map[string]bool
err := json.Unmarshal(row.Cells[accessControlIDX], &accessControl)
if err != nil {
return v0alpha1.TeamSearchResults{}, fmt.Errorf("error parsing team search response: error unmarshalling access control: %w", err)
}
hit.AccessControl = accessControl
}
sr.Hits[i] = *hit
}
+28
View File
@@ -1,10 +1,12 @@
package search
import (
"encoding/binary"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
@@ -29,6 +31,18 @@ func TestParseResults(t *testing.T) {
Name: "externalUID",
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
{
Name: "memberCount",
Type: resourcepb.ResourceTableColumnDefinition_INT64,
},
{
Name: "permission",
Type: resourcepb.ResourceTableColumnDefinition_INT32,
},
{
Name: "accessControl",
Type: resourcepb.ResourceTableColumnDefinition_OBJECT,
},
},
Rows: []*resourcepb.ResourceTableRow{
{
@@ -41,6 +55,17 @@ func TestParseResults(t *testing.T) {
[]byte("team1@example.com"),
[]byte("true"),
[]byte("team1-uid"),
func() []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, 10)
return b
}(),
func() []byte {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, 4)
return b
}(),
[]byte("{\"test\":true}"),
},
},
},
@@ -55,6 +80,9 @@ func TestParseResults(t *testing.T) {
require.Equal(t, "team1@example.com", results.Hits[0].Email)
require.True(t, results.Hits[0].Provisioned)
require.Equal(t, "team1-uid", results.Hits[0].ExternalUID)
require.Equal(t, int64(10), results.Hits[0].MemberCount)
require.Equal(t, team.PermissionTypeAdmin, results.Hits[0].Permission)
require.Equal(t, map[string]bool{"test": true}, results.Hits[0].AccessControl)
})
t.Run("should handle nil result", func(t *testing.T) {
+2
View File
@@ -414,6 +414,7 @@ type Cfg struct {
RudderstackDataPlaneURL string
RudderstackWriteKey string
RudderstackSDKURL string
RudderstackV3SDKURL string
RudderstackConfigURL string
RudderstackIntegrationsURL string
IntercomSecret string
@@ -1281,6 +1282,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
cfg.RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
cfg.RudderstackDataPlaneURL = analytics.Key("rudderstack_data_plane_url").String()
cfg.RudderstackSDKURL = analytics.Key("rudderstack_sdk_url").String()
cfg.RudderstackV3SDKURL = analytics.Key("rudderstack_v3_sdk_url").String()
cfg.RudderstackConfigURL = analytics.Key("rudderstack_config_url").String()
cfg.RudderstackIntegrationsURL = analytics.Key("rudderstack_integrations_url").String()
cfg.IntercomSecret = analytics.Key("intercom_secret").String()
@@ -0,0 +1,12 @@
SELECT r.{{ .Ident "key_path" }}, r.{{ .Ident "value" }}
FROM (
{{ range $id, $key_path := .KeyPaths }}
{{ if eq $id 0 }}
SELECT {{ $.Arg $id }} AS idx, {{ $.Arg $key_path }} AS key_path
{{ else }}
UNION ALL SELECT {{ $.Arg $id }}, {{ $.Arg $key_path }}
{{ end }}
{{ end }}
) AS requested_keys
INNER JOIN {{ .TableName }} r ON r.{{ .Ident "key_path" }} = requested_keys.{{ .Ident "key_path" }}
ORDER BY requested_keys.{{ .Ident "idx" }};
+70 -8
View File
@@ -34,9 +34,10 @@ func mustTemplate(filename string) *template.Template {
// Templates.
var (
sqlKVGet = mustTemplate("sqlkv_get.sql")
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
sqlKVKeys = mustTemplate("sqlkv_keys.sql")
sqlKVKeys = mustTemplate("sqlkv_keys.sql")
sqlKVGet = mustTemplate("sqlkv_get.sql")
sqlKVBatchGet = mustTemplate("sqlkv_batch_get.sql")
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
)
// sqlKVSection can be embedded in structs used when rendering query templates
@@ -107,13 +108,23 @@ func (req sqlKVGetRequest) Results() ([]byte, error) {
return req.Value, nil
}
type sqlKVDeleteRequest struct {
type sqlKVBatchGetRequest struct {
sqltemplate.SQLTemplate
sqlKVSectionKey
sqlKVSection
Keys []string
}
func (req sqlKVDeleteRequest) Validate() error {
return req.sqlKVSectionKey.Validate()
func (req sqlKVBatchGetRequest) Validate() error {
return req.sqlKVSection.Validate()
}
func (req sqlKVBatchGetRequest) KeyPaths() []string {
result := make([]string, 0, len(req.Keys))
for _, key := range req.Keys {
result = append(result, req.Section+"/"+key)
}
return result
}
type sqlKVKeysRequest struct {
@@ -142,6 +153,15 @@ func (req sqlKVKeysRequest) SortAscending() bool {
return req.Options.Sort != SortOrderDesc
}
type sqlKVDeleteRequest struct {
sqltemplate.SQLTemplate
sqlKVSectionKey
}
func (req sqlKVDeleteRequest) Validate() error {
return req.sqlKVSectionKey.Validate()
}
var _ KV = &sqlKV{}
type sqlKV struct {
@@ -188,6 +208,7 @@ func (k *sqlKV) Keys(ctx context.Context, section string, opt ListOptions) iter.
yield("", err)
return
}
defer closeRows(rows, yield)
for rows.Next() {
var key string
@@ -225,7 +246,41 @@ func (k *sqlKV) Get(ctx context.Context, section string, key string) (io.ReadClo
func (k *sqlKV) BatchGet(ctx context.Context, section string, keys []string) iter.Seq2[KeyValue, error] {
return func(yield func(KeyValue, error) bool) {
panic("not implemented!")
if len(keys) == 0 {
return
}
rows, err := dbutil.QueryRows(ctx, k.db, sqlKVBatchGet, sqlKVBatchGetRequest{
SQLTemplate: sqltemplate.New(k.dialect),
sqlKVSection: sqlKVSection{section},
Keys: keys,
})
if err != nil {
yield(KeyValue{}, err)
return
}
defer closeRows(rows, yield)
for rows.Next() {
var key string
var value []byte
if err := rows.Scan(&key, &value); err != nil {
yield(KeyValue{}, fmt.Errorf("error reading row: %w", err))
return
}
kv := KeyValue{
Key: strings.TrimPrefix(key, section+"/"),
Value: io.NopCloser(bytes.NewReader(value)),
}
if !yield(kv, nil) {
return
}
}
if err := rows.Err(); err != nil {
yield(KeyValue{}, fmt.Errorf("failed to read rows: %w", err))
}
}
}
@@ -273,3 +328,10 @@ func (k *sqlKV) BatchDelete(ctx context.Context, section string, keys []string)
func (k *sqlKV) UnixTimestamp(ctx context.Context) (int64, error) {
panic("not implemented!")
}
func closeRows[T any](rows db.Rows, yield func(T, error) bool) {
if err := rows.Close(); err != nil {
var zero T
yield(zero, fmt.Errorf("error closing rows: %w", err))
}
}
@@ -14,9 +14,12 @@ import (
)
const (
TEAM_SEARCH_EMAIL = "email"
TEAM_SEARCH_PROVISIONED = "provisioned"
TEAM_SEARCH_EXTERNAL_UID = "externalUID"
TEAM_SEARCH_EMAIL = "email"
TEAM_SEARCH_PROVISIONED = "provisioned"
TEAM_SEARCH_EXTERNAL_UID = "externalUID"
TEAM_SEARCH_MEMBER_COUNT = "memberCount"
TEAM_SEARCH_PERMISSION = "permission"
TEAM_SEARCH_ACCESS_CONTROL = "accessControl"
)
var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{
@@ -35,6 +38,21 @@ var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColum
Type: resourcepb.ResourceTableColumnDefinition_STRING,
Description: "External UID of the team",
},
TEAM_SEARCH_MEMBER_COUNT: {
Name: TEAM_SEARCH_MEMBER_COUNT,
Type: resourcepb.ResourceTableColumnDefinition_INT64,
Description: "Number of members in the team",
},
TEAM_SEARCH_PERMISSION: {
Name: TEAM_SEARCH_PERMISSION,
Type: resourcepb.ResourceTableColumnDefinition_INT32,
Description: "Permission of the team",
},
TEAM_SEARCH_ACCESS_CONTROL: {
Name: TEAM_SEARCH_ACCESS_CONTROL,
Type: resourcepb.ResourceTableColumnDefinition_OBJECT,
Description: "Access control of the team",
},
}
func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) {
@@ -46,8 +64,8 @@ func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) {
return resource.DocumentBuilderInfo{
GroupResource: schema.GroupResource{
Group: "iam.grafana.app",
Resource: "searchTeams",
Group: v0alpha1.TeamResourceInfo.GroupResource().Group,
Resource: v0alpha1.TeamResourceInfo.GroupResource().Resource,
},
Fields: fields,
Builder: new(teamSearchBuilder),
+5 -4
View File
@@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/util/debouncer"
)
@@ -126,7 +127,7 @@ type backend struct {
notifier eventNotifier
// resource version manager
rvManager *resourceVersionManager
rvManager *rvmanager.ResourceVersionManager
// testing
simulatedNetworkLatency time.Duration
@@ -163,7 +164,7 @@ func (b *backend) initLocked(ctx context.Context) error {
}
// Initialize ResourceVersionManager
rvManager, err := NewResourceVersionManager(ResourceManagerOptions{
rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{
Dialect: b.dialect,
DB: b.db,
})
@@ -928,12 +929,12 @@ func (b *backend) listLatestRVs(ctx context.Context) (groupResourceRV, error) {
func (b *backend) fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, group, resource string) (int64, error) {
ctx, span := tracer.Start(ctx, "sql.backend.fetchLatestRV")
defer span.End()
res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{
res, err := dbutil.QueryRow(ctx, x, rvmanager.SqlResourceVersionGet, rvmanager.SqlResourceVersionGetRequest{
SQLTemplate: sqltemplate.New(d),
Group: group,
Resource: resource,
ReadOnly: true,
Response: new(resourceVersionResponse),
Response: new(rvmanager.ResourceVersionResponse),
})
if errors.Is(err, sql.ErrNoRows) {
return 1, nil
+20
View File
@@ -40,6 +40,26 @@ type testBackend struct {
test.TestDBProvider
}
func expectSuccessfulResourceVersionLock(t *testing.T, dbp test.TestDBProvider, rv int64, timestamp int64) {
dbp.SQLMock.ExpectQuery("select resource_version, unix_timestamp for update").
WillReturnRows(sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
AddRow(rv, timestamp))
}
func expectSuccessfulResourceVersionSaveRV(t *testing.T, dbp test.TestDBProvider) {
dbp.SQLMock.ExpectExec("update resource set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
dbp.SQLMock.ExpectExec("update resource_history set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
dbp.SQLMock.ExpectExec("update resource_version set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
}
func expectSuccessfulResourceVersionExec(t *testing.T, dbp test.TestDBProvider, cbs ...func()) {
for _, cb := range cbs {
cb()
}
expectSuccessfulResourceVersionLock(t, dbp, 100, 200)
expectSuccessfulResourceVersionSaveRV(t, dbp)
}
func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) {
b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected))
}
+2 -2
View File
@@ -281,13 +281,13 @@ func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resou
}
if b.dialect.DialectName() == "sqlite" {
nextRV, err := b.rvManager.lock(ctx, tx, key.Group, key.Resource)
nextRV, err := b.rvManager.Lock(ctx, tx, key.Group, key.Resource)
if err != nil {
b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key))
} else {
b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key))
// Save the incremented RV
if err := b.rvManager.saveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil {
if err := b.rvManager.SaveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil {
b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key))
} else {
b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key))
@@ -17,6 +17,7 @@ import (
dbsql "github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util/testutil"
@@ -94,7 +95,7 @@ func TestIntegrationListIter(t *testing.T) {
return fmt.Errorf("failed to insert test data: %w", err)
}
if _, err = dbutil.Exec(ctx, tx, sqlResourceUpdateRV, sqlResourceUpdateRVRequest{
if _, err = dbutil.Exec(ctx, tx, rvmanager.SqlResourceUpdateRV, rvmanager.SqlResourceUpdateRVRequest{
SQLTemplate: sqltemplate.New(dialect),
GUIDToRV: map[string]int64{
item.guid: item.resourceVersion,
+1 -71
View File
@@ -38,10 +38,8 @@ var (
sqlResourceList = mustTemplate("resource_list.sql")
sqlResourceHistoryList = mustTemplate("resource_history_list.sql")
sqlResourceHistoryListModifiedSince = mustTemplate("resource_history_list_since_modified.sql")
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
sqlResourceHistoryRead = mustTemplate("resource_history_read.sql")
sqlResourceHistoryReadLatestRV = mustTemplate("resource_history_read_latest_rv.sql")
sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql")
sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql")
sqlResourceHistoryGet = mustTemplate("resource_history_get.sql")
@@ -51,10 +49,7 @@ var (
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
sqlResourceVersionGet = mustTemplate("resource_version_get.sql")
sqlResourceVersionUpdate = mustTemplate("resource_version_update.sql")
sqlResourceVersionInsert = mustTemplate("resource_version_insert.sql")
sqlResourceVersionList = mustTemplate("resource_version_list.sql")
sqlResourceVersionList = mustTemplate("resource_version_list.sql")
sqlResourceBlobInsert = mustTemplate("resource_blob_insert.sql")
sqlResourceBlobQuery = mustTemplate("resource_blob_query.sql")
@@ -365,76 +360,11 @@ func (r sqlResourceBlobQueryRequest) Validate() error {
return nil
}
// update RV
type sqlResourceUpdateRVRequest struct {
sqltemplate.SQLTemplate
GUIDToRV map[string]int64
GUIDToSnowflakeRV map[string]int64
}
func (r sqlResourceUpdateRVRequest) Validate() error {
return nil // TODO
}
func (r sqlResourceUpdateRVRequest) SlashFunc() string {
if r.DialectName() == "postgres" {
return "CHR(47)"
}
return "CHAR(47)"
}
func (r sqlResourceUpdateRVRequest) TildeFunc() string {
if r.DialectName() == "postgres" {
return "CHR(126)"
}
return "CHAR(126)"
}
// resource_version table requests.
type resourceVersionResponse struct {
ResourceVersion int64
CurrentEpoch int64
}
func (r *resourceVersionResponse) Results() (*resourceVersionResponse, error) {
return r, nil
}
type groupResourceVersion struct {
Group, Resource string
ResourceVersion int64
}
type sqlResourceVersionUpsertRequest struct {
sqltemplate.SQLTemplate
Group, Resource string
ResourceVersion int64
}
func (r sqlResourceVersionUpsertRequest) Validate() error {
return nil // TODO
}
type sqlResourceVersionGetRequest struct {
sqltemplate.SQLTemplate
Group, Resource string
ReadOnly bool
Response *resourceVersionResponse
}
func (r sqlResourceVersionGetRequest) Validate() error {
return nil // TODO
}
func (r sqlResourceVersionGetRequest) Results() (*resourceVersionResponse, error) {
return &resourceVersionResponse{
ResourceVersion: r.Response.ResourceVersion,
CurrentEpoch: r.Response.CurrentEpoch,
}, nil
}
type sqlResourceVersionListRequest struct {
sqltemplate.SQLTemplate
*groupResourceVersion
+12 -11
View File
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
@@ -162,10 +163,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
sqlResourceUpdateRV: {
rvmanager.SqlResourceUpdateRV: {
{
Name: "single path",
Data: &sqlResourceUpdateRVRequest{
Data: &rvmanager.SqlResourceUpdateRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
GUIDToRV: map[string]int64{
"guid1": 123,
@@ -228,10 +229,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
sqlResourceHistoryUpdateRV: {
rvmanager.SqlResourceHistoryUpdateRV: {
{
Name: "single path",
Data: &sqlResourceUpdateRVRequest{
Data: &rvmanager.SqlResourceUpdateRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
GUIDToRV: map[string]int64{
"guid1": 123,
@@ -334,23 +335,23 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
sqlResourceVersionGet: {
rvmanager.SqlResourceVersionGet: {
{
Name: "single path",
Data: &sqlResourceVersionGetRequest{
Data: &rvmanager.SqlResourceVersionGetRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "resource",
Group: "group",
Response: new(resourceVersionResponse),
Response: new(rvmanager.ResourceVersionResponse),
ReadOnly: false,
},
},
},
sqlResourceVersionUpdate: {
rvmanager.SqlResourceVersionUpdate: {
{
Name: "increment resource version",
Data: &sqlResourceVersionUpsertRequest{
Data: &rvmanager.SqlResourceVersionUpsertRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "resource",
Group: "group",
@@ -359,10 +360,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
sqlResourceVersionInsert: {
rvmanager.SqlResourceVersionInsert: {
{
Name: "single path",
Data: &sqlResourceVersionUpsertRequest{
Data: &rvmanager.SqlResourceVersionUpsertRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
ResourceVersion: int64(12354),
},
@@ -0,0 +1,84 @@
package rvmanager
import (
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
type SqlResourceUpdateRVRequest struct {
sqltemplate.SQLTemplate
GUIDToRV map[string]int64
GUIDToSnowflakeRV map[string]int64
}
func (r SqlResourceUpdateRVRequest) Validate() error {
return nil // TODO
}
func (r SqlResourceUpdateRVRequest) SlashFunc() string {
if r.DialectName() == "postgres" {
return "CHR(47)"
}
return "CHAR(47)"
}
func (r SqlResourceUpdateRVRequest) TildeFunc() string {
if r.DialectName() == "postgres" {
return "CHR(126)"
}
return "CHAR(126)"
}
type ResourceVersionResponse struct {
ResourceVersion int64
CurrentEpoch int64
}
func (r *ResourceVersionResponse) Results() (*ResourceVersionResponse, error) {
return r, nil
}
type sqlResourceVersionGetRequest struct {
sqltemplate.SQLTemplate
Group, Resource string
ReadOnly bool
Response *ResourceVersionResponse
}
func (r sqlResourceVersionGetRequest) Validate() error {
return nil // TODO
}
func (r sqlResourceVersionGetRequest) Results() (*ResourceVersionResponse, error) {
return &ResourceVersionResponse{
ResourceVersion: r.Response.ResourceVersion,
CurrentEpoch: r.Response.CurrentEpoch,
}, nil
}
type SqlResourceVersionUpsertRequest struct {
sqltemplate.SQLTemplate
Group, Resource string
ResourceVersion int64
}
func (r SqlResourceVersionUpsertRequest) Validate() error {
return nil // TODO
}
type SqlResourceVersionGetRequest struct {
sqltemplate.SQLTemplate
Group, Resource string
ReadOnly bool
Response *ResourceVersionResponse
}
func (r SqlResourceVersionGetRequest) Validate() error {
return nil // TODO
}
func (r SqlResourceVersionGetRequest) Results() (*ResourceVersionResponse, error) {
return &ResourceVersionResponse{
ResourceVersion: r.Response.ResourceVersion,
CurrentEpoch: r.Response.CurrentEpoch,
}, nil
}
@@ -1,4 +1,4 @@
package sql
package rvmanager
import (
"context"
@@ -11,6 +11,7 @@ import (
"github.com/bwmarrin/snowflake"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -20,6 +21,8 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager")
var (
rvmWriteDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "rvmanager_write_duration_seconds",
@@ -62,8 +65,8 @@ const (
defaultBatchTimeout = 5 * time.Second
)
// resourceVersionManager handles resource version operations
type resourceVersionManager struct {
// ResourceVersionManager handles resource version operations
type ResourceVersionManager struct {
dialect sqltemplate.Dialect
db db.DB
batchMu sync.RWMutex
@@ -100,7 +103,7 @@ type ResourceManagerOptions struct {
}
// NewResourceVersionManager creates a new ResourceVersionManager
func NewResourceVersionManager(opts ResourceManagerOptions) (*resourceVersionManager, error) {
func NewResourceVersionManager(opts ResourceManagerOptions) (*ResourceVersionManager, error) {
if opts.MaxBatchSize == 0 {
opts.MaxBatchSize = defaultMaxBatchSize
}
@@ -113,7 +116,7 @@ func NewResourceVersionManager(opts ResourceManagerOptions) (*resourceVersionMan
if opts.DB == nil {
return nil, errors.New("db is required")
}
return &resourceVersionManager{
return &ResourceVersionManager{
dialect: opts.Dialect,
db: opts.DB,
batchChMap: make(map[string]chan *writeOp),
@@ -123,7 +126,7 @@ func NewResourceVersionManager(opts ResourceManagerOptions) (*resourceVersionMan
}
// ExecWithRV executes the given function with an incremented resource version
func (m *resourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb.ResourceKey, fn WriteEventFunc) (rv int64, err error) {
func (m *ResourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb.ResourceKey, fn WriteEventFunc) (rv int64, err error) {
rvmInflightWrites.WithLabelValues(key.Group, key.Resource).Inc()
defer rvmInflightWrites.WithLabelValues(key.Group, key.Resource).Dec()
@@ -179,7 +182,7 @@ func (m *resourceVersionManager) ExecWithRV(ctx context.Context, key *resourcepb
}
// startBatchProcessor is responsible for processing batches of write operations
func (m *resourceVersionManager) startBatchProcessor(group, resource string) {
func (m *ResourceVersionManager) startBatchProcessor(group, resource string) {
ctx := context.TODO()
batchKey := fmt.Sprintf("%s/%s", group, resource)
@@ -216,7 +219,11 @@ func (m *resourceVersionManager) startBatchProcessor(group, resource string) {
}
}
func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource string, batch []writeOp) {
var readCommitted = &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
}
func (m *ResourceVersionManager) execBatch(ctx context.Context, group, resource string, batch []writeOp) {
ctx, span := tracer.Start(ctx, "sql.resourceVersionManager.execBatch")
defer span.End()
@@ -245,7 +252,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource
guids := make([]string, len(batch)) // The GUIDs of the created resources in the same order as the batch
rvs := make([]int64, len(batch)) // The RVs of the created resources in the same order as the batch
err = m.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error {
err = m.db.WithTx(ctx, readCommitted, func(ctx context.Context, tx db.Tx) error {
span.AddEvent("starting_batch_transaction")
writeTimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
@@ -268,7 +275,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource
lockTimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
rvmExecBatchPhaseDuration.WithLabelValues(group, resource, "waiting_for_lock").Observe(v)
}))
rv, err := m.lock(ctx, tx, group, resource)
rv, err := m.Lock(ctx, tx, group, resource)
lockTimer.ObserveDuration()
if err != nil {
span.AddEvent("resource_version_lock_failed", trace.WithAttributes(
@@ -292,7 +299,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource
rv++
}
// Update the resource version for the created resources in both the resource and the resource history
if _, err := dbutil.Exec(ctx, tx, sqlResourceUpdateRV, sqlResourceUpdateRVRequest{
if _, err := dbutil.Exec(ctx, tx, SqlResourceUpdateRV, SqlResourceUpdateRVRequest{
SQLTemplate: sqltemplate.New(m.dialect),
GUIDToRV: guidToRV,
}); err != nil {
@@ -303,7 +310,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource
}
span.AddEvent("resource_versions_updated")
if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryUpdateRV, sqlResourceUpdateRVRequest{
if _, err := dbutil.Exec(ctx, tx, SqlResourceHistoryUpdateRV, SqlResourceUpdateRVRequest{
SQLTemplate: sqltemplate.New(m.dialect),
GUIDToRV: guidToRV,
GUIDToSnowflakeRV: guidToSnowflakeRV,
@@ -316,7 +323,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource
span.AddEvent("resource_history_versions_updated")
// Record the latest RV in the resource version table
err = m.saveRV(ctx, tx, group, resource, rv)
err = m.SaveRV(ctx, tx, group, resource, rv)
if err != nil {
span.AddEvent("save_rv_failed", trace.WithAttributes(
attribute.String("error", err.Error()),
@@ -350,20 +357,20 @@ func snowflakeFromRv(rv int64) int64 {
return (((rv / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (rv % 1000)
}
// lock locks the resource version for the given key
func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, group, resource string) (nextRV int64, err error) {
// Lock locks the resource version for the given key
func (m *ResourceVersionManager) Lock(ctx context.Context, x db.ContextExecer, group, resource string) (nextRV int64, err error) {
// 1. Lock the row and prevent concurrent updates until the transaction is committed
res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{
res, err := dbutil.QueryRow(ctx, x, SqlResourceVersionGet, sqlResourceVersionGetRequest{
SQLTemplate: sqltemplate.New(m.dialect),
Group: group,
Resource: resource,
Response: new(resourceVersionResponse),
Response: new(ResourceVersionResponse),
ReadOnly: false, // Lock the row for update
})
if errors.Is(err, sql.ErrNoRows) {
// If there wasn't a row for this resource, create it
if _, err = dbutil.Exec(ctx, x, sqlResourceVersionInsert, sqlResourceVersionUpsertRequest{
if _, err = dbutil.Exec(ctx, x, SqlResourceVersionInsert, SqlResourceVersionUpsertRequest{
SQLTemplate: sqltemplate.New(m.dialect),
Group: group,
Resource: resource,
@@ -372,11 +379,11 @@ func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, g
}
// Fetch the newly created resource version
res, err = dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{
res, err = dbutil.QueryRow(ctx, x, SqlResourceVersionGet, sqlResourceVersionGetRequest{
SQLTemplate: sqltemplate.New(m.dialect),
Group: group,
Resource: resource,
Response: new(resourceVersionResponse),
Response: new(ResourceVersionResponse),
ReadOnly: true,
})
if err != nil {
@@ -390,8 +397,8 @@ func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, g
return max(res.CurrentEpoch, res.ResourceVersion+1), nil
}
func (m *resourceVersionManager) saveRV(ctx context.Context, x db.ContextExecer, group, resource string, rv int64) error {
_, err := dbutil.Exec(ctx, x, sqlResourceVersionUpdate, sqlResourceVersionUpsertRequest{
func (m *ResourceVersionManager) SaveRV(ctx context.Context, x db.ContextExecer, group, resource string, rv int64) error {
_, err := dbutil.Exec(ctx, x, SqlResourceVersionUpdate, SqlResourceVersionUpsertRequest{
SQLTemplate: sqltemplate.New(m.dialect),
Group: group,
Resource: resource,
@@ -1,4 +1,4 @@
package sql
package rvmanager
import (
"testing"
@@ -0,0 +1,30 @@
package rvmanager
import (
"embed"
"fmt"
"text/template"
)
// Templates setup.
var (
//go:embed data/*.sql
sqlTemplatesFS embed.FS
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`))
)
func mustTemplate(filename string) *template.Template {
if t := sqlTemplates.Lookup(filename); t != nil {
return t
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
var (
SqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
SqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
SqlResourceVersionGet = mustTemplate("resource_version_get.sql")
SqlResourceVersionUpdate = mustTemplate("resource_version_update.sql")
SqlResourceVersionInsert = mustTemplate("resource_version_insert.sql")
)
+49 -26
View File
@@ -103,6 +103,7 @@ func namespacedKey(nsPrefix, key string) string {
func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-get"
t.Run("get existing key", func(t *testing.T) {
// First save a key
@@ -221,6 +222,7 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) {
func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-delete"
t.Run("delete existing key", func(t *testing.T) {
// First create a key
@@ -262,6 +264,7 @@ func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) {
func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"})
@@ -360,6 +363,7 @@ func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) {
func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys-with-limits"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1", "c2", "d1", "d2"})
@@ -416,6 +420,7 @@ func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) {
func runTestKVKeysWithSort(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys-with-sort"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"})
@@ -619,29 +624,29 @@ func runTestKVUnixTimestamp(t *testing.T, kv resource.KV, nsPrefix string) {
func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
section := nsPrefix + "-batchget"
nsPrefix += "-batchget"
t.Run("batch get existing keys", func(t *testing.T) {
// Setup test data
testData := map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
namespacedKey(nsPrefix, "key1"): "value1",
namespacedKey(nsPrefix, "key2"): "value2",
namespacedKey(nsPrefix, "key3"): "value3",
}
// Save test data
for key, value := range testData {
saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value))
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader(value))
}
// Batch get all keys
keys := []string{"key1", "key2", "key3"}
keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
type result struct {
key string
value string
}
var results []result
for kv, err := range kv.BatchGet(ctx, section, keys) {
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
value, err := io.ReadAll(kv.Value)
require.NoError(t, err)
@@ -651,10 +656,10 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
}
// Verify results
assert.Len(t, results, 3)
require.Len(t, results, 3)
// Check that all keys are present and in order
expectedKeys := []string{"key1", "key2", "key3"}
expectedKeys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
actualKeys := make([]string, len(results))
for i, r := range results {
actualKeys[i] = r.key
@@ -663,22 +668,40 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
// Verify values
for _, r := range results {
assert.Equal(t, testData[r.key], r.value)
assert.Equal(t, testData[r.key], r.value, "key = %s", r.key)
}
})
t.Run("batch get with empty section", func(t *testing.T) {
var kvs []resource.KeyValue
var errs []error
keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
for kv, err := range kv.BatchGet(ctx, "", keys) {
if err != nil {
errs = append(errs, err)
continue
}
kvs = append(kvs, kv)
}
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "section is required")
assert.Empty(t, kvs)
})
t.Run("batch get with non-existent keys", func(t *testing.T) {
// Setup some test data
saveKVHelper(t, kv, ctx, section, "existing-key", strings.NewReader("existing-value"))
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "existing-key"), strings.NewReader("existing-value"))
// Batch get with mix of existing and non-existent keys
keys := []string{"existing-key", "non-existent-1", "non-existent-2"}
keys := namespacedKeys(nsPrefix, []string{"existing-key", "non-existent-1", "non-existent-2"})
type result struct {
key string
value string
}
var results []result
for kv, err := range kv.BatchGet(ctx, section, keys) {
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
value, err := io.ReadAll(kv.Value)
require.NoError(t, err)
@@ -688,15 +711,15 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
}
// Should only return the existing key
assert.Len(t, results, 1)
assert.Equal(t, "existing-key", results[0].key)
require.Len(t, results, 1)
assert.Equal(t, namespacedKey(nsPrefix, "existing-key"), results[0].key)
assert.Equal(t, "existing-value", results[0].value)
})
t.Run("batch get with all non-existent keys", func(t *testing.T) {
keys := []string{"non-existent-1", "non-existent-2", "non-existent-3"}
keys := namespacedKeys(nsPrefix, []string{"non-existent-1", "non-existent-2", "non-existent-3"})
var results []resource.KeyValue
for kv, err := range kv.BatchGet(ctx, section, keys) {
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
results = append(results, kv)
}
@@ -708,7 +731,7 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
t.Run("batch get with empty keys list", func(t *testing.T) {
keys := []string{}
var results []resource.KeyValue
for kv, err := range kv.BatchGet(ctx, section, keys) {
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
results = append(results, kv)
}
@@ -718,16 +741,16 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
})
t.Run("batch get with empty section", func(t *testing.T) {
keys := []string{"some-key"}
keys := namespacedKeys(nsPrefix, []string{"some-key"})
var errors []error
for kv, err := range kv.BatchGet(ctx, "", keys) {
if err != nil {
errors = append(errors, err)
break
continue
}
_ = kv // unused
}
assert.Len(t, errors, 1)
require.Len(t, errors, 1)
assert.Contains(t, errors[0].Error(), "section is required")
})
@@ -741,13 +764,13 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
// Save test data
for key, value := range testData {
saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value))
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, key), strings.NewReader(value))
}
// Batch get in specific order
keys := []string{"z-key", "a-key", "m-key"}
keys := namespacedKeys(nsPrefix, []string{"z-key", "invalid-key1", "a-key", "invalid-key2", "m-key", "invalid-key3"})
var results []string
for kv, err := range kv.BatchGet(ctx, section, keys) {
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
err = kv.Value.Close()
require.NoError(t, err)
@@ -755,8 +778,8 @@ func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
}
// Verify order is preserved
assert.Len(t, results, 3)
expectedOrder := []string{"z-key", "a-key", "m-key"}
require.Len(t, results, 3)
expectedOrder := namespacedKeys(nsPrefix, []string{"z-key", "a-key", "m-key"})
assert.Equal(t, expectedOrder, results)
})
}
-1
View File
@@ -50,7 +50,6 @@ func TestSQLKV(t *testing.T) {
TestKVSave: true,
TestKVConcurrent: true,
TestKVUnixTimestamp: true,
TestKVBatchGet: true,
TestKVBatchDelete: true,
},
})
@@ -21,8 +21,7 @@ import (
func TestIntegrationTeamSearch(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
// TODO: Add rest.Mode3 and rest.Mode4 when they're supported
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2}
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
for _, mode := range modes {
t.Run(fmt.Sprintf("Team search with dual writer mode %d", mode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
@@ -38,6 +37,7 @@ func TestIntegrationTeamSearch(t *testing.T) {
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagKubernetesAuthnMutation,
},
UnifiedStorageEnableSearch: true,
})
doTeamSearchTests(t, helper)
})
@@ -59,7 +59,6 @@ func doTeamSearchTests(t *testing.T, helper *apis.K8sTestHelper) {
require.NoError(t, err)
require.NotNil(t, team1)
// Create a second team with a different name
team2YAML := helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml")
team2YAML.Object["metadata"].(map[string]interface{})["name"] = "testteam2"
team2YAML.Object["spec"].(map[string]interface{})["title"] = "Another Team"
+7 -9
View File
@@ -146,17 +146,15 @@ async function initRudderstackBackend() {
return;
}
// this will need to be updated when rudderstackSdkV3Url is added
// Desired logic: if only one of the sdk urls is provided, use respective code
// Logic: if only one of the sdk urls is provided, use respective code
// otherwise defer to the feature toggle.
const fakeConfigRudderstackSdkV3Url: string | undefined = undefined;
const hasOldSdkUrl = Boolean(config.rudderstackSdkUrl);
const hasNewSdkUrl = Boolean(fakeConfigRudderstackSdkV3Url);
const onlyOneConfigURLSet = hasOldSdkUrl !== hasNewSdkUrl;
const useNewRudderstack = onlyOneConfigURLSet ? hasNewSdkUrl : config.featureToggles.rudderstackUpgrade;
const hasNewSdkUrl = Boolean(config.rudderstackV3SdkUrl);
const onlyOneSdkUrlSet = hasOldSdkUrl !== hasNewSdkUrl;
const useNewRudderstack = onlyOneSdkUrlSet ? hasNewSdkUrl : config.featureToggles.rudderstackUpgrade;
const configUrl = useNewRudderstack ? fakeConfigRudderstackSdkV3Url : config.rudderstackSdkUrl;
const sdkUrl = useNewRudderstack ? config.rudderstackV3SdkUrl : config.rudderstackSdkUrl;
const modulePromise = useNewRudderstack
? import('./backends/analytics/RudderstackV3Backend')
@@ -168,8 +166,8 @@ async function initRudderstackBackend() {
writeKey: config.rudderstackWriteKey,
dataPlaneUrl: config.rudderstackDataPlaneUrl,
user: contextSrv.user,
sdkUrl: config.rudderstackSdkUrl,
configUrl: configUrl,
sdkUrl,
configUrl: config.rudderstackConfigUrl,
integrationsUrl: config.rudderstackIntegrationsUrl,
buildInfo: config.buildInfo,
})

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