Compare commits

..

2 Commits

Author SHA1 Message Date
Torkel Ödegaard d666c4e2a7 Update 2025-12-18 17:11:01 +01:00
Torkel Ödegaard 5012b4079e Dashboards: Do not show alert rules button for new dashboads 2025-12-18 16:19:33 +01:00
179 changed files with 595 additions and 14289 deletions
@@ -95,7 +95,6 @@ runs:
- 'nx.json'
- 'tsconfig.json'
- '.yarn/**'
- 'apps/dashboard/pkg/migration/**'
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
@@ -300,9 +300,15 @@
"y": 0
},
"id": 6,
"options": {},
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown",
"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"
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -743,9 +743,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -766,9 +764,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -961,12 +961,8 @@
"hide": "dontHide",
"refresh": "onDashboardLoad",
"skipUrlSync": false,
"datasource": {
"type": "",
"uid": "$datasource"
},
"query": {
"kind": "",
"kind": "prometheus",
"spec": {
"__legacyStringValue": "label_values(up, job)"
}
@@ -992,12 +988,8 @@
"hide": "dontHide",
"refresh": "onDashboardLoad",
"skipUrlSync": false,
"datasource": {
"type": "",
"uid": "$datasource"
},
"query": {
"kind": "",
"kind": "prometheus",
"spec": {
"__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)"
}
@@ -978,11 +978,8 @@
"skipUrlSync": false,
"query": {
"kind": "DataQuery",
"group": "",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "$datasource"
},
"spec": {
"__legacyStringValue": "label_values(up, job)"
}
@@ -1010,11 +1007,8 @@
"skipUrlSync": false,
"query": {
"kind": "DataQuery",
"group": "",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "$datasource"
},
"spec": {
"__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)"
}
@@ -115,14 +115,7 @@
"kind": "logs",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "logs",
"originalOptions": {
"height": 100
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -120,14 +120,7 @@
"group": "logs",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "logs",
"originalOptions": {
"height": 100
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -182,20 +182,7 @@
"kind": "table",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"grid": {
"max": 100,
"min": 0
},
"legend": true,
"y2_format": "bytes",
"y_format": "short"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -189,20 +189,7 @@
"group": "table",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"grid": {
"max": 100,
"min": 0
},
"legend": true,
"y2_format": "bytes",
"y_format": "short"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -435,29 +435,7 @@
"kind": "table",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"styles": [
{
"colors": [
"red",
"yellow",
"green"
],
"pattern": "/.*/",
"thresholds": [
"10",
"20"
],
"unit": "short"
}
],
"table": "table2"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -449,29 +449,7 @@
"group": "table",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "table",
"originalOptions": {
"styles": [
{
"colors": [
"red",
"yellow",
"green"
],
"pattern": "/.*/",
"thresholds": [
"10",
"20"
],
"unit": "short"
}
],
"table": "table2"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -110,15 +110,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -115,15 +115,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -361,15 +361,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -372,15 +372,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -167,15 +167,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -174,15 +174,7 @@
"group": "text",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -273,15 +273,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -282,15 +282,7 @@
"group": "text",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -296,7 +296,6 @@
}
},
{
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"datasource": {
"type": "grafana-testdata-datasource"
},
@@ -307,7 +306,15 @@
"y": 0
},
"id": 6,
"mode": "markdown",
"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"
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -1256,13 +1256,13 @@
"spec": {
"pluginVersion": "11.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
}
}
"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"
},
"fieldConfig": {
"defaults": {},
@@ -1301,13 +1301,13 @@
"version": "11.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown"
}
}
"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"
},
"fieldConfig": {
"defaults": {},
@@ -62,12 +62,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -157,12 +151,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -253,12 +241,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -338,12 +320,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -425,12 +401,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -510,12 +480,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -595,12 +559,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -681,12 +639,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -778,12 +730,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -981,12 +927,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1066,12 +1006,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1151,12 +1085,6 @@
"spec": {
"pluginVersion": "7.4.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -67,12 +67,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -165,12 +159,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -264,12 +252,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -352,12 +334,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -442,12 +418,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -530,12 +500,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -618,12 +582,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -707,12 +665,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -807,12 +759,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1015,12 +961,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1103,12 +1043,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -1191,12 +1125,6 @@
"version": "7.4.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "gauge",
"originalOptions": {
"nullPointMode": "null"
}
},
"baseColor": "#299c46",
"reduceOptions": {
"calcs": [
@@ -412,17 +412,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -466,17 +456,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -520,17 +500,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -1711,17 +1681,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2101,17 +2061,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Just verify that the tooltip time has millisecond resolution ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2155,17 +2105,7 @@
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Verify that axis labels look ok",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -429,17 +429,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -485,17 +475,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -541,17 +521,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -1809,17 +1779,7 @@
"group": "text",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2212,17 +2172,7 @@
"group": "text",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Just verify that the tooltip time has millisecond resolution ",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -2268,17 +2218,7 @@
"group": "text",
"version": "",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "text",
"originalOptions": {
"content": "Verify that axis labels look ok",
"editable": true,
"error": false,
"mode": "markdown"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -74,44 +74,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -153,46 +116,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -234,44 +158,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -317,46 +204,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -398,44 +246,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -477,44 +288,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -78,44 +78,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -160,46 +123,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -244,44 +168,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -329,46 +216,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -413,44 +261,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -495,44 +306,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -665,42 +665,7 @@
"kind": "heatmap",
"spec": {
"pluginVersion": "",
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -691,42 +691,7 @@
"group": "heatmap",
"version": "",
"spec": {
"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"
}
}
},
"options": {},
"fieldConfig": {
"defaults": {},
"overrides": []
@@ -56,14 +56,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"panel-tests"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -102,15 +94,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -150,15 +133,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"templating",
"gdev"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -198,15 +172,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"datasource-test"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -246,12 +211,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": []
}
},
"maxItems": 100,
"query": "",
"showHeadings": true,
@@ -288,15 +247,6 @@
"spec": {
"pluginVersion": "9.0.0-pre",
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -58,14 +58,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"panel-tests"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -105,15 +97,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -154,15 +137,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"templating",
"gdev"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -203,15 +177,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"datasource-test"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -252,12 +217,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": []
}
},
"maxItems": 100,
"query": "",
"showHeadings": true,
@@ -295,15 +254,6 @@
"version": "9.0.0-pre",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "dashlist",
"originalOptions": {
"tags": [
"gdev",
"demo"
]
}
},
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -2,7 +2,6 @@ package conversion
import (
"context"
"strings"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apiserver/pkg/endpoints/request"
@@ -80,57 +79,5 @@ 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,10 +1185,6 @@ 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)
}
@@ -1536,10 +1532,6 @@ 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)
}
@@ -1717,10 +1709,6 @@ 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)
}
@@ -2308,24 +2296,20 @@ 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": originalOptions,
"originalOptions": extractAngularOptions(panelMap),
}
}
@@ -290,7 +290,6 @@
}
},
{
"content": "# Graph panel \u003e\u003e Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"datasource": {
"type": "grafana-testdata-datasource"
},
@@ -301,7 +300,15 @@
"y": 0
},
"id": 6,
"mode": "markdown",
"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"
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -654,9 +654,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -679,9 +677,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -737,9 +737,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -760,9 +758,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": false,
"label": "namespace",
@@ -717,9 +717,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": true,
"label": "cluster",
@@ -741,9 +739,7 @@
"text": "prod",
"value": "prod"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"hide": 0,
"includeAll": false,
"label": "namespace",
+1 -7
View File
@@ -23,13 +23,7 @@ func NewSimpleRepositoryTester(validator RepositoryValidator) SimpleRepositoryTe
// TestRepository validates the repository and then runs a health check
func (t *SimpleRepositoryTester) TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) {
// Determine if this is a CREATE or UPDATE operation
// If the repository has been observed by the controller (ObservedGeneration > 0),
// it's an existing repository and we should treat it as UPDATE
cfg := repo.Config()
isCreate := cfg.Status.ObservedGeneration == 0
errors := t.validator.ValidateRepository(repo, isCreate)
errors := t.validator.ValidateRepository(repo)
if len(errors) > 0 {
rsp := &provisioning.TestResults{
Code: http.StatusUnprocessableEntity, // Invalid
@@ -32,9 +32,7 @@ func NewValidator(minSyncInterval time.Duration, allowedTargets []provisioning.S
}
// ValidateRepository solely does configuration checks on the repository object. It does not run a health check or compare against existing repositories.
// isCreate indicates whether this is a CREATE operation (true) or UPDATE operation (false).
// When isCreate is false, allowedTargets validation is skipped to allow existing repositories to continue working.
func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool) field.ErrorList {
func (v *RepositoryValidator) ValidateRepository(repo Repository) field.ErrorList {
list := repo.Validate()
cfg := repo.Config()
@@ -46,7 +44,7 @@ func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool)
if cfg.Spec.Sync.Target == "" {
list = append(list, field.Required(field.NewPath("spec", "sync", "target"),
"The target type is required when sync is enabled"))
} else if isCreate && !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) {
} else if !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) {
list = append(list,
field.Invalid(
field.NewPath("spec", "target"),
@@ -303,8 +303,7 @@ func TestValidateRepository(t *testing.T) {
validator := NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder}, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Tests validate new configurations, so always pass isCreate=true
errors := validator.ValidateRepository(tt.repository, true)
errors := validator.ValidateRepository(tt.repository)
require.Len(t, errors, tt.expectedErrs)
if tt.validateError != nil {
tt.validateError(t, errors)
+1 -4
View File
@@ -335,9 +335,6 @@ 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 =
@@ -2264,7 +2261,7 @@ fail_tests_on_console = true
# List of targets that can be controlled by a repository, separated by |.
# Instance means the whole grafana instance will be controlled by a repository.
# Folder limits it to a folder within the grafana instance.
allowed_targets = folder
allowed_targets = instance|folder
# Whether image rendering is allowed for dashboard previews.
# Requires image rendering service to be configured.
-3
View File
@@ -322,9 +322,6 @@
# 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,9 +299,15 @@
"y": 0
},
"id": 6,
"options": {},
"content": "# Graph panel >> Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions",
"mode": "markdown",
"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"
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
@@ -54,6 +54,18 @@ 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.
@@ -62,12 +74,6 @@ 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,12 +642,6 @@ 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,6 +31,7 @@ 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.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
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) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
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.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
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) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
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,9 +71,8 @@ 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.
- **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.
- **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.
- **+ Add query** - Add an additional query.
- **+ Add from saved queries** - Add an additional query by reusing a saved query.
@@ -88,9 +88,8 @@ 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.
- **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.
- **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.
- **+ 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 open the **Saved queries** drop-down menu and click **Replace query** in the query editor:
To access your saved queries, click **+ Add from saved queries** or **Replace with saved 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 query** in the **Saved queries** drop-down menu updates your existing query.
Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** 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, open the **Saved queries** drop-down menu and click the **Save query** option:
1. From the query editor, click the **Save query** icon:
{{< 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.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a saved query.
- Click **Replace with saved 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** option in the **Saved queries** drop-down menu.
1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** button (or icon).
1. (Optional) Click **+ Add query** or **Add from saved queries** to add more queries as needed.
1. Click **Run queries**.
+6 -1
View File
@@ -1888,6 +1888,11 @@
"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
@@ -4334,7 +4339,7 @@
},
"public/app/plugins/panel/heatmap/utils.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 14
"count": 16
}
},
"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.52.0",
"@grafana/scenes-react": "6.52.0",
"@grafana/scenes": "^6.51.0",
"@grafana/scenes-react": "^6.51.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -289,7 +289,6 @@ export interface GrafanaConfig {
rudderstackWriteKey: string;
rudderstackDataPlaneUrl: string;
rudderstackSdkUrl: string;
rudderstackV3SdkUrl: string;
rudderstackConfigUrl: string;
rudderstackIntegrationsUrl: string;
applicationInsightsConnectionString: string;
+5 -9
View File
@@ -126,6 +126,11 @@ 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;
@@ -494,10 +499,6 @@ 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
*/
@@ -1164,11 +1165,6 @@ 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
*/
-1
View File
@@ -224,7 +224,6 @@ export class GrafanaBootConfig {
rudderstackWriteKey?: string;
rudderstackDataPlaneUrl?: string;
rudderstackSdkUrl?: string;
rudderstackV3SdkUrl?: string;
rudderstackConfigUrl?: string;
rudderstackIntegrationsUrl?: string;
analyticsConsoleReporting = false;
@@ -1,460 +0,0 @@
export interface DashboardSceneJsonApiV2 {
/**
* Read the currently open dashboard as v2beta1 Dashboard kind JSON (JSON string).
*/
getCurrentDashboard(space?: number): string;
/**
* Read query errors for the currently open dashboard (JSON string).
*
* This returns a JSON array of objects shaped like:
* `{ panelId, panelTitle, refId?, datasource?, message, severity }`.
*/
getCurrentDashboardErrors(space?: number): string;
/**
* Read current dashboard variables (JSON string).
*
* This returns JSON shaped like:
* `{ variables: [{ name, value }] }`
* where `value` is `string | string[]`.
*/
getCurrentDashboardVariables(space?: number): string;
/**
* Apply dashboard variable values (JSON string).
*
* Accepts either:
* - `{ variables: [{ name, value }] }`
* - or a map `{ [name]: value }`
*
* where `value` is `string | string[]`.
*/
applyCurrentDashboardVariables(varsJson: string): void;
/**
* Read the current dashboard time range (JSON string).
*
* This returns JSON shaped like:
* `{ from, to, timezone? }`.
*/
getCurrentDashboardTimeRange(space?: number): string;
/**
* Apply the current dashboard time range (JSON string).
*
* Accepts JSON shaped like:
* `{ from, to, timezone? }` where `from/to` are Grafana raw strings (e.g. `now-6h`, `now`).
*/
applyCurrentDashboardTimeRange(timeRangeJson: string): void;
/**
* Select a tab within the current dashboard (JSON string).
*
* Accepts JSON shaped like:
* `{ title?: string, slug?: string }`.
*/
selectCurrentDashboardTab(tabJson: string): void;
/**
* Read current in-dashboard navigation state (JSON string).
*
* This returns JSON shaped like:
* `{ tab: { slug: string, title?: string } | null }`.
*/
getCurrentDashboardNavigation(space?: number): string;
/**
* Read the currently selected element (edit pane selection) (JSON string).
*
* This returns JSON shaped like:
* `{ isEditing: boolean, selection: null | { mode: "single" | "multi", item?: object, items?: object[] } }`.
*
* Note: selection is only meaningful in edit mode. Implementations should return `selection: null` when not editing.
*/
getCurrentDashboardSelection(space?: number): string;
/**
* Scroll/focus a row within the current dashboard (JSON string).
*
* Accepts JSON shaped like:
* `{ title?: string, rowKey?: string }`.
*/
focusCurrentDashboardRow(rowJson: string): void;
/**
* Scroll/focus a panel within the current dashboard (JSON string).
*
* Accepts JSON shaped like:
* `{ panelId: number }`.
*/
focusCurrentDashboardPanel(panelJson: string): void;
/**
* Apply a v2beta1 Dashboard kind JSON (JSON string).
*
* Implementations must enforce **spec-only** updates by rejecting any changes to
* `apiVersion`, `kind`, `metadata`, or `status`.
*/
applyCurrentDashboard(resourceJson: string): void;
/**
* Preview a set of in-place dashboard operations (JSON string).
*
* Returns JSON shaped like:
* `{ ok: boolean, applied?: number, unsupported?: number, errors?: string[] }`
*
* @internal Experimental: this API is intended for automation and may evolve.
*/
previewCurrentDashboardOps(opsJson: string): string;
/**
* Apply a set of in-place dashboard operations (JSON string).
*
* Implementations should update the currently open DashboardScene in place when possible,
* to avoid unnecessary panel/query reloads.
*
* Returns JSON shaped like:
* `{ ok: boolean, applied?: number, didRebuild?: boolean, errors?: string[] }`
*
* @internal Experimental: this API is intended for automation and may evolve.
*/
applyCurrentDashboardOps(opsJson: string): string;
}
let singletonInstance: DashboardSceneJsonApiV2 | undefined;
/**
* Used during startup by Grafana to register the implementation.
*
* @internal
*/
export function setDashboardSceneJsonApiV2(instance: DashboardSceneJsonApiV2) {
singletonInstance = instance;
}
/**
* Returns the registered DashboardScene JSON API.
*
* @public
*/
export function getDashboardSceneJsonApiV2(): DashboardSceneJsonApiV2 {
if (!singletonInstance) {
throw new Error('DashboardScene JSON API is not available');
}
return singletonInstance;
}
/**
* A grouped, ergonomic API wrapper around the DashboardScene JSON API.
*
* This is purely a convenience layer: it calls the same underlying registered implementation
* as the top-level helper functions, but organizes functionality into namespaces like
* `navigation`, `variables`, and `timeRange`.
*
* @public
*/
export function getDashboardApi() {
const api = getDashboardSceneJsonApiV2();
let cachedDashboardSchemaBundle: { bundle: unknown; loadedAt: number } | undefined;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function collectRefs(value: unknown, out: Set<string>) {
if (Array.isArray(value)) {
for (const v of value) {
collectRefs(v, out);
}
return;
}
if (!isRecord(value)) {
return;
}
for (const [k, v] of Object.entries(value)) {
if (k === '$ref' && typeof v === 'string') {
out.add(v);
} else {
collectRefs(v, out);
}
}
}
function buildOpenApiSchemaBundle(openapi: unknown) {
if (!isRecord(openapi)) {
throw new Error('OpenAPI document is not an object');
}
const info = openapi['info'];
if (!isRecord(info)) {
throw new Error('OpenAPI document is missing info');
}
const title = info['title'];
if (typeof title !== 'string' || title.length === 0) {
throw new Error('OpenAPI document is missing info.title');
}
// This endpoint is expected to return the group/version doc, so validate it explicitly.
if (title !== 'dashboard.grafana.app/v2beta1') {
throw new Error(`OpenAPI document is not dashboard.grafana.app/v2beta1 (info.title="${title}")`);
}
const components = openapi['components'];
if (!isRecord(components)) {
throw new Error('OpenAPI document is missing components');
}
const schemas = components['schemas'];
if (!isRecord(schemas)) {
throw new Error('OpenAPI document is missing components.schemas');
}
// Find the Dashboard kind schema key by GVK annotation.
const dashboardKey = Object.entries(schemas).find(([_, schema]) => {
if (!isRecord(schema)) {
return false;
}
const gvk = schema['x-kubernetes-group-version-kind'];
if (!Array.isArray(gvk)) {
return false;
}
return gvk.some((x) => {
return (
isRecord(x) &&
x['group'] === 'dashboard.grafana.app' &&
x['version'] === 'v2beta1' &&
x['kind'] === 'Dashboard'
);
});
})?.[0];
if (!dashboardKey) {
throw new Error('Could not find dashboard.grafana.app/v2beta1 Dashboard schema in OpenAPI document');
}
const rootRef = `#/components/schemas/${dashboardKey}`;
const pickedSchemas: Record<string, unknown> = {};
const visited = new Set<string>();
const queue: string[] = [rootRef];
while (queue.length) {
const ref = queue.shift()!;
if (!ref.startsWith('#/components/schemas/')) {
continue;
}
const key = ref.slice('#/components/schemas/'.length);
if (visited.has(key)) {
continue;
}
visited.add(key);
const schema = schemas[key];
if (!schema) {
continue;
}
pickedSchemas[key] = schema;
const refs = new Set<string>();
collectRefs(schema, refs);
for (const r of refs) {
if (r.startsWith('#/components/schemas/')) {
queue.push(r);
}
}
}
// Sanity-check the root schema shape (helps LLM consumers, and catches wrong schema sources quickly).
const rootSchema = pickedSchemas[dashboardKey];
if (!isRecord(rootSchema)) {
throw new Error('Dashboard schema is not an object');
}
const required = rootSchema['required'];
if (!Array.isArray(required)) {
throw new Error('Dashboard schema is missing required fields list');
}
for (const req of ['apiVersion', 'kind', 'metadata', 'spec']) {
if (!required.includes(req)) {
throw new Error(`Dashboard schema is missing required field "${req}"`);
}
}
return {
format: 'openapi3.schemaBundle',
source: {
url: '/openapi/v3/apis/dashboard.grafana.app/v2beta1',
},
group: 'dashboard.grafana.app',
version: 'v2beta1',
kind: 'Dashboard',
root: { $ref: rootRef },
stats: { schemas: Object.keys(pickedSchemas).length },
validation: {
ok: true,
info: {
title,
},
root: {
ref: rootRef,
required: ['apiVersion', 'kind', 'metadata', 'spec'],
},
},
components: {
schemas: pickedSchemas,
},
};
}
return {
/**
* Prints/returns a quick reference for the grouped dashboard API, including expected JSON shapes.
*/
help: () => {
const text = [
'Dashboard API (DashboardScene JSON API, schema v2 kinds)',
'',
'All inputs/outputs are JSON strings.',
'Edits are spec-only: apiVersion/kind/metadata/status must not change.',
'',
'Schema (for LLMs):',
'- schema.getSources(space?): string',
'- schema.getDashboard(space?): Promise<string>',
'- schema.getDashboardSync(space?): string',
' - getDashboard() fetches the OpenAPI v3 document for dashboard.grafana.app/v2beta1 and returns a schema bundle.',
' - getDashboard() validates the document is for dashboard.grafana.app/v2beta1 and that the root schema is the Dashboard kind.',
'',
'Read/apply dashboard:',
'- dashboard.getCurrent(space?): string',
'- dashboard.apply(resourceJson: string): void',
'- dashboard.previewOps(opsJson: string): string',
'- dashboard.applyOps(opsJson: string): string',
' - Ops are applied in-place when possible to avoid full dashboard reloads.',
' - Supported ops (JSON array elements):',
' - { op: "mergePanelConfig", panelId, merge: { vizConfig: { fieldConfig: { defaults: {...} }, options: {...} } } }',
' - { op: "setPanelTitle", panelId, title }',
' - { op: "setGridPos", panelId, x, y, w, h }',
' - { op: "addPanel", title?, pluginId?, rowTitle?, rowKey? }',
' - { op: "removePanel", panelId }',
' - { op: "addRow", title? }',
' - { op: "removeRow", title? | rowKey? }',
' - { op: "movePanelToRow", panelId, rowKey? | rowTitle? }',
' - { op: "movePanelToTab", panelId, tabTitle? | tabSlug? }',
' - { op: "addTab", title? }',
' - { op: "removeTab", title? | slug? }',
' - resourceJson must be a v2beta1 Dashboard kind object:',
' { apiVersion: "dashboard.grafana.app/v2beta1", kind: "Dashboard", metadata: {...}, spec: {...}, status: {...} }',
'',
'Errors:',
'- errors.getCurrent(space?): string',
' - returns JSON: { errors: [{ panelId, panelTitle, refId?, datasource?, message, severity }] }',
'',
'Variables:',
'- variables.getCurrent(space?): string',
' - returns JSON: { variables: [{ name, value }] } where value is string | string[]',
'- variables.apply(varsJson: string): void',
' - accepts JSON: { variables: [{ name, value }] } OR { [name]: value }',
'',
'Time range:',
'- timeRange.getCurrent(space?): string',
' - returns JSON: { from: string, to: string, timezone?: string }',
'- timeRange.apply(timeRangeJson: string): void',
' - accepts JSON: { from: "now-6h", to: "now", timezone?: "browser" | "utc" | ... }',
'',
'Navigation:',
'- navigation.getCurrent(space?): string',
' - returns JSON: { tab: { slug: string, title?: string } | null }',
'- navigation.getSelection(space?): string',
' - returns JSON: { isEditing: boolean, selection: null | { mode: "single" | "multi", item?: object, items?: object[] } }',
'- navigation.selectTab(tabJson: string): void',
' - accepts JSON: { title?: string, slug?: string }',
'- navigation.focusRow(rowJson: string): void',
' - accepts JSON: { title?: string, rowKey?: string }',
'- navigation.focusPanel(panelJson: string): void',
' - accepts JSON: { panelId: number }',
'',
'Examples:',
'- const schema = JSON.parse(await window.dashboardApi.schema.getDashboard(0))',
'- window.dashboardApi.timeRange.apply(JSON.stringify({ from: "now-6h", to: "now", timezone: "browser" }))',
'- window.dashboardApi.navigation.selectTab(JSON.stringify({ title: "Overview" }))',
'- window.dashboardApi.navigation.focusPanel(JSON.stringify({ panelId: 12 }))',
].join('\n');
// Calling help is an explicit action; logging is useful in the browser console.
// Return the text as well so callers can print/store it as they prefer.
try {
// eslint-disable-next-line no-console
console.log(text);
} catch {
// ignore
}
return text;
},
schema: {
/**
* Returns where this API loads schema documents from.
*/
getSources: (space = 2) => {
return JSON.stringify(
{
openapi3: {
url: '/openapi/v3/apis/dashboard.grafana.app/v2beta1',
note: 'This is the Kubernetes-style OpenAPI document for dashboard.grafana.app/v2beta1. `schema.getDashboard()` extracts the Dashboard schemas into a smaller bundle.',
},
},
null,
space
);
},
/**
* Fetches and returns an OpenAPI schema bundle for `dashboard.grafana.app/v2beta1` `Dashboard`.
*
* Returns a JSON string (async) shaped like:
* `{ format, source, group, version, kind, root, stats, components: { schemas } }`.
*/
getDashboard: async (space = 2) => {
if (!cachedDashboardSchemaBundle) {
const rsp = await fetch('/openapi/v3/apis/dashboard.grafana.app/v2beta1', { credentials: 'same-origin' });
if (!rsp.ok) {
throw new Error(
`Failed to fetch OpenAPI document from /openapi/v3/apis/dashboard.grafana.app/v2beta1 (status ${rsp.status})`
);
}
const openapi: unknown = await rsp.json();
cachedDashboardSchemaBundle = { bundle: buildOpenApiSchemaBundle(openapi), loadedAt: Date.now() };
}
return JSON.stringify(cachedDashboardSchemaBundle.bundle, null, space);
},
/**
* Returns the cached schema bundle (sync), if previously loaded by `schema.getDashboard()`.
*/
getDashboardSync: (space = 2) => {
if (!cachedDashboardSchemaBundle) {
throw new Error('Schema bundle is not loaded. Call `await dashboardApi.schema.getDashboard()` first.');
}
return JSON.stringify(cachedDashboardSchemaBundle.bundle, null, space);
},
},
dashboard: {
getCurrent: (space = 2) => api.getCurrentDashboard(space),
apply: (resourceJson: string) => api.applyCurrentDashboard(resourceJson),
previewOps: (opsJson: string) => api.previewCurrentDashboardOps(opsJson),
applyOps: (opsJson: string) => api.applyCurrentDashboardOps(opsJson),
},
errors: {
getCurrent: (space = 2) =>
JSON.stringify({ errors: JSON.parse(api.getCurrentDashboardErrors(space)) }, null, space),
},
variables: {
getCurrent: (space = 2) => api.getCurrentDashboardVariables(space),
apply: (varsJson: string) => api.applyCurrentDashboardVariables(varsJson),
},
timeRange: {
getCurrent: (space = 2) => api.getCurrentDashboardTimeRange(space),
apply: (timeRangeJson: string) => api.applyCurrentDashboardTimeRange(timeRangeJson),
},
navigation: {
getCurrent: (space = 2) => api.getCurrentDashboardNavigation(space),
getSelection: (space = 2) => api.getCurrentDashboardSelection(space),
selectTab: (tabJson: string) => api.selectCurrentDashboardTab(tabJson),
focusRow: (rowJson: string) => api.focusCurrentDashboardRow(rowJson),
focusPanel: (panelJson: string) => api.focusCurrentDashboardPanel(panelJson),
},
};
}
@@ -6,7 +6,6 @@ export * from './templateSrv';
export * from './live';
export * from './LocationService';
export * from './appEvents';
export * from './dashboardSceneJsonApi';
export {
setPluginComponentHook,
@@ -185,10 +185,6 @@ 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,7 +200,6 @@ 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,7 +229,6 @@ 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,
+8 -23
View File
@@ -54,8 +54,7 @@ func init() {
}
logger := level.NewFilter(format(os.Stderr), level.AllowInfo())
root = newManager(logger)
// Use default Info level during package initialization before config is loaded
initAppSDKLogger(logger, slog.LevelInfo)
initAppSDKLogger(logger)
RegisterContextualLogProvider(func(ctx context.Context) ([]any, bool) {
pFromCtx := ctx.Value(logParamsContextKey{})
@@ -81,7 +80,7 @@ func newManager(logger gokitlog.Logger) *logManager {
}
}
func (lm *logManager) initialize(loggers []logWithFilters, levelStr string) {
func (lm *logManager) initialize(loggers []logWithFilters) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
@@ -114,7 +113,7 @@ func (lm *logManager) initialize(loggers []logWithFilters, levelStr string) {
lm.loggersByName[name].Swap(&compositeLogger{loggers: ctxLoggers})
}
initAppSDKLogger(lm.ConcreteLogger, stringToSlogLevel(levelStr))
initAppSDKLogger(lm.ConcreteLogger)
}
func (lm *logManager) New(ctx ...any) *ConcreteLogger {
@@ -515,7 +514,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
configLoggers = append(configLoggers, handler)
}
if len(configLoggers) > 0 {
root.initialize(configLoggers, defaultLevelName)
root.initialize(configLoggers)
}
return nil
@@ -552,22 +551,8 @@ func SetupConsoleLogger(level string) error {
return nil
}
// 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))
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))
}
+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)
+4 -1
View File
@@ -64,7 +64,10 @@ 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()))
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
//nolint:staticcheck // not yet migrated to OpenFeature
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) {
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
}
rw := web.Rw(w, r)
next.ServeHTTP(rw, r)
+2 -5
View File
@@ -6,7 +6,6 @@ import (
"log/slog"
"math"
"strconv"
"strings"
"google.golang.org/grpc"
@@ -103,8 +102,7 @@ func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition {
columns := getDefaultColumns()
for _, field := range fields {
fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX)
if col, ok := builders.TeamSearchTableColumnDefinitions[fieldName]; ok {
if col, ok := builders.TeamSearchTableColumnDefinitions[field]; ok {
columns = append(columns, col)
}
}
@@ -123,8 +121,7 @@ func getDefaultColumns() []*resourcepb.ResourceTableColumnDefinition {
func createCells(t *team.TeamDTO, fields []string) [][]byte {
cells := createDefaultCells(t)
for _, field := range fields {
fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX)
switch fieldName {
switch field {
case builders.TEAM_SEARCH_EMAIL:
cells = append(cells, []byte(t.Email))
case builders.TEAM_SEARCH_PROVISIONED:
+4 -19
View File
@@ -2,7 +2,6 @@ package iam
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
@@ -13,7 +12,6 @@ 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"
@@ -139,12 +137,6 @@ 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
@@ -162,23 +154,16 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request)
}
searchRequest := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: iamv0alpha1.TeamResourceInfo.GroupResource().Group,
Resource: iamv0alpha1.TeamResourceInfo.GroupResource().Resource,
Namespace: requester.GetNamespace(),
},
},
Options: &resourcepb.ListOptions{},
Query: queryParams.Get("query"),
Limit: int64(limit),
Offset: int64(offset),
Page: int64(page),
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
Fields: []string{
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,
builders.TEAM_SEARCH_EMAIL,
builders.TEAM_SEARCH_PROVISIONED,
builders.TEAM_SEARCH_EXTERNAL_UID,
},
}
+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{"title", "fields.email", "fields.provisioned", "fields.externalUID"}
expectedFields := []string{"email", "provisioned", "externalUID"}
if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) {
t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields)
}
+1 -2
View File
@@ -673,8 +673,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
//
// the only time to add configuration checks here is if you need to compare
// the incoming change to the current configuration
isCreate := a.GetOperation() == admission.Create
list := b.validator.ValidateRepository(repo, isCreate)
list := b.validator.ValidateRepository(repo)
cfg := repo.Config()
if a.GetOperation() == admission.Update {
@@ -9,13 +9,10 @@ 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"
@@ -75,15 +72,6 @@ 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 -2
View File
@@ -131,8 +131,7 @@ func (s *ExtendedJWT) authenticateAsUser(
return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err)
}
// TODO: How to support other identity types like render and anonymous here?
if !claims.IsIdentityType(t, claims.TypeUser, claims.TypeServiceAccount) {
if !claims.IsIdentityType(t, claims.TypeUser) {
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject)
}
+1 -35
View File
@@ -53,17 +53,6 @@ var (
Namespace: "default", // org ID of 1 is special and translates to default
},
}
validIDTokenClaimsWithServiceAccount = idTokenClaims{
Claims: jwt.Claims{
Subject: "service-account:3",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "default", // org ID of 1 is special and translates to default
},
}
validIDTokenClaimsWithStackSet = idTokenClaims{
Claims: jwt.Claims{
Subject: "user:2",
@@ -129,7 +118,7 @@ var (
}
invalidSubjectIDTokenClaims = idTokenClaims{
Claims: jwt.Claims{
Subject: "anonymous:2",
Subject: "service-account:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
@@ -297,29 +286,6 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
},
},
},
{
name: "should authenticate as service account",
accessToken: &validAccessTokenClaims,
idToken: &validIDTokenClaimsWithServiceAccount,
orgID: 1,
want: &authn.Identity{
ID: "3",
Type: claims.TypeServiceAccount,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaims,
IDTokenClaims: &validIDTokenClaimsWithServiceAccount,
Namespace: "default",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
FetchPermissionsParams: authn.FetchPermissionsParams{
RestrictedActions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
},
},
},
},
{
name: "should authenticate as user in the user namespace",
accessToken: &validAccessTokenClaimsWildcard,
+3 -1
View File
@@ -23,7 +23,9 @@ 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 and/or removed entirely.
// 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
IsEnabledGlobally(flag string) bool
// Get the enabled flags -- this *may* also include disabled flags (with value false)
+7 -15
View File
@@ -185,6 +185,13 @@ 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",
@@ -813,13 +820,6 @@ 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,14 +1921,6 @@ 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;",
+1 -2
View File
@@ -24,6 +24,7 @@ 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
@@ -112,7 +113,6 @@ 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,7 +261,6 @@ 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
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
27 unifiedRequestLog GA @grafana/grafana-backend-group false false false
28 renderAuthJWT preview @grafana/grafana-operator-experience-squad false false false
29 refactorVariablesTimeRange preview @grafana/dashboards-squad false false false
30 faroDatasourceSelector preview @grafana/app-o11y false false true
113 oauthRequireSubClaim experimental @grafana/identity-access-team false false false
114 refreshTokenRequired experimental @grafana/identity-access-team false false false
115 newDashboardWithFiltersAndGroupBy experimental @grafana/dashboards-squad false false false
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
heatmapRowsAxisOptions experimental @grafana/dataviz-squad false false true
264 preventPanelChromeOverflow preview @grafana/grafana-frontend-platform false false true
265 jaegerEnableGrpcEndpoint experimental @grafana/oss-big-tent false false false
266 pluginStoreServiceLoading experimental @grafana/plugins-platform-backend false false false
+4 -4
View File
@@ -79,6 +79,10 @@ 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"
@@ -335,10 +339,6 @@ 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"
+1 -29
View File
@@ -922,19 +922,6 @@
"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",
@@ -1647,20 +1634,6 @@
"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",
@@ -3529,8 +3502,7 @@
"metadata": {
"name": "unifiedRequestLog",
"resourceVersion": "1764664939750",
"creationTimestamp": "2023-03-31T13:38:09Z",
"deletionTimestamp": "2025-12-18T14:21:02Z"
"creationTimestamp": "2023-03-31T13:38:09Z"
},
"spec": {
"description": "Writes error logs to the request logger",
@@ -32,7 +32,6 @@ 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,7 +94,6 @@ 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,
@@ -2169,57 +2169,6 @@ func TestRouteGetRuleStatuses(t *testing.T) {
})
})
t.Run("compact mode with receiver_name filter returns only matching rules", func(t *testing.T) {
fakeStore, _, api := setupAPI(t)
ruleA := gen.With(
gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
NamespaceUID: "folder-1",
RuleGroup: "group-1",
OrgID: orgID,
}),
gen.WithNotificationSettings(
ngmodels.NotificationSettings{
Receiver: "receiver-a",
GroupBy: []string{"alertname"},
},
),
).GenerateRef()
fakeStore.PutRule(context.Background(), ruleA)
ruleB := gen.With(
gen.WithGroupKey(ngmodels.AlertRuleGroupKey{
NamespaceUID: "folder-2",
RuleGroup: "group-2",
OrgID: orgID,
}),
gen.WithNotificationSettings(
ngmodels.NotificationSettings{
Receiver: "receiver-b",
GroupBy: []string{"alertname"},
},
),
).GenerateRef()
fakeStore.PutRule(context.Background(), ruleB)
r, err := http.NewRequest("GET", "/api/v1/rules?compact=true&receiver_name=receiver-a", nil)
require.NoError(t, err)
c := &contextmodel.ReqContext{
Context: &web.Context{Req: r},
SignedInUser: &user.SignedInUser{
OrgID: orgID,
Permissions: queryPermissions,
},
}
resp := api.RouteGetRuleStatuses(c)
require.Equal(t, http.StatusOK, resp.Status())
var res apimodels.RuleResponse
require.NoError(t, json.Unmarshal(resp.Body(), &res))
require.Len(t, res.Data.RuleGroups, 1)
require.Equal(t, "group-1", res.Data.RuleGroups[0].Name)
require.Empty(t, res.Data.RuleGroups[0].Rules[0].Query, "Query should be empty in compact mode")
})
t.Run("provenance as expected", func(t *testing.T) {
fakeStore, fakeAIM, api, provStore := setupAPIFull(t)
// Rule without provenance
+6 -18
View File
@@ -642,23 +642,6 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
_ = rows.Close()
}()
opts := AlertRuleConvertOptions{}
if query.Compact {
opts.ExcludeAlertQueries = true
opts.ExcludeNotificationSettings = true
opts.ExcludeMetadata = true
if query.ReceiverName != "" || query.TimeIntervalName != "" {
// Need NotificationSettings for these filters
opts.ExcludeNotificationSettings = false
}
if query.HasPrometheusRuleDefinition != nil {
// Need Metadata for this filter
opts.ExcludeMetadata = false
}
}
// Process rules and implement per-group pagination
var groupsFetched int64
var rulesFetched int64
@@ -670,7 +653,12 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
continue
}
converted, err := convertAlertRuleToModel(*rule, st.Logger, opts)
var converted ngmodels.AlertRule
if query.Compact {
converted, err = alertRuleToModelsAlertRuleCompact(*rule, st.Logger)
} else {
converted, err = alertRuleToModelsAlertRule(*rule, st.Logger)
}
if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRulesByGroup", "error", err)
+12 -13
View File
@@ -15,23 +15,22 @@ type compactQuery struct {
DatasourceUID string `json:"datasourceUid"`
}
// AlertRuleConvertOptions controls which fields to parse during conversion from alertRule to models.AlertRule.
// By default all fields are included. Set Exclude* to true to skip parsing expensive fields.
type AlertRuleConvertOptions struct {
ExcludeAlertQueries bool // Only parse datasource UIDs from queries
ExcludeNotificationSettings bool
ExcludeMetadata bool
func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) {
return convertAlertRuleToModel(ar, l, false)
}
func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) {
return convertAlertRuleToModel(ar, l, AlertRuleConvertOptions{})
// alertRuleToModelsAlertRuleCompact transforms an alertRule to a models.AlertRule
// ignoring alert queries (except for data source UIDs), notification settings, and metadata.
func alertRuleToModelsAlertRuleCompact(ar alertRule, l log.Logger) (models.AlertRule, error) {
return convertAlertRuleToModel(ar, l, true)
}
// convertAlertRuleToModel creates a models.AlertRule from an alertRule.
// opts.Exclude* fields control which expensive fields to skip parsing, reducing JSON serializations.
func convertAlertRuleToModel(ar alertRule, l log.Logger, opts AlertRuleConvertOptions) (models.AlertRule, error) {
// When 'compact' is set to 'true', it skips parsing the alert queries (except for the data source UID), notification
// settings, and metadata, thus reducing the number of JSON serializations needed.
func convertAlertRuleToModel(ar alertRule, l log.Logger, compact bool) (models.AlertRule, error) {
var data []models.AlertQuery
if opts.ExcludeAlertQueries {
if compact {
var cqs []compactQuery
if err := json.Unmarshal([]byte(ar.Data), &cqs); err != nil {
return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err)
@@ -119,7 +118,7 @@ func convertAlertRuleToModel(ar alertRule, l log.Logger, opts AlertRuleConvertOp
}
}
if !opts.ExcludeNotificationSettings && ar.NotificationSettings != "" {
if !compact && ar.NotificationSettings != "" {
ns, err := parseNotificationSettings(ar.NotificationSettings)
if err != nil {
return models.AlertRule{}, fmt.Errorf("failed to parse notification settings: %w", err)
@@ -127,7 +126,7 @@ func convertAlertRuleToModel(ar alertRule, l log.Logger, opts AlertRuleConvertOp
result.NotificationSettings = ns
}
if !opts.ExcludeMetadata && ar.Metadata != "" {
if !compact && ar.Metadata != "" {
err = json.Unmarshal([]byte(ar.Metadata), &result.Metadata)
if err != nil {
return models.AlertRule{}, fmt.Errorf("failed to metadata: %w", err)
+1 -81
View File
@@ -84,11 +84,7 @@ func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) {
Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`,
}
compactResult, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{
ExcludeAlertQueries: true,
ExcludeNotificationSettings: true,
ExcludeMetadata: true,
})
compactResult, err := alertRuleToModelsAlertRuleCompact(rule, &logtest.Fake{})
require.NoError(t, err)
// Should have datasource UIDs.
@@ -146,82 +142,6 @@ func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) {
// Should have metadata (metadata is parsed from JSON to struct).
require.NotEqual(t, ngmodels.AlertRuleMetadata{}, fullResult.Metadata)
})
t.Run("compact mode with notification settings included for filtering", func(t *testing.T) {
rule := alertRule{
ID: 1,
OrgID: 1,
UID: "test-uid",
Title: "Test Rule",
Condition: "A",
Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}}]`,
IntervalSeconds: 60,
Version: 1,
NamespaceUID: "ns-uid",
RuleGroup: "test-group",
NoDataState: "NoData",
ExecErrState: "Error",
NotificationSettings: `[{"receiver":"test-receiver"}]`,
Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`,
}
result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{
ExcludeAlertQueries: true,
ExcludeNotificationSettings: false,
ExcludeMetadata: true,
})
require.NoError(t, err)
// Should have compact query data (only datasource UIDs).
require.Len(t, result.Data, 1)
require.Equal(t, "ds1", result.Data[0].DatasourceUID)
require.Empty(t, result.Data[0].RefID)
// Should have notification settings for filtering.
require.Len(t, result.NotificationSettings, 1)
require.Equal(t, "test-receiver", result.NotificationSettings[0].Receiver)
// Should not have metadata.
require.Equal(t, ngmodels.AlertRuleMetadata{}, result.Metadata)
})
t.Run("compact mode with metadata included for filtering", func(t *testing.T) {
rule := alertRule{
ID: 1,
OrgID: 1,
UID: "test-uid",
Title: "Test Rule",
Condition: "A",
Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}}]`,
IntervalSeconds: 60,
Version: 1,
NamespaceUID: "ns-uid",
RuleGroup: "test-group",
NoDataState: "NoData",
ExecErrState: "Error",
NotificationSettings: `[{"receiver":"test-receiver"}]`,
Metadata: `{"prometheus_style_rule":{"original_rule_definition":"alert: TestAlert\n expr: rate(metric[5m]) > 1"}}`,
}
result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{
ExcludeAlertQueries: true,
ExcludeNotificationSettings: true,
ExcludeMetadata: false,
})
require.NoError(t, err)
// Should have compact query data (only datasource UIDs).
require.Len(t, result.Data, 1)
require.Equal(t, "ds1", result.Data[0].DatasourceUID)
require.Empty(t, result.Data[0].RefID)
// Should not have notification settings.
require.Empty(t, result.NotificationSettings)
// Should have metadata for filtering.
require.NotEqual(t, ngmodels.AlertRuleMetadata{}, result.Metadata)
require.True(t, result.HasPrometheusRuleDefinition())
})
}
func TestAlertRuleVersionToAlertRule(t *testing.T) {
+1 -3
View File
@@ -414,7 +414,6 @@ type Cfg struct {
RudderstackDataPlaneURL string
RudderstackWriteKey string
RudderstackSDKURL string
RudderstackV3SDKURL string
RudderstackConfigURL string
RudderstackIntegrationsURL string
IntercomSecret string
@@ -1282,7 +1281,6 @@ 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()
@@ -2167,7 +2165,7 @@ func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error {
}
cfg.ProvisioningAllowedTargets = iniFile.Section("provisioning").Key("allowed_targets").Strings("|")
if len(cfg.ProvisioningAllowedTargets) == 0 {
cfg.ProvisioningAllowedTargets = []string{"folder"}
cfg.ProvisioningAllowedTargets = []string{"instance", "folder"}
}
cfg.ProvisioningAllowImageRendering = iniFile.Section("provisioning").Key("allow_image_rendering").MustBool(true)
cfg.ProvisioningMinSyncInterval = iniFile.Section("provisioning").Key("min_sync_interval").MustDuration(10 * time.Second)
@@ -1,12 +0,0 @@
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" }};
+8 -70
View File
@@ -34,10 +34,9 @@ func mustTemplate(filename string) *template.Template {
// Templates.
var (
sqlKVKeys = mustTemplate("sqlkv_keys.sql")
sqlKVGet = mustTemplate("sqlkv_get.sql")
sqlKVBatchGet = mustTemplate("sqlkv_batch_get.sql")
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
sqlKVGet = mustTemplate("sqlkv_get.sql")
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
sqlKVKeys = mustTemplate("sqlkv_keys.sql")
)
// sqlKVSection can be embedded in structs used when rendering query templates
@@ -108,23 +107,13 @@ func (req sqlKVGetRequest) Results() ([]byte, error) {
return req.Value, nil
}
type sqlKVBatchGetRequest struct {
type sqlKVDeleteRequest struct {
sqltemplate.SQLTemplate
sqlKVSection
Keys []string
sqlKVSectionKey
}
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
func (req sqlKVDeleteRequest) Validate() error {
return req.sqlKVSectionKey.Validate()
}
type sqlKVKeysRequest struct {
@@ -153,15 +142,6 @@ 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 {
@@ -208,7 +188,6 @@ 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
@@ -246,41 +225,7 @@ 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) {
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))
}
panic("not implemented!")
}
}
@@ -328,10 +273,3 @@ 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))
}
}
@@ -46,8 +46,8 @@ func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) {
return resource.DocumentBuilderInfo{
GroupResource: schema.GroupResource{
Group: v0alpha1.TeamResourceInfo.GroupResource().Group,
Resource: v0alpha1.TeamResourceInfo.GroupResource().Resource,
Group: "iam.grafana.app",
Resource: "searchTeams",
},
Fields: fields,
Builder: new(teamSearchBuilder),
+4 -5
View File
@@ -31,7 +31,6 @@ 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"
)
@@ -127,7 +126,7 @@ type backend struct {
notifier eventNotifier
// resource version manager
rvManager *rvmanager.ResourceVersionManager
rvManager *resourceVersionManager
// testing
simulatedNetworkLatency time.Duration
@@ -164,7 +163,7 @@ func (b *backend) initLocked(ctx context.Context) error {
}
// Initialize ResourceVersionManager
rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{
rvManager, err := NewResourceVersionManager(ResourceManagerOptions{
Dialect: b.dialect,
DB: b.db,
})
@@ -929,12 +928,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, rvmanager.SqlResourceVersionGet, rvmanager.SqlResourceVersionGetRequest{
res, err := dbutil.QueryRow(ctx, x, sqlResourceVersionGet, sqlResourceVersionGetRequest{
SQLTemplate: sqltemplate.New(d),
Group: group,
Resource: resource,
ReadOnly: true,
Response: new(rvmanager.ResourceVersionResponse),
Response: new(resourceVersionResponse),
})
if errors.Is(err, sql.ErrNoRows) {
return 1, nil
-20
View File
@@ -40,26 +40,6 @@ 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,7 +17,6 @@ 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"
@@ -95,7 +94,7 @@ func TestIntegrationListIter(t *testing.T) {
return fmt.Errorf("failed to insert test data: %w", err)
}
if _, err = dbutil.Exec(ctx, tx, rvmanager.SqlResourceUpdateRV, rvmanager.SqlResourceUpdateRVRequest{
if _, err = dbutil.Exec(ctx, tx, sqlResourceUpdateRV, sqlResourceUpdateRVRequest{
SQLTemplate: sqltemplate.New(dialect),
GUIDToRV: map[string]int64{
item.guid: item.resourceVersion,
+71 -1
View File
@@ -38,8 +38,10 @@ 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")
@@ -49,7 +51,10 @@ var (
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
sqlResourceVersionList = mustTemplate("resource_version_list.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")
sqlResourceBlobInsert = mustTemplate("resource_blob_insert.sql")
sqlResourceBlobQuery = mustTemplate("resource_blob_query.sql")
@@ -360,11 +365,76 @@ 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
+11 -12
View File
@@ -8,7 +8,6 @@ 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"
)
@@ -163,10 +162,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
rvmanager.SqlResourceUpdateRV: {
sqlResourceUpdateRV: {
{
Name: "single path",
Data: &rvmanager.SqlResourceUpdateRVRequest{
Data: &sqlResourceUpdateRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
GUIDToRV: map[string]int64{
"guid1": 123,
@@ -229,10 +228,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
rvmanager.SqlResourceHistoryUpdateRV: {
sqlResourceHistoryUpdateRV: {
{
Name: "single path",
Data: &rvmanager.SqlResourceUpdateRVRequest{
Data: &sqlResourceUpdateRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
GUIDToRV: map[string]int64{
"guid1": 123,
@@ -335,23 +334,23 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
rvmanager.SqlResourceVersionGet: {
sqlResourceVersionGet: {
{
Name: "single path",
Data: &rvmanager.SqlResourceVersionGetRequest{
Data: &sqlResourceVersionGetRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "resource",
Group: "group",
Response: new(rvmanager.ResourceVersionResponse),
Response: new(resourceVersionResponse),
ReadOnly: false,
},
},
},
rvmanager.SqlResourceVersionUpdate: {
sqlResourceVersionUpdate: {
{
Name: "increment resource version",
Data: &rvmanager.SqlResourceVersionUpsertRequest{
Data: &sqlResourceVersionUpsertRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Resource: "resource",
Group: "group",
@@ -360,10 +359,10 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
rvmanager.SqlResourceVersionInsert: {
sqlResourceVersionInsert: {
{
Name: "single path",
Data: &rvmanager.SqlResourceVersionUpsertRequest{
Data: &sqlResourceVersionUpsertRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
ResourceVersion: int64(12354),
},

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