From c62bc501d38da7119433774c09bd661129cc0956 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:33:13 +0000 Subject: [PATCH 01/29] Update dependency @types/node to v22.10.9 (#99413) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-icons/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/mssql/package.json | 2 +- .../app/plugins/datasource/mysql/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 48 +++++++++---------- 21 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 5a117590e23..0ccf9b743f3 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/lodash": "4.17.14", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 06400ab720c..cecf1602206 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -65,7 +65,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 0b2c08dc592..0e70a96c6bd 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.0", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/semver": "7.5.8", "esbuild": "0.24.2", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 9ff5b7b4f8b..1f3ca5b4913 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index d7b9d631c27..64befb9b038 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "esbuild": "0.24.2", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 684b9aed7fc..933fa923886 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index b2d54ad027b..c7f072a4662 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -92,7 +92,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index cab5a2ac907..a6da2fa240c 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -41,7 +41,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 65f5cd996f3..7b1872de24c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 019a11edb0a..e0c782fb730 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 2fbb13c34ce..5785f1bf348 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -35,7 +35,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 8cea9b0a65b..7b2e560795a 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 9d271b159df..daee4d49f97 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 23dc2a0f2e4..9d4990a6b85 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -30,7 +30,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index e1b2b9f8e89..4f66d778407 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -31,7 +31,7 @@ "@types/jest": "29.5.14", "@types/lodash": "4.17.14", "@types/logfmt": "^1.2.3", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-window": "1.8.8", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index c37bed22a65..e353840ecef 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 757edda8588..8cf4be5928e 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -23,7 +23,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "ts-node": "10.9.2", "typescript": "5.7.3", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 4bf0208ecdc..3ea03a1e72a 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index f81c885cc34..c867a299c7b 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 3a9f96042ae..1aa5c446a8a 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.1.0", "@types/jest": "29.5.14", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", diff --git a/yarn.lock b/yarn.lock index 2cc6e403916..6d65dc10968 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,7 +2679,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2721,7 +2721,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2751,7 +2751,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2793,7 +2793,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/uuid": "npm:10.0.0" @@ -2834,7 +2834,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" "@types/logfmt": "npm:^1.2.3" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-window": "npm:1.8.8" @@ -2874,7 +2874,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2905,7 +2905,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -2933,7 +2933,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -2971,7 +2971,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3024,7 +3024,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3074,7 +3074,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@types/jest": "npm:29.5.14" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" @@ -3132,7 +3132,7 @@ __metadata: "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/papaparse": "npm:5.3.15" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -3179,7 +3179,7 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@rollup/plugin-node-resolve": "npm:16.0.0" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/semver": "npm:7.5.8" esbuild: "npm:0.24.2" rimraf: "npm:6.0.1" @@ -3314,7 +3314,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" @@ -3407,7 +3407,7 @@ __metadata: "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/systemjs": "npm:6.15.1" jest: "npm:^29.6.4" @@ -3527,7 +3527,7 @@ __metadata: "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -3658,7 +3658,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/babel__core": "npm:^7" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" esbuild: "npm:0.24.2" @@ -3756,7 +3756,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.14" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/react-virtualized-auto-sizer": "npm:1.0.4" @@ -3843,7 +3843,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/lodash": "npm:4.17.14" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-color": "npm:3.0.13" @@ -9437,12 +9437,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.10.7, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": - version: 22.10.7 - resolution: "@types/node@npm:22.10.7" +"@types/node@npm:*, @types/node@npm:22.10.9, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4, @types/node@npm:^22.0.0": + version: 22.10.9 + resolution: "@types/node@npm:22.10.9" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/64cde1c2f5e5f7d597d3bd462f52c3c2d688a66623eb75d25e1d1d63d384ef553a27100635ad0dbb7d74da517048aa636947863eb624cf85f25d2f22370ce474 + checksum: 10/8a13d4e27e85b2e1878e2158400981feb5d5b9508cc920d475d5c327d93c7442d4baa5d2248d1503ffdd5964d955f54d4cf1009f3a3b812464e58cd6425a81cc languageName: node linkType: hard @@ -17437,7 +17437,7 @@ __metadata: "@types/lodash": "npm:4.17.14" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" - "@types/node": "npm:22.10.7" + "@types/node": "npm:22.10.9" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/pluralize": "npm:^0.0.33" From e110338dce30fa1bd1db454792ac38bccbbe598e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:51:31 +0200 Subject: [PATCH 02/29] Update dependency react-highlight-words to v0.21.0 (#99442) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 17 ++++++++--------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0ccf9b743f3..21488de372d 100644 --- a/package.json +++ b/package.json @@ -367,7 +367,7 @@ "react-draggable": "4.4.6", "react-dropzone": "^14.2.3", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index c7f072a4662..80c1a9171e7 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -64,7 +64,7 @@ "monaco-promql": "1.7.4", "pluralize": "8.0.0", "prismjs": "1.29.0", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-select": "5.9.0", "react-use": "17.6.0", "react-window": "1.8.11", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7b1872de24c..1d2eb1a9bbb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -92,7 +92,7 @@ "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-dropzone": "14.3.5", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", diff --git a/yarn.lock b/yarn.lock index 6d65dc10968..bf6dc173208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3572,7 +3572,7 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-select: "npm:5.9.0" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" @@ -3895,7 +3895,7 @@ __metadata: react-custom-scrollbars-2: "npm:4.5.0" react-dom: "npm:18.3.1" react-dropzone: "npm:14.3.5" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-hook-form: "npm:^7.49.2" react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" @@ -17603,7 +17603,7 @@ __metadata: react-draggable: "npm:4.4.6" react-dropzone: "npm:^14.2.3" react-grid-layout: "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch" - react-highlight-words: "npm:0.20.0" + react-highlight-words: "npm:0.21.0" react-hook-form: "npm:^7.49.2" react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" @@ -25281,16 +25281,15 @@ __metadata: languageName: node linkType: hard -"react-highlight-words@npm:0.20.0": - version: 0.20.0 - resolution: "react-highlight-words@npm:0.20.0" +"react-highlight-words@npm:0.21.0": + version: 0.21.0 + resolution: "react-highlight-words@npm:0.21.0" dependencies: highlight-words-core: "npm:^1.2.0" memoize-one: "npm:^4.0.0" - prop-types: "npm:^15.5.8" peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 - checksum: 10/5adf2cfb1f325ae51ea4dd2cb7522eb433b25534355868d1a3f4556b2b9f7a774c2a1aaa143abebb63a1b3a5590e70ba3d765942a47ff754a1a513cdc5b2f58b + react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + checksum: 10/5054e1d3f536bf672047cc22d954837289061754cd342c5439b153069fca6997890f44af4cf9154149f70844dc9cdbc18d5fd82b7458b430fc42b7f1c11a6bf2 languageName: node linkType: hard From 4e740d84109e7a1f534624847846656c9206be49 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 23 Jan 2025 12:57:00 +0000 Subject: [PATCH 03/29] GCM: Time-range fix (#98455) * Include timeRange on all query executors * Ensure queries run against query specific time range * Fix lint * Improve safety of annotations queries --- pkg/tsdb/cloud-monitoring/annotation_query.go | 6 ++++++ pkg/tsdb/cloud-monitoring/cloudmonitoring.go | 18 ++++++++++-------- pkg/tsdb/cloud-monitoring/promql_query.go | 2 +- pkg/tsdb/cloud-monitoring/slo_query.go | 2 +- .../cloud-monitoring/time_series_filter.go | 2 +- pkg/tsdb/cloud-monitoring/time_series_query.go | 8 ++++---- pkg/tsdb/cloud-monitoring/types.go | 6 ++++-- pkg/tsdb/cloud-monitoring/utils.go | 10 +++++----- 8 files changed, 32 insertions(+), 22 deletions(-) diff --git a/pkg/tsdb/cloud-monitoring/annotation_query.go b/pkg/tsdb/cloud-monitoring/annotation_query.go index 5905e42317e..ee11abe95bd 100644 --- a/pkg/tsdb/cloud-monitoring/annotation_query.go +++ b/pkg/tsdb/cloud-monitoring/annotation_query.go @@ -3,6 +3,7 @@ package cloudmonitoring import ( "context" "encoding/json" + "errors" "strconv" "strings" "time" @@ -38,6 +39,11 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query } `json:"timeSeriesList"` }{} + if len(req.Queries) != 1 { + return nil, errors.New("multiple queries received in annotation-request") + } + + // It's okay to use the first query for annotations as there should only be one firstQuery := req.Queries[0] err = json.Unmarshal(firstQuery.JSON, &tslq) if err != nil { diff --git a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go index 319a0c66992..107e51008c4 100644 --- a/pkg/tsdb/cloud-monitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloud-monitoring/cloudmonitoring.go @@ -387,11 +387,11 @@ func queryModel(query backend.DataQuery) (grafanaQuery, error) { func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataRequest) ([]cloudMonitoringQueryExecutor, error) { cloudMonitoringQueryExecutors := make([]cloudMonitoringQueryExecutor, 0, len(req.Queries)) - startTime := req.Queries[0].TimeRange.From - endTime := req.Queries[0].TimeRange.To - durationSeconds := int(endTime.Sub(startTime).Seconds()) - for _, query := range req.Queries { + for index, query := range req.Queries { + startTime := req.Queries[index].TimeRange.From + endTime := req.Queries[index].TimeRange.To + durationSeconds := int(endTime.Sub(startTime).Seconds()) q, err := queryModel(query) if err != nil { return nil, fmt.Errorf("could not unmarshal CloudMonitoringQuery json: %w", err) @@ -401,8 +401,9 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR switch query.QueryType { case string(dataquery.QueryTypeTIMESERIESLIST), string(dataquery.QueryTypeANNOTATION): cmtsf := &cloudMonitoringTimeSeriesList{ - refID: query.RefID, - aliasBy: q.AliasBy, + refID: query.RefID, + aliasBy: q.AliasBy, + timeRange: req.Queries[index].TimeRange, } if q.TimeSeriesList.View == nil || *q.TimeSeriesList.View == "" { fullString := "FULL" @@ -417,7 +418,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR aliasBy: q.AliasBy, parameters: q.TimeSeriesQuery, IntervalMS: query.Interval.Milliseconds(), - timeRange: req.Queries[0].TimeRange, + timeRange: req.Queries[index].TimeRange, logger: logger, } case string(dataquery.QueryTypeSLO): @@ -425,6 +426,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR refID: query.RefID, aliasBy: q.AliasBy, parameters: q.SloQuery, + timeRange: req.Queries[index].TimeRange, } cmslo.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds()) queryInterface = cmslo @@ -433,7 +435,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR refID: query.RefID, aliasBy: q.AliasBy, parameters: q.PromQLQuery, - timeRange: req.Queries[0].TimeRange, + timeRange: req.Queries[index].TimeRange, logger: logger, } queryInterface = cmp diff --git a/pkg/tsdb/cloud-monitoring/promql_query.go b/pkg/tsdb/cloud-monitoring/promql_query.go index 63baf4584f2..e71a22ade55 100644 --- a/pkg/tsdb/cloud-monitoring/promql_query.go +++ b/pkg/tsdb/cloud-monitoring/promql_query.go @@ -31,7 +31,7 @@ func (promQLQ *cloudMonitoringProm) run(ctx context.Context, req *backend.QueryD return dr, backend.DataResponse{}, "", nil } - span := traceReq(ctx, req, dsInfo, r, "") + span := traceReq(ctx, req, dsInfo, r, "", promQLQ.timeRange) defer span.End() requestBody := map[string]any{ diff --git a/pkg/tsdb/cloud-monitoring/slo_query.go b/pkg/tsdb/cloud-monitoring/slo_query.go index 7aa6544482d..bcf53610cf7 100644 --- a/pkg/tsdb/cloud-monitoring/slo_query.go +++ b/pkg/tsdb/cloud-monitoring/slo_query.go @@ -12,7 +12,7 @@ import ( func (sloQ *cloudMonitoringSLO) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { - return runTimeSeriesRequest(ctx, req, s, dsInfo, sloQ.parameters.ProjectName, sloQ.params, nil, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, sloQ.parameters.ProjectName, sloQ.params, nil, logger, sloQ.timeRange) } func (sloQ *cloudMonitoringSLO) parseResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/time_series_filter.go b/pkg/tsdb/cloud-monitoring/time_series_filter.go index 7df6fd16f4e..f2fe2da5dab 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_filter.go +++ b/pkg/tsdb/cloud-monitoring/time_series_filter.go @@ -17,7 +17,7 @@ import ( func (timeSeriesFilter *cloudMonitoringTimeSeriesList) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { - return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesFilter.parameters.ProjectName, timeSeriesFilter.params, nil, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesFilter.parameters.ProjectName, timeSeriesFilter.params, nil, logger, timeSeriesFilter.timeRange) } func parseTimeSeriesResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/time_series_query.go b/pkg/tsdb/cloud-monitoring/time_series_query.go index 84f66a193a4..bacf61e645b 100644 --- a/pkg/tsdb/cloud-monitoring/time_series_query.go +++ b/pkg/tsdb/cloud-monitoring/time_series_query.go @@ -18,7 +18,7 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *ba if timeSeriesQuery.parameters.GraphPeriod != "disabled" { if timeSeriesQuery.parameters.GraphPeriod == "auto" || timeSeriesQuery.parameters.GraphPeriod == "" { intervalCalculator := gcmTime.NewCalculator(gcmTime.CalculatorOptions{}) - interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) + interval := intervalCalculator.Calculate(timeSeriesQuery.timeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) timeSeriesQuery.parameters.GraphPeriod = interval.Text } return fmt.Sprintf(" | graph_period %s", timeSeriesQuery.parameters.GraphPeriod) @@ -29,14 +29,14 @@ func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *ba func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, logger log.Logger) (*backend.DataResponse, any, string, error) { timeSeriesQuery.parameters.Query += timeSeriesQuery.appendGraphPeriod(req) - from := req.Queries[0].TimeRange.From - to := req.Queries[0].TimeRange.To + from := timeSeriesQuery.timeRange.From + to := timeSeriesQuery.timeRange.To timeFormat := "2006/01/02-15:04:05" timeSeriesQuery.parameters.Query += fmt.Sprintf(" | within d'%s', d'%s'", from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) requestBody := map[string]any{ "query": timeSeriesQuery.parameters.Query, } - return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesQuery.parameters.ProjectName, nil, requestBody, logger) + return runTimeSeriesRequest(ctx, req, s, dsInfo, timeSeriesQuery.parameters.ProjectName, nil, requestBody, logger, timeSeriesQuery.timeRange) } func (timeSeriesQuery *cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *backend.DataResponse, diff --git a/pkg/tsdb/cloud-monitoring/types.go b/pkg/tsdb/cloud-monitoring/types.go index 39610af7500..e16415d3aca 100644 --- a/pkg/tsdb/cloud-monitoring/types.go +++ b/pkg/tsdb/cloud-monitoring/types.go @@ -42,7 +42,8 @@ type ( aliasBy string parameters *dataquery.TimeSeriesList // Processed properties - params url.Values + timeRange backend.TimeRange + params url.Values } // cloudMonitoringSLO is used to build time series with a filter but for the SLO case cloudMonitoringSLO struct { @@ -50,7 +51,8 @@ type ( aliasBy string parameters *dataquery.SLOQuery // Processed properties - params url.Values + timeRange backend.TimeRange + params url.Values } // cloudMonitoringProm is used to build a promQL queries diff --git a/pkg/tsdb/cloud-monitoring/utils.go b/pkg/tsdb/cloud-monitoring/utils.go index 4499ebe5dad..1843fd6db52 100644 --- a/pkg/tsdb/cloud-monitoring/utils.go +++ b/pkg/tsdb/cloud-monitoring/utils.go @@ -124,11 +124,11 @@ func doRequestWithPagination(ctx context.Context, r *http.Request, dsInfo dataso return d, nil } -func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo, _ *http.Request, target string) trace.Span { +func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo, _ *http.Request, target string, timeRange backend.TimeRange) trace.Span { _, span := tracing.DefaultTracer().Start(ctx, "cloudMonitoring query", trace.WithAttributes( attribute.String("target", target), - attribute.String("from", req.Queries[0].TimeRange.From.String()), - attribute.String("until", req.Queries[0].TimeRange.To.String()), + attribute.String("from", timeRange.From.String()), + attribute.String("until", timeRange.To.String()), attribute.Int64("datasource_id", dsInfo.id), attribute.Int64("org_id", req.PluginContext.OrgID), )) @@ -137,7 +137,7 @@ func traceReq(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasou } func runTimeSeriesRequest(ctx context.Context, req *backend.QueryDataRequest, - s *Service, dsInfo datasourceInfo, projectName string, params url.Values, body map[string]any, logger log.Logger) (*backend.DataResponse, cloudMonitoringResponse, string, error) { + s *Service, dsInfo datasourceInfo, projectName string, params url.Values, body map[string]any, logger log.Logger, timeRange backend.TimeRange) (*backend.DataResponse, cloudMonitoringResponse, string, error) { dr := &backend.DataResponse{} projectName, err := s.ensureProject(ctx, dsInfo, projectName) if err != nil { @@ -154,7 +154,7 @@ func runTimeSeriesRequest(ctx context.Context, req *backend.QueryDataRequest, return dr, cloudMonitoringResponse{}, "", nil } - span := traceReq(ctx, req, dsInfo, r, params.Encode()) + span := traceReq(ctx, req, dsInfo, r, params.Encode(), timeRange) defer span.End() d, err := doRequestWithPagination(ctx, r, dsInfo, params, body, logger) From 2716db4270b9c7f590aed8a25dbb65956bd8a42d Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:13:29 +0100 Subject: [PATCH 04/29] Alerting: Use stack_id instead of id in cloud failures panel in the Insights page (#99424) * Use stack_id instead of id in cloud failures panel in the insights page * update all the wrong id to stack_id --- .../alerting/unified/insights/mimir/AlertsByState.tsx | 2 +- .../alerting/unified/insights/mimir/InvalidConfig.tsx | 2 +- .../alerting/unified/insights/mimir/Notifications.tsx | 4 ++-- .../app/features/alerting/unified/insights/mimir/Silences.tsx | 2 +- .../RuleGroupEvaluationDurationIntervalRatioScene.tsx | 2 +- .../mimir/perGroup/RuleGroupEvaluationDurationScene.tsx | 2 +- .../insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx | 4 ++-- .../insights/mimir/perGroup/RuleGroupIntervalScene.tsx | 2 +- .../unified/insights/mimir/perGroup/RulesPerGroupScene.tsx | 2 +- .../insights/mimir/rules/EvalSuccessVsFailuresScene.tsx | 4 ++-- .../unified/insights/mimir/rules/MissedIterationsScene.tsx | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx index 5ea2438a856..f977a642fd4 100644 --- a/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx +++ b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getAlertsByStateScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (state) (grafanacloud_instance_alertmanager_alerts{id="${INSTANCE_ID}"})` + ? `sum by (state) (grafanacloud_instance_alertmanager_alerts{stack_id="${INSTANCE_ID}"})` : `sum by (state) (grafanacloud_instance_alertmanager_alerts)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx index 8c9a43227cf..d5834b1abb9 100644 --- a/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx +++ b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getInvalidConfigScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config{id="${INSTANCE_ID}"})` + ? `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config{stack_id="${INSTANCE_ID}"})` : `sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/Notifications.tsx b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx index 59c0ecdc4f4..0f2d254a2ec 100644 --- a/public/app/features/alerting/unified/insights/mimir/Notifications.tsx +++ b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getNotificationsScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second{id="${INSTANCE_ID}"}) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{id="${INSTANCE_ID}"})` + ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second{stack_id="${INSTANCE_ID}"}) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{stack_id="${INSTANCE_ID}"})` : `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)`; const exprB = INSTANCE_ID - ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{id="${INSTANCE_ID}"})` + ? `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second{stack_id="${INSTANCE_ID}"})` : `sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/Silences.tsx b/public/app/features/alerting/unified/insights/mimir/Silences.tsx index 8cbddfd0205..670038808a3 100644 --- a/public/app/features/alerting/unified/insights/mimir/Silences.tsx +++ b/public/app/features/alerting/unified/insights/mimir/Silences.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../InsightsMenuButton'; export function getSilencesScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum by (state) (grafanacloud_instance_alertmanager_silences{id="${INSTANCE_ID}"})` + ? `sum by (state) (grafanacloud_instance_alertmanager_silences{stack_id="${INSTANCE_ID}"})` : `sum by (state) (grafanacloud_instance_alertmanager_silences)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx index 50f4084ac7c..f05e8fb818d 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationIntervalRatioScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationDurationIntervalRatioScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group"} / grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx index 12ff616336a..5ae445093cd 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationDurationScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx index 4eb70e39d0b..d29901ae7c3 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupEvaluationsScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group", id="${INSTANCE_ID}"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group", stack_id="${INSTANCE_ID}"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; const exprB = INSTANCE_ID - ? `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx index f54fc582bc4..521580c096c 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRuleGroupIntervalScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", id="${INSTANCE_ID}"}` + ? `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group", stack_id="${INSTANCE_ID}"}` : `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group"}`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx index c2c9ac1b44d..3000ce2c31d 100644 --- a/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getRulesPerGroupScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group", id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group", stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group"})`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx index e82fadeb898..bd457de9e64 100644 --- a/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx @@ -6,11 +6,11 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getEvalSuccessVsFailuresScene(datasource: DataSourceRef, panelTitle: string) { const exprA = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_evaluations_total:rate5m{id="${INSTANCE_ID}"}) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_evaluations_total:rate5m{stack_id="${INSTANCE_ID}"}) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_evaluations_total:rate5m) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)`; const exprB = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)`; const query = new SceneQueryRunner({ diff --git a/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx index c32f399146c..9331c90c53a 100644 --- a/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx +++ b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx @@ -6,7 +6,7 @@ import { InsightsMenuButton } from '../../InsightsMenuButton'; export function getMissedIterationsScene(datasource: DataSourceRef, panelTitle: string) { const expr = INSTANCE_ID - ? `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m{id="${INSTANCE_ID}"})` + ? `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m{stack_id="${INSTANCE_ID}"})` : `sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m)`; const query = new SceneQueryRunner({ From 89dd54a47493df60debddc665ffcd91de3a3c04d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 23 Jan 2025 06:36:07 -0700 Subject: [PATCH 05/29] Folder: delete from folder table after children (#99399) Co-authored-by: maicon --- pkg/services/folder/folderimpl/folder.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index cdcb9c2ee2b..8d86292c758 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -992,6 +992,12 @@ func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderComm } } + err = s.store.Delete(ctx, []string{cmd.UID}, cmd.OrgID) + if err != nil { + s.log.InfoContext(ctx, "failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) + return err + } + if err = s.legacyDelete(ctx, cmd, folders); err != nil { return err } @@ -1240,11 +1246,11 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold for _, f := range descendants { descendantUIDs = append(descendantUIDs, f.UID) } - s.log.InfoContext(ctx, "deleting folder and its descendants", "org_id", cmd.OrgID, "uid", cmd.UID) - toDelete := append(descendantUIDs, cmd.UID) - err = s.store.Delete(ctx, toDelete, cmd.OrgID) + s.log.InfoContext(ctx, "deleting folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID) + + err = s.store.Delete(ctx, descendantUIDs, cmd.OrgID) if err != nil { - s.log.InfoContext(ctx, "failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) + s.log.InfoContext(ctx, "failed deleting descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "err", err) return descendantUIDs, err } return descendantUIDs, nil From d39e57e836d77722d26bc909154fcad4044c72a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:42:29 +0000 Subject: [PATCH 06/29] Update dependency react-i18next to v15.4.0 (#99444) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bf6dc173208..96111fd4d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25312,8 +25312,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.2.0 - resolution: "react-i18next@npm:15.2.0" + version: 15.4.0 + resolution: "react-i18next@npm:15.4.0" dependencies: "@babel/runtime": "npm:^7.25.0" html-parse-stringify: "npm:^3.0.1" @@ -25325,7 +25325,7 @@ __metadata: optional: true react-native: optional: true - checksum: 10/9b2937f7beab763c494d55a801f21bfdbfe98e9509994c350d24fa404ded573f41e8607eeba290c686d5877d34f0ddefe48e9d6876720d5ed0e1243bcdd5dda6 + checksum: 10/4b3666d819f01cf96a256af4419b26938d314e33c6388eafccc29f67ad02994e5d53e7bf82eac656cade7f7bcd04f4a237f0b293165d7eda91d62e3fde605a38 languageName: node linkType: hard From a037c6f34472ec8e16ab1020121a1be30b0e4186 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 23 Jan 2025 17:25:03 +0300 Subject: [PATCH 07/29] K8s/Folders: Remove kubernetesFolders flag and full path metadata (#99256) * remove full path * remove more * remove KubernetesFolders tests * remove feature toggles * remove feature toggles * skip permissions test * skip permissions test --------- Co-authored-by: Jack Baldry --- .../feature-toggles/index.md | 212 ++++--- .../src/types/featureToggles.gen.ts | 2 - pkg/api/folder.go | 570 +----------------- pkg/api/folder_test.go | 348 +---------- pkg/apimachinery/utils/meta.go | 41 -- pkg/registry/apis/folders/conversions.go | 38 -- pkg/registry/apis/folders/conversions_test.go | 18 - pkg/registry/apis/folders/register.go | 2 - pkg/services/featuremgmt/registry.go | 12 - pkg/services/featuremgmt/toggles_gen.csv | 2 - pkg/services/featuremgmt/toggles_gen.go | 8 - pkg/services/featuremgmt/toggles_gen.json | 152 ++--- pkg/services/folder/folderimpl/folder.go | 14 +- .../folderimpl/folder_unifiedstorage_test.go | 1 - pkg/tests/apis/folder/folders_test.go | 40 +- 15 files changed, 209 insertions(+), 1251 deletions(-) delete mode 100644 pkg/registry/apis/folders/conversions_test.go diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2bd6d4eef14..435c59f79ef 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -129,113 +129,111 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- [Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | -| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `canvasPanelNesting` | Allow elements nesting | -| `vizActions` | Allow actions in visualizations | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | -| `datasourceAPIServers` | Expose some datasources as apiservers. | -| `provisioning` | Next generation provisioning... and git | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `externalCorePlugins` | Allow core plugins to be loaded as external | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | -| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | -| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | -| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | -| `kubernetesRestore` | Allow restoring objects in k8s | -| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s | -| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | -| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs | -| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | -| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | -| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | -| `queryServiceFromUI` | Routes requests to the new query service | -| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | -| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | -| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | -| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | -| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | -| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardNewLayouts` | Enables experimental new dashboard layouts | -| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | -| `tableSharedCrosshair` | Enables shared crosshair in table panel | -| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | -| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | -| `scopeApi` | In-development feature flag for the scope api using the app platform. | -| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | -| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | -| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | -| `expressionParser` | Enable new expression parser | -| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | -| `queryLibrary` | Enables Query Library feature in Explore | -| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `alertingListViewV2` | Enables the new alert list view design | -| `dashboardRestore` | Enables deleted dashboard restore feature | -| `alertingCentralAlertHistory` | Enables the new central alert history. | -| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | -| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | -| `dataplaneAggregator` | Enable grafana dataplane aggregator | -| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | -| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | -| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | -| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | -| `rolePickerDrawer` | Enables the new role picker drawer design | -| `pluginsSriChecks` | Enables SRI checks for plugin assets | -| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | -| `timeRangeProvider` | Enables time pickers sync | -| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | -| `playlistsReconciler` | Enables experimental reconciler for playlists | -| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | -| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | -| `enableSCIM` | Enables SCIM support for user and group management | -| `crashDetection` | Enables browser crash detection reporting to Faro. | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | -| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | -| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | -| `investigationsBackend` | Enable the investigations backend API | -| `k8SFolderCounts` | Enable folder's api server counts | -| `k8SFolderMove` | Enable folder's api server move | -| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | -| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | -| `grafanaAdvisor` | Enables Advisor app | -| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | -| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | +| Feature toggle name | Description | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | +| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | +| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | +| `storage` | Configurable storage for dashboards, datasources, and resources | +| `canvasPanelNesting` | Allow elements nesting | +| `vizActions` | Allow actions in visualizations | +| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | +| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | +| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | +| `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `datasourceAPIServers` | Expose some datasources as apiservers. | +| `provisioning` | Next generation provisioning... and git | +| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | +| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | +| `libraryPanelRBAC` | Enables RBAC support for library panels | +| `wargamesTesting` | Placeholder feature flag for internal testing | +| `externalCorePlugins` | Allow core plugins to be loaded as external | +| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | +| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | +| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | +| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | +| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | +| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | +| `kubernetesRestore` | Allow restoring objects in k8s | +| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | +| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | +| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | +| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | +| `queryServiceFromUI` | Routes requests to the new query service | +| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | +| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | +| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | +| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | +| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | +| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | +| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | +| `dashboardNewLayouts` | Enables experimental new dashboard layouts | +| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | +| `tableSharedCrosshair` | Enables shared crosshair in table panel | +| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | +| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | +| `scopeApi` | In-development feature flag for the scope api using the app platform. | +| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | +| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | +| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | +| `expressionParser` | Enable new expression parser | +| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | +| `queryLibrary` | Enables Query Library feature in Explore | +| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | +| `alertingListViewV2` | Enables the new alert list view design | +| `dashboardRestore` | Enables deleted dashboard restore feature | +| `alertingCentralAlertHistory` | Enables the new central alert history. | +| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | +| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | +| `dataplaneAggregator` | Enable grafana dataplane aggregator | +| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | +| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | +| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | +| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | +| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | +| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | +| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | +| `rolePickerDrawer` | Enables the new role picker drawer design | +| `pluginsSriChecks` | Enables SRI checks for plugin assets | +| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | +| `timeRangeProvider` | Enables time pickers sync | +| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | +| `playlistsReconciler` | Enables experimental reconciler for playlists | +| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | +| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | +| `enableSCIM` | Enables SCIM support for user and group management | +| `crashDetection` | Enables browser crash detection reporting to Faro. | +| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | +| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | +| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | +| `investigationsBackend` | Enable the investigations backend API | +| `k8SFolderCounts` | Enable folder's api server counts | +| `k8SFolderMove` | Enable folder's api server move | +| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | +| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | +| `grafanaAdvisor` | Enables Advisor app | +| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | +| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e26b79a8056..97e532863af 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -112,9 +112,7 @@ export interface FeatureToggles { kubernetesDashboards?: boolean; kubernetesCliDashboards?: boolean; kubernetesRestore?: boolean; - kubernetesFolders?: boolean; kubernetesFoldersServiceV2?: boolean; - grafanaAPIServerTestingWithExperimentalAPIs?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; diff --git a/pkg/api/folder.go b/pkg/api/folder.go index c6ca4e308fb..af38eb9de2c 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -3,16 +3,8 @@ package api import ( "context" "errors" - "fmt" "net/http" "strconv" - "strings" - - k8sErrors "k8s.io/apimachinery/pkg/api/errors" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/apierrors" @@ -20,13 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" - folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/infra/slugify" - internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" - grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" @@ -36,9 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" ) @@ -57,38 +42,16 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions)) }) }) - if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { - // Use k8s client to implement legacy API - handler := newFolderK8sHandler(hs) - folderRoute.Post("/", handler.createFolder) - folderRoute.Get("/", handler.getFolders) - folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { - folderUidRoute.Put("/", handler.updateFolder) - folderUidRoute.Delete("/", handler.deleteFolder) - folderUidRoute.Get("/", handler.getFolder) - if hs.Features.IsEnabledGlobally(featuremgmt.FlagK8SFolderCounts) { - folderUidRoute.Get("/counts", handler.countFolderContent) - } else { - folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) - } - if hs.Features.IsEnabledGlobally(featuremgmt.FlagK8SFolderMove) { - folderUidRoute.Post("/move", handler.moveFolder) - } else { - folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) - } - folderUidRoute.Get("parents", handler.getFolderParents) - }) - } else { - folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder)) - folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders)) - folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { - folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder)) - folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder)) - folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID)) - folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) - folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) - }) - } + + folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder)) + folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders)) + folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { + folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder)) + folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder)) + folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID)) + folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts)) + folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder)) + }) }) } @@ -683,516 +646,3 @@ type GetFolderDescendantCountsResponse struct { // in: body Body folder.DescendantCounts `json:"body"` } - -type folderK8sHandler struct { - namespacer request.NamespaceMapper - gvr schema.GroupVersionResource - clientConfigProvider grafanaapiserver.DirectRestConfigProvider - // #TODO check if it makes more sense to move this to FolderAPIBuilder - accesscontrolService accesscontrol.Service - userService user.Service -} - -//----------------------------------------------------------------------------------------- -// Folder k8s wrapper functions -//----------------------------------------------------------------------------------------- - -func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler { - return &folderK8sHandler{ - gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(hs.Cfg), - clientConfigProvider: hs.clientConfigProvider, - accesscontrolService: hs.accesscontrolService, - userService: hs.userService, - } -} - -func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - cmd := &folder.CreateFolderCommand{} - if err := web.Bind(c.Req, cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - obj, err := internalfolders.LegacyCreateCommandToUnstructured(cmd) - if err != nil { - fk8s.writeError(c, err) - return - } - out, err := client.Create(c.Req.Context(), obj, v1.CreateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) getFolders(c *contextmodel.ReqContext) { - // NOTE: the current implementation is temporary and it will be - // replaced by a proper indexing service/search API - // Also, the current implementation does not support pagination - - parentUid := strings.ToUpper(c.Query("parentUid")) - - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - - // check that parent exists - if parentUid != "" { - _, err := client.Get(c.Req.Context(), c.Query("parentUid"), v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - } - - out, err := client.List(c.Req.Context(), v1.ListOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - hits := make([]dtos.FolderSearchHit, 0) - for _, item := range out.Items { - // convert item to legacy folder format - f, _ := internalfolders.UnstructuredToLegacyFolder(item, c.SignedInUser.GetOrgID()) - if f == nil { - fk8s.writeError(c, fmt.Errorf("unable covert unstructured item to legacy folder")) - return - } - - // it we are at root level, skip subfolder - if parentUid == "" && f.ParentUID != "" { - continue // query filter - } - // if we are at a nested folder, then skip folders that don't belong to parentUid - if parentUid != "" && strings.ToUpper(f.ParentUID) != parentUid { - continue - } - - hits = append(hits, dtos.FolderSearchHit{ - ID: f.ID, // nolint:staticcheck - UID: f.UID, - Title: f.Title, - ParentUID: f.ParentUID, - }) - } - - c.JSON(http.StatusOK, hits) -} - -func (fk8s *folderK8sHandler) countFolderContent(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - uid := web.Params(c.Req)[":uid"] - - counts, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "counts") - if err != nil { - fk8s.writeError(c, err) - return - } - - out, err := toFolderLegacyCounts(counts) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, out) -} - -func (fk8s *folderK8sHandler) getFolderParents(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - uid := web.Params(c.Req)[":uid"] - - out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "parents") - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, out) -} - -func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - - var out *unstructured.Unstructured - var err error - - if uid == accesscontrol.GeneralFolderUID { - out = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "title": folder.RootFolder.Title, - "description": folder.RootFolder.Description, - }, - }, - } - out.SetName(folder.RootFolder.UID) - } else { - out, err = client.Get(c.Req.Context(), uid, v1.GetOptions{}) - } - - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - uid := web.Params(c.Req)[":uid"] - err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - c.JSON(http.StatusOK, "") -} - -func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return // error is already sent - } - - var ctx = c.Req.Context() - - cmd := &folder.UpdateFolderCommand{} - if err := web.Bind(c.Req, cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - cmd.UID = web.Params(c.Req)[":uid"] - - obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - updated, err := internalfolders.LegacyUpdateCommandToUnstructured(obj, cmd) - if err != nil { - return - } - - out, err := client.Update(ctx, updated, v1.UpdateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -func (fk8s *folderK8sHandler) moveFolder(c *contextmodel.ReqContext) { - client, ok := fk8s.getClient(c) - if !ok { - return - } - - ctx := c.Req.Context() - - cmd := folder.MoveFolderCommand{} - if err := web.Bind(c.Req, &cmd); err != nil { - c.JsonApiErr(http.StatusBadRequest, "bad request data", err) - return - } - cmd.UID = web.Params(c.Req)[":uid"] - - obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - obj, err = internalfolders.LegacyMoveCommandToUnstructured(obj, cmd) - if err != nil { - fk8s.writeError(c, err) - return - } - - out, err := client.Update(c.Req.Context(), obj, v1.UpdateOptions{}) - if err != nil { - fk8s.writeError(c, err) - return - } - - folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) - if err != nil { - fk8s.writeError(c, err) - return - } - - c.JSON(http.StatusOK, folderDTO) -} - -//----------------------------------------------------------------------------------------- -// Utility functions -//----------------------------------------------------------------------------------------- - -func (fk8s *folderK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) { - dyn, err := dynamic.NewForConfig(fk8s.clientConfigProvider.GetDirectRestConfig(c)) - if err != nil { - c.JsonApiErr(500, "client", err) - return nil, false - } - return dyn.Resource(fk8s.gvr).Namespace(fk8s.namespacer(c.OrgID)), true -} - -func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) { - //nolint:errorlint - statusError, ok := err.(*k8sErrors.StatusError) - if ok { - message := statusError.Status().Message - // #TODO: Is there a better way to set the correct meesage? Instead of "access denied to folder", currently we are - // returning something like `folders.folder.grafana.app is forbidden: User "" cannot create resource "folders" in - // API group "folder.grafana.app" in the namespace "default": folder`` - if statusError.Status().Code == http.StatusForbidden { - message = dashboards.ErrFolderAccessDenied.Error() - } - c.JsonApiErr(int(statusError.Status().Code), message, err) - return - } - errhttp.Write(c.Req.Context(), err, c.Resp) -} - -func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item unstructured.Unstructured, orgID int64) (dtos.Folder, error) { - f, createdBy := internalfolders.UnstructuredToLegacyFolder(item, orgID) - - dontCheckCanView := false - checkCanView := true - // no need to check view permission for the starting folder since it's already checked by the callers - folderDTO, err := fk8s.toDTO(c, f, createdBy, dontCheckCanView) - if err != nil { - return dtos.Folder{}, err - } - - if len(f.Fullpath) == 0 || len(f.FullpathUIDs) == 0 { - return folderDTO, nil - } - - parentsFullPath, err := internalfolders.GetParentTitles(f.Fullpath) - if err != nil { - return dtos.Folder{}, err - } - parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") - - // The first part of the path is the newly created folder which we don't need to include - // in the parents field - if len(parentsFullPath) < 2 || len(parentsFullPathUIDs) < 2 { - return folderDTO, nil - } - - parents := []dtos.Folder{} - for i, v := range parentsFullPath[1:] { - slug := slugify.Slugify(v) - uid := parentsFullPathUIDs[1:][i] - url := dashboards.GetFolderURL(uid, slug) - - ff := folder.Folder{ - UID: uid, - Title: v, - URL: url, - } - parentDTO, err := fk8s.toDTO(c, &ff, "", checkCanView) - if err != nil { - // #TODO should we log this error? - return dtos.Folder{}, err - } - - parents = append(parents, parentDTO) - } - - folderDTO.Parents = parents - - return folderDTO, nil -} - -func toUID(rawIdentifier string) string { - // #TODO Is there a preexisting function we can use instead, something along the lines of UserIdentifier? - parts := strings.Split(rawIdentifier, ":") - if len(parts) < 2 { - return "" - } - return parts[1] -} - -func (fk8s *folderK8sHandler) toDTO(c *contextmodel.ReqContext, fold *folder.Folder, createdBy string, checkCanView bool) (dtos.Folder, error) { - // #TODO revisit how/where we get orgID - ctx := c.Req.Context() - - g, err := guardian.NewByFolder(c.Req.Context(), fold, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return dtos.Folder{}, err - } - - canEdit, _ := g.CanEdit() - canSave, _ := g.CanSave() - canAdmin, _ := g.CanAdmin() - canDelete, _ := g.CanDelete() - - // Finding creator and last updater of the folder - updater, creator := anonString, anonString - // #TODO refactor the various conversions of the folder so that we either set created by in folder.Folder or - // we convert from unstructured to folder DTO without an intermediate conversion to folder.Folder - if len(createdBy) > 0 { - creator = fk8s.getIdentityName(ctx, toUID(createdBy)) - } - if len(createdBy) > 0 { - updater = fk8s.getIdentityName(ctx, toUID(createdBy)) - } - - acMetadata, _ := fk8s.getFolderACMetadata(c, fold) - - if checkCanView { - canView, _ := g.CanView() - if !canView { - return dtos.Folder{ - UID: REDACTED, - Title: REDACTED, - }, nil - } - } - metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc() - - return dtos.Folder{ - ID: fold.ID, // nolint:staticcheck - UID: fold.UID, - Title: fold.Title, - URL: fold.URL, - HasACL: fold.HasACL, - CanSave: canSave, - CanEdit: canEdit, - CanAdmin: canAdmin, - CanDelete: canDelete, - CreatedBy: creator, - Created: fold.Created, - UpdatedBy: updater, - Updated: fold.Updated, - // #TODO version doesn't seem to be used--confirm or set it properly - Version: fold.Version, - AccessControl: acMetadata, - ParentUID: fold.ParentUID, - }, nil -} - -func (fk8s *folderK8sHandler) getIdentityName(ctx context.Context, uid string) string { - ctx, span := tracer.Start(ctx, "api.getUserLogin") - defer span.End() - - ident, err := fk8s.userService.GetByUID(ctx, &user.GetUserByUIDQuery{ - UID: uid, - }) - if err != nil { - return anonString - } - - if ident.IsServiceAccount { - return ident.Name - } - return ident.Login -} - -func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) { - if !c.QueryBool("accesscontrol") { - return nil, nil - } - - folderIDs, err := getParents(f) - if err != nil { - return nil, err - } - - allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs) - metadata := map[string]bool{} - // Flatten metadata - if any parent has a permission, the child folder inherits it - for _, md := range allMetadata { - for action := range md { - metadata[action] = true - } - } - return metadata, nil -} - -func getParents(f *folder.Folder) (map[string]bool, error) { - folderIDs := map[string]bool{f.UID: true} - if (f.UID == accesscontrol.GeneralFolderUID) || (f.UID == folder.SharedWithMeFolderUID) { - return folderIDs, nil - } - - parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/") - // The first part of the path is the newly created folder which we don't need to check here - if len(parentsFullPathUIDs) < 2 { - return folderIDs, nil - } - - for _, uid := range parentsFullPathUIDs[1:] { - folderIDs[uid] = true - } - - return folderIDs, nil -} - -func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) { - ds, err := folderalpha1.UnstructuredToDescendantCounts(u) - if err != nil { - return nil, err - } - - var out = make(folder.DescendantCounts) - for _, v := range ds.Counts { - // if stats come from unified storage, we will use them - if v.Group != "sql-fallback" { - out[v.Resource] = v.Count - continue - } - // if stats are from single tenant DB and they are not in unified storage, we will use them - if _, ok := out[v.Resource]; !ok { - out[v.Resource] = v.Count - } - } - return &out, nil -} diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index b676bb15696..a464727a31b 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -8,18 +8,15 @@ import ( "net/http/httptest" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/api/dtos" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - conversions "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -532,349 +529,6 @@ func (m mockClientConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext func (m mockClientConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {} -func TestUpdateFolderLegacyAndUnifiedStorage(t *testing.T) { - testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"} - testSignedInUser := &user.SignedInUser{UserID: 99, UserUID: "fdxsqt7t5ryf4a", Login: "testuser"} - - legacyFolder := folder.Folder{ - UID: "ady4yobv315a8e", - Title: "Example folder 226", - URL: "/dashboards/f/ady4yobv315a8e/example-folder-226", - CreatedBy: 99, - CreatedByUID: "fdxsqt7t5ryf4a", - Created: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC), - UpdatedBy: 99, - UpdatedByUID: "fdxsqt7t5ryf4a", - Updated: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC), - Version: 3, - } - - namespacer := func(_ int64) string { return "1" } - unifiedStorageFolder, err := conversions.LegacyFolderToUnstructured(&legacyFolder, namespacer) - require.NoError(t, err) - - expectedFolder := dtos.Folder{ - UID: legacyFolder.UID, - OrgID: 0, - Title: legacyFolder.Title, - URL: legacyFolder.URL, - HasACL: false, - CanSave: false, - CanEdit: true, - CanAdmin: false, - CanDelete: false, - CreatedBy: "testuser", - Created: legacyFolder.Created, - UpdatedBy: "testuser", - Updated: legacyFolder.Updated, - Version: legacyFolder.Version, - } - - mux := http.NewServeMux() - - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(200) - err := json.NewEncoder(w).Encode(unifiedStorageFolder) - require.NoError(t, err) - }) - mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(200) - err := json.NewEncoder(w).Encode(unifiedStorageFolder) - require.NoError(t, err) - }) - - folderApiServerMock := httptest.NewServer(mux) - defer folderApiServerMock.Close() - - t.Run("happy path", func(t *testing.T) { - type testCase struct { - description string - folderUID string - legacyFolder folder.Folder - expectedFolder dtos.Folder - expectedFolderServiceError error - unifiedStorageEnabled bool - unifiedStorageMode grafanarest.DualWriterMode - expectedCode int - } - - tcs := []testCase{ - { - description: "Happy Path - Legacy", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: false, - }, - { - description: "Happy Path - Unified storage, mode 1", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode1, - }, - { - description: "Happy Path - Unified storage, mode 2", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode2, - }, - { - description: "Happy Path - Unified storage, mode 3", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode3, - }, - { - description: "Happy Path - Unified storage, mode 4", - expectedCode: http.StatusOK, - legacyFolder: legacyFolder, - folderUID: legacyFolder.UID, - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode4, - }, - { - description: "Folder Not Found - Legacy", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: false, - expectedFolderServiceError: dashboards.ErrFolderNotFound, - }, - { - description: "Folder Not Found - Unified storage, mode 1", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode1, - }, - { - description: "Folder Not Found - Unified storage, mode 2", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode2, - }, - { - description: "Folder Not Found - Unified storage, mode 3", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode3, - }, - { - description: "Folder Not Found - Unified storage, mode 4", - expectedCode: http.StatusNotFound, - legacyFolder: legacyFolder, - folderUID: "notfound", - expectedFolder: expectedFolder, - unifiedStorageEnabled: true, - unifiedStorageMode: grafanarest.Mode4, - }, - } - - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - setUpRBACGuardian(t) - - cfg := setting.NewCfg() - cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { - DualWriterMode: tc.unifiedStorageMode, - }, - } - - featuresArr := []any{featuremgmt.FlagNestedFolders} - if tc.unifiedStorageEnabled { - featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders) - } - - server := SetupAPITestServer(t, func(hs *HTTPServer) { - hs.Cfg = cfg - hs.folderService = &foldertest.FakeService{ - ExpectedFolder: &tc.legacyFolder, - ExpectedError: tc.expectedFolderServiceError, - } - hs.QuotaService = quotatest.New(false, nil) - hs.SearchService = &mockSearchService{ - ExpectedResult: model.HitList{}, - } - hs.userService = &usertest.FakeUserService{ - ExpectedUser: testuser, - ExpectedSignedInUser: testSignedInUser, - } - hs.Features = featuremgmt.WithFeatures( - featuresArr..., - ) - hs.clientConfigProvider = mockClientConfigProvider{ - host: folderApiServerMock.URL, - } - }) - - req := server.NewRequest(http.MethodPut, fmt.Sprintf("/api/folders/%s", tc.folderUID), strings.NewReader(`{"title":"new title"}`)) - req.Header.Set("Content-Type", "application/json") - webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ - 1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, - {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("ady4yobv315a8e")}, - }), - }}) - - res, err := server.Send(req) - require.NoError(t, err) - - require.Equal(t, tc.expectedCode, res.StatusCode) - defer func() { require.NoError(t, res.Body.Close()) }() - - if tc.expectedCode == http.StatusOK { - body := dtos.Folder{} - require.NoError(t, json.NewDecoder(res.Body).Decode(&body)) - - //nolint:staticcheck - body.ID = 0 - body.Version = 0 - tc.expectedFolder.Version = 0 - require.Equal(t, tc.expectedFolder, body) - } - }) - } - }) -} - -func TestToFolderCounts(t *testing.T) { - var tests = []struct { - name string - input *unstructured.Unstructured - expected *folder.DescendantCounts - expectError bool - }{ - { - name: "with only counts from unified storage", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "folders", - "count": int64(1), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "alertRules", - "count": int64(0), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 1, - "dashboards": 3, - "alertRules": 0, - }, - }, - { - name: "with counts from both storages", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "folders", - "count": int64(1), - }, - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "sql-fallback", - "resource": "folders", - "count": int64(0), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 1, - "dashboards": 3, - }, - }, - { - name: "it uses the values from sql-fallaback if not found in unified storage", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": []interface{}{ - map[string]interface{}{ - "group": "alpha", - "resource": "dashboards", - "count": int64(3), - }, - map[string]interface{}{ - "group": "sql-fallback", - "resource": "folders", - "count": int64(2), - }, - }, - }, - }, - expected: &folder.DescendantCounts{ - "folders": 2, - "dashboards": 3, - }, - }, - { - name: "malformed input", - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "folder.grafana.app/v0alpha1", - "counts": map[string]interface{}{}, - }, - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual, err := toFolderLegacyCounts(tc.input) - if tc.expectError { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tc.expected, actual) - }) - } -} - // for now, test only the general folder func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"} @@ -971,7 +625,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { featuresArr := []any{featuremgmt.FlagNestedFolders} if tc.unifiedStorageEnabled { - featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders) + featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFoldersServiceV2) } server := SetupAPITestServer(t, func(hs *HTTPServer) { diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index fb6be0230f1..4d39d4fcd57 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -57,15 +57,6 @@ const oldAnnoKeyOriginPath = "grafana.app/originPath" const oldAnnoKeyOriginHash = "grafana.app/originHash" const oldAnnoKeyOriginTimestamp = "grafana.app/originTimestamp" -// annoKeyFullPath encodes the full path in folder resources -// revisit keeping these folder-specific annotations once we have complete support for mode 1 -// Deprecated: this goes away when folders have a better solution -const annoKeyFullPath = "grafana.app/fullPath" - -// annoKeyFullPathUIDs encodes the full path in folder resources -// Deprecated: this goes away when folders have a better solution -const annoKeyFullPathUIDs = "grafana.app/fullPathUIDs" - // ResourceRepositoryInfo is encoded into kubernetes metadata annotations. // This value identifies indicates the state of the resource in its provisioning source when // the spec was last saved. Currently this is derived from the dashboards provisioning table. @@ -140,18 +131,6 @@ type GrafanaMetaAccessor interface { // NOTE the type must match the existing value, or an error will be thrown SetStatus(any) error - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - GetFullPath() string - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - SetFullPath(path string) - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - GetFullPathUIDs() string - - // Deprecated: this is a temporary hack for folders, it will be removed without notice soon - SetFullPathUIDs(path string) - // Find a title in the object // This will reflect the object and try to get: // * spec.title @@ -706,26 +685,6 @@ func (m *grafanaMetaAccessor) SetStatus(s any) (err error) { return } -func (m *grafanaMetaAccessor) GetFullPath() string { - // nolint:staticcheck - return m.get(annoKeyFullPath) -} - -func (m *grafanaMetaAccessor) SetFullPath(path string) { - // nolint:staticcheck - m.SetAnnotation(annoKeyFullPath, path) -} - -func (m *grafanaMetaAccessor) GetFullPathUIDs() string { - // nolint:staticcheck - return m.get(annoKeyFullPathUIDs) -} - -func (m *grafanaMetaAccessor) SetFullPathUIDs(path string) { - // nolint:staticcheck - m.SetAnnotation(annoKeyFullPathUIDs, path) -} - func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { // look for Spec.Title or Spec.Name spec := m.r.FieldByName("Spec") diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 474911d1ac1..470b22ff0f6 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -2,7 +2,6 @@ package folders import ( "fmt" - "regexp" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -114,16 +113,6 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f // meta.GetUpdatedTimestamp() but it currently gets overwritten in prepareObjectForStorage(). Updated: createdTime, OrgID: orgID, - - // This will need to be restructured so the full path is looked up when saving - // it can't be saved in the resource metadata because then everything must cascade - // nolint:staticcheck - Fullpath: meta.GetFullPath(), - - // This will need to be restructured so the full path is looked up when saving - // it can't be saved in the resource metadata because then everything must cascade - // nolint:staticcheck - FullpathUIDs: meta.GetFullPathUIDs(), } // CreatedBy needs to be returned separately because it's the user UID (string) but // folder.Folder expects user ID (int64). @@ -172,14 +161,6 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) if v.ParentUID != "" { meta.SetFolder(v.ParentUID) } - if v.Fullpath != "" { - // nolint:staticcheck - meta.SetFullPath(v.Fullpath) - } - if v.FullpathUIDs != "" { - // nolint:staticcheck - meta.SetFullPathUIDs(v.FullpathUIDs) - } f.UID = gapiutil.CalculateClusterWideUID(f) return f, nil } @@ -203,22 +184,3 @@ func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) { created := meta.GetCreationTimestamp().Time return &created, nil } - -func GetParentTitles(fullPath string) ([]string, error) { - // Find all forward slashes which aren't escaped - r, err := regexp.Compile(`[^\\](/)`) - if err != nil { - return nil, err - } - indices := r.FindAllStringIndex(fullPath, -1) - - var start int - titles := []string{} - for _, i := range indices { - titles = append(titles, fullPath[start:i[0]+1]) - start = i[0] + 2 - } - - titles = append(titles, fullPath[start:]) - return titles, nil -} diff --git a/pkg/registry/apis/folders/conversions_test.go b/pkg/registry/apis/folders/conversions_test.go deleted file mode 100644 index 70310d50968..00000000000 --- a/pkg/registry/apis/folders/conversions_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package folders - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetParentTitles(t *testing.T) { - path := "get\\/folder-folder-0/get\\/folder-folder-1/another" - - titles, err := GetParentTitles(path) - require.Nil(t, err) - require.Equal(t, 3, len(titles)) - require.Equal(t, "get\\/folder-folder-0", titles[0]) - require.Equal(t, "get\\/folder-folder-1", titles[1]) - require.Equal(t, "another", titles[2]) -} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index ffc15901864..e7e163ff156 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -65,9 +65,7 @@ func RegisterAPIService(cfg *setting.Cfg, unified resource.ResourceClient, ) *FolderAPIBuilder { if !featuremgmt.AnyEnabled(features, - featuremgmt.FlagKubernetesFolders, featuremgmt.FlagKubernetesFoldersServiceV2, - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagProvisioning) { return nil // skip registration unless opting into Kubernetes folders or unless we want to customize registration when testing diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9e36b24d863..3ac4af60495 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -708,24 +708,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, }, - { - Name: "kubernetesFolders", - Description: "Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - }, { Name: "kubernetesFoldersServiceV2", Description: "Use the Folders Service V2, and route Folder Service requests to k8s", Stage: FeatureStageExperimental, Owner: grafanaSearchAndStorageSquad, }, - { - Name: "grafanaAPIServerTestingWithExperimentalAPIs", - Description: "Facilitate integration testing of experimental APIs", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - }, { Name: "datasourceQueryTypes", Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7f960ff666c..013ef1b7373 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -93,9 +93,7 @@ kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true, kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false -kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false kubernetesFoldersServiceV2,experimental,@grafana/search-and-storage,false,false,false -grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b9447726172..c9bfd15c6b7 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -383,18 +383,10 @@ const ( // Allow restoring objects in k8s FlagKubernetesRestore = "kubernetesRestore" - // FlagKubernetesFolders - // Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s - FlagKubernetesFolders = "kubernetesFolders" - // FlagKubernetesFoldersServiceV2 // Use the Folders Service V2, and route Folder Service requests to k8s FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2" - // FlagGrafanaAPIServerTestingWithExperimentalAPIs - // Facilitate integration testing of experimental APIs - FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs" - // FlagDatasourceQueryTypes // Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) FlagDatasourceQueryTypes = "datasourceQueryTypes" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 52db6dd9222..4170f739681 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -7,7 +7,7 @@ "metadata": { "name": "ABTestFeatureToggleA", "resourceVersion": "1736782112674", - "creationTimestamp": "2025-01-13T15:28:32Z" + "creationTimestamp": "2025-01-13T21:13:13Z" }, "spec": { "description": "Test feature toggle to see how cohorts could be set up AB testing", @@ -21,7 +21,7 @@ "metadata": { "name": "ABTestFeatureToggleB", "resourceVersion": "1736782112674", - "creationTimestamp": "2025-01-13T15:28:32Z" + "creationTimestamp": "2025-01-13T21:13:13Z" }, "spec": { "description": "Test feature toggle to see how cohorts could be set up AB testing", @@ -277,7 +277,7 @@ "metadata": { "name": "alertingNotificationsStepMode", "resourceVersion": "1737362059637", - "creationTimestamp": "2024-11-06T09:35:49Z", + "creationTimestamp": "2024-11-22T11:07:45Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-20 08:34:19.63725 +0000 UTC" } @@ -294,7 +294,7 @@ "metadata": { "name": "alertingPrometheusRulesPrimary", "resourceVersion": "1727332930692", - "creationTimestamp": "2024-09-09T13:56:47Z", + "creationTimestamp": "2024-09-27T12:27:16Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-26 06:42:10.692959 +0000 UTC" } @@ -371,7 +371,7 @@ "metadata": { "name": "alertingUIOptimizeReducer", "resourceVersion": "1731923458730", - "creationTimestamp": "2024-11-18T09:06:02Z", + "creationTimestamp": "2024-11-18T10:59:00Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-18 09:50:58.730825 +0000 UTC" } @@ -458,7 +458,7 @@ "name": "appPlatformAccessTokens", "resourceVersion": "1725549369316", "creationTimestamp": "2024-09-05T16:18:44Z", - "deletionTimestamp": "2024-10-14T13:14:46Z" + "deletionTimestamp": "2024-10-14T10:47:18Z" }, "spec": { "description": "Enables the use of access tokens for the App Platform", @@ -472,7 +472,7 @@ "metadata": { "name": "appPlatformGrpcClientAuth", "resourceVersion": "1728662061076", - "creationTimestamp": "2024-10-11T15:54:21Z" + "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { "description": "Enables the gRPC client to authenticate with the App Platform by using ID \u0026 access tokens", @@ -608,7 +608,7 @@ "name": "autoMigrateXYChartPanel", "resourceVersion": "1722537244598", "creationTimestamp": "2024-03-22T15:44:37Z", - "deletionTimestamp": "2024-11-14T01:17:06Z", + "deletionTimestamp": "2024-11-14T16:36:18Z", "annotations": { "grafana.app/updatedTimestamp": "2024-08-01 18:34:04.598082 +0000 UTC" } @@ -684,7 +684,7 @@ "metadata": { "name": "azureMonitorDisableLogLimit", "resourceVersion": "1727698096407", - "creationTimestamp": "2024-09-30T11:51:51Z", + "creationTimestamp": "2024-10-24T13:32:09Z", "deletionTimestamp": "2024-10-22T09:44:12Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-30 12:08:16.407109 +0000 UTC" @@ -701,7 +701,7 @@ "metadata": { "name": "azureMonitorEnableUserAuth", "resourceVersion": "1732189410576", - "creationTimestamp": "2024-11-21T11:42:29Z", + "creationTimestamp": "2024-11-27T14:01:54Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-21 11:43:30.576196 +0000 UTC" } @@ -902,7 +902,7 @@ "name": "cloudwatchMetricInsightsCrossAccount", "resourceVersion": "1729265619643", "creationTimestamp": "2024-07-02T10:34:12Z", - "deletionTimestamp": "2025-01-10T15:06:19Z", + "deletionTimestamp": "2025-01-10T22:23:23Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-18 15:33:39.643165 +0000 UTC" } @@ -950,7 +950,7 @@ "metadata": { "name": "crashDetection", "resourceVersion": "1730381712885", - "creationTimestamp": "2024-10-31T13:35:12Z" + "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { "description": "Enables browser crash detection reporting to Faro.", @@ -963,7 +963,7 @@ "metadata": { "name": "dashboardNewLayouts", "resourceVersion": "1729671312626", - "creationTimestamp": "2024-10-16T08:44:05Z", + "creationTimestamp": "2024-10-23T08:55:45Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-23 08:15:12.626632 +0000 UTC" } @@ -997,7 +997,7 @@ "name": "dashboardRestoreUI", "resourceVersion": "1720021873452", "creationTimestamp": "2024-06-25T14:43:13Z", - "deletionTimestamp": "2024-10-08T14:24:51Z", + "deletionTimestamp": "2024-10-11T08:29:58Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -1064,8 +1064,8 @@ "metadata": { "name": "dashboardSchemaV2", "resourceVersion": "1730192092473", - "creationTimestamp": "2024-10-29T08:54:52Z", - "deletionTimestamp": "2024-12-19T12:03:44Z" + "creationTimestamp": "2024-10-29T10:35:18Z", + "deletionTimestamp": "2024-12-19T12:28:20Z" }, "spec": { "description": "Enables the new dashboard schema version 2, implementing changes necessary for dynamic dashboards and dashboards as code.", @@ -1159,7 +1159,7 @@ "metadata": { "name": "datasourceConnectionsTab", "resourceVersion": "1737049826022", - "creationTimestamp": "2025-01-16T17:36:09Z", + "creationTimestamp": "2025-01-21T17:39:48Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-16 17:50:26.022636488 +0000 UTC" } @@ -1331,7 +1331,7 @@ "metadata": { "name": "elasticsearchCrossClusterSearch", "resourceVersion": "1733848475752", - "creationTimestamp": "2024-12-09T13:53:38Z", + "creationTimestamp": "2024-12-12T22:20:04Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:34:35.752111 +0000 UTC" } @@ -1346,7 +1346,7 @@ "metadata": { "name": "elasticsearchImprovedParsing", "resourceVersion": "1736808262603", - "creationTimestamp": "2025-01-13T20:32:35Z", + "creationTimestamp": "2025-01-15T17:05:54Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-13 22:44:22.603729 +0000 UTC" } @@ -1374,7 +1374,7 @@ "metadata": { "name": "enableExtensionsAdminPage", "resourceVersion": "1730819353237", - "creationTimestamp": "2024-11-05T09:18:42Z", + "creationTimestamp": "2024-11-05T15:55:10Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-05 15:09:13.237578 +0000 UTC" } @@ -1404,7 +1404,7 @@ "metadata": { "name": "enableSCIM", "resourceVersion": "1730980484343", - "creationTimestamp": "2024-11-07T11:54:44Z" + "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { "description": "Enables SCIM support for user and group management", @@ -1416,7 +1416,7 @@ "metadata": { "name": "enableScopesInMetricsExplore", "resourceVersion": "1729765731452", - "creationTimestamp": "2024-10-24T10:28:51Z" + "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { "description": "Enables the scopes usage in Metrics Explore", @@ -1501,7 +1501,7 @@ "metadata": { "name": "exploreMetricsRelatedLogs", "resourceVersion": "1730125602673", - "creationTimestamp": "2024-10-28T14:26:42Z" + "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { "description": "Display Related Logs in Explore Metrics", @@ -1635,7 +1635,7 @@ "metadata": { "name": "feedbackButton", "resourceVersion": "1733158016122", - "creationTimestamp": "2024-12-02T16:46:56Z" + "creationTimestamp": "2024-12-02T17:08:15Z" }, "spec": { "description": "Enables a button to send feedback from the Grafana UI", @@ -1734,7 +1734,8 @@ "metadata": { "name": "grafanaAPIServerTestingWithExperimentalAPIs", "resourceVersion": "1727945615419", - "creationTimestamp": "2024-10-03T08:53:35Z" + "creationTimestamp": "2024-10-03T10:11:40Z", + "deletionTimestamp": "2025-01-22T20:53:53Z" }, "spec": { "description": "Facilitate integration testing of experimental APIs", @@ -1763,7 +1764,7 @@ "metadata": { "name": "grafanaAdvisor", "resourceVersion": "1737365459765", - "creationTimestamp": "2025-01-20T09:30:59Z" + "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { "description": "Enables Advisor app", @@ -1897,7 +1898,7 @@ "metadata": { "name": "improvedExternalSessionHandlingSAML", "resourceVersion": "1737370880023", - "creationTimestamp": "2025-01-09T16:33:07Z", + "creationTimestamp": "2025-01-09T17:02:49Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-20 11:01:20.02358 +0000 UTC" } @@ -1973,7 +1974,7 @@ "metadata": { "name": "investigationsBackend", "resourceVersion": "1734447689720", - "creationTimestamp": "2024-12-17T15:01:29Z" + "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { "description": "Enable the investigations backend API", @@ -2000,7 +2001,7 @@ "metadata": { "name": "jaegerBackendMigration", "resourceVersion": "1731599633815", - "creationTimestamp": "2024-11-14T15:53:53Z" + "creationTimestamp": "2024-11-15T14:40:20Z" }, "spec": { "description": "Enables querying the Jaeger data source without the proxy", @@ -2026,7 +2027,7 @@ "metadata": { "name": "k8SFolderCounts", "resourceVersion": "1735294794086", - "creationTimestamp": "2024-12-27T10:19:54Z" + "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { "description": "Enable folder's api server counts", @@ -2039,7 +2040,7 @@ "metadata": { "name": "k8SFolderMove", "resourceVersion": "1735294794086", - "creationTimestamp": "2024-12-27T10:19:54Z" + "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { "description": "Enable folder's api server move", @@ -2081,7 +2082,7 @@ "metadata": { "name": "kubernetesCliDashboards", "resourceVersion": "1733520389522", - "creationTimestamp": "2024-12-06T21:26:29Z" + "creationTimestamp": "2024-12-13T22:55:43Z" }, "spec": { "description": "Use the k8s client to retrieve dashboards internally", @@ -2121,6 +2122,7 @@ "name": "kubernetesFolders", "resourceVersion": "1725863636605", "creationTimestamp": "2024-09-10T09:22:08Z", + "deletionTimestamp": "2025-01-22T20:49:15Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-09 06:33:56.605329 +0000 UTC" } @@ -2135,7 +2137,7 @@ "metadata": { "name": "kubernetesFoldersServiceV2", "resourceVersion": "1735336477446", - "creationTimestamp": "2024-12-27T21:54:37Z" + "creationTimestamp": "2025-01-13T21:15:35Z" }, "spec": { "description": "Use the Folders Service V2, and route Folder Service requests to k8s", @@ -2165,7 +2167,7 @@ "metadata": { "name": "kubernetesRestore", "resourceVersion": "1735880498698", - "creationTimestamp": "2025-01-03T05:01:38Z" + "creationTimestamp": "2025-01-03T14:48:47Z" }, "spec": { "description": "Allow restoring objects in k8s", @@ -2219,7 +2221,7 @@ "metadata": { "name": "logQLScope", "resourceVersion": "1730842404843", - "creationTimestamp": "2024-11-05T21:33:24Z" + "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { "description": "In-development feature that will allow injection of labels into loki queries.", @@ -2338,7 +2340,7 @@ "metadata": { "name": "lokiLabelNamesQueryApi", "resourceVersion": "1734096677730", - "creationTimestamp": "2024-12-13T13:31:17Z" + "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { "description": "Defaults to using the Loki `/labels` API instead of `/series`", @@ -2364,7 +2366,7 @@ "name": "lokiMetricDataplane", "resourceVersion": "1720021873452", "creationTimestamp": "2023-04-13T13:07:08Z", - "deletionTimestamp": "2024-08-21T13:49:48Z", + "deletionTimestamp": "2024-11-26T16:32:17Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -2469,7 +2471,7 @@ "metadata": { "name": "lokiShardSplitting", "resourceVersion": "1729678036788", - "creationTimestamp": "2024-10-23T10:06:42Z", + "creationTimestamp": "2024-10-23T11:21:03Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-23 10:07:16.788828 +0000 UTC" } @@ -2660,7 +2662,7 @@ "name": "notificationBanner", "resourceVersion": "1727777007488", "creationTimestamp": "2024-05-13T09:32:34Z", - "deletionTimestamp": "2025-01-10T06:02:47Z", + "deletionTimestamp": "2025-01-10T10:18:43Z", "annotations": { "grafana.app/updatedTimestamp": "2024-10-01 10:03:27.48823 +0000 UTC" } @@ -2706,8 +2708,8 @@ "metadata": { "name": "onPremToCloudMigrationsAlerts", "resourceVersion": "1728048163201", - "creationTimestamp": "2024-10-04T13:22:43Z", - "deletionTimestamp": "2024-12-02T13:37:41Z" + "creationTimestamp": "2024-10-07T10:53:24Z", + "deletionTimestamp": "2024-12-17T11:56:18Z" }, "spec": { "description": "Enables the migration of alerts and its child resources to your Grafana Cloud stack. Requires `onPremToCloudMigrations` to be enabled in conjunction.", @@ -2719,7 +2721,7 @@ "metadata": { "name": "onPremToCloudMigrationsAuthApiMig", "resourceVersion": "1732033809064", - "creationTimestamp": "2024-11-19T16:30:09Z" + "creationTimestamp": "2024-11-21T18:46:06Z" }, "spec": { "description": "Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction.", @@ -2795,7 +2797,7 @@ "name": "panelTitleSearchInV1", "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-13T12:04:24Z", - "deletionTimestamp": "2025-01-20T20:07:05Z" + "deletionTimestamp": "2025-01-21T09:59:32Z" }, "spec": { "description": "Enable searching for dashboards using panel title in search v1", @@ -2809,7 +2811,7 @@ "name": "passScopeToDashboardApi", "resourceVersion": "1718290335877", "creationTimestamp": "2024-06-20T15:49:19Z", - "deletionTimestamp": "2024-10-14T10:53:41Z" + "deletionTimestamp": "2024-10-25T12:56:54Z" }, "spec": { "description": "Enables the passing of scopes to dashboards fetching in Grafana", @@ -2823,7 +2825,7 @@ "metadata": { "name": "passwordlessMagicLinkAuthentication", "resourceVersion": "1730232874003", - "creationTimestamp": "2024-10-29T20:14:34Z" + "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { "description": "Enable passwordless login via magic link authentication", @@ -2877,7 +2879,7 @@ "metadata": { "name": "playlistsReconciler", "resourceVersion": "1734463170112", - "creationTimestamp": "2024-11-01T12:08:30Z", + "creationTimestamp": "2024-12-20T03:09:31Z", "deletionTimestamp": "2024-12-19T19:17:00Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-17 19:19:30.112629 +0000 UTC" @@ -2966,7 +2968,7 @@ "metadata": { "name": "pluginsSriChecks", "resourceVersion": "1727785264632", - "creationTimestamp": "2024-10-01T12:21:04Z" + "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { "description": "Enables SRI checks for plugin assets", @@ -2978,7 +2980,7 @@ "metadata": { "name": "preinstallAutoUpdate", "resourceVersion": "1731581146864", - "creationTimestamp": "2024-11-06T14:45:43Z", + "creationTimestamp": "2024-11-07T12:14:25Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-14 10:45:46.864585 +0000 UTC" } @@ -3058,7 +3060,7 @@ "name": "prometheusConfigOverhaulAuth", "resourceVersion": "1720021873452", "creationTimestamp": "2023-07-26T16:09:53Z", - "deletionTimestamp": "2025-01-02T20:43:41Z", + "deletionTimestamp": "2025-01-02T21:19:11Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3107,7 +3109,7 @@ "name": "prometheusMetricEncyclopedia", "resourceVersion": "1720021873452", "creationTimestamp": "2023-03-07T18:41:05Z", - "deletionTimestamp": "2024-12-30T14:42:45Z", + "deletionTimestamp": "2024-12-30T21:16:04Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3156,7 +3158,7 @@ "metadata": { "name": "prometheusSpecialCharsInLabelValues", "resourceVersion": "1735845919509", - "creationTimestamp": "2024-12-12T23:52:48Z", + "creationTimestamp": "2024-12-18T21:31:08Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 19:25:19.509884 +0000 UTC" } @@ -3172,7 +3174,7 @@ "metadata": { "name": "prometheusUsesCombobox", "resourceVersion": "1735845919509", - "creationTimestamp": "2024-09-12T11:19:18Z", + "creationTimestamp": "2024-10-23T11:18:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 19:25:19.509884 +0000 UTC" } @@ -3187,7 +3189,7 @@ "metadata": { "name": "provisioning", "resourceVersion": "1732265054297", - "creationTimestamp": "2024-11-22T08:44:14Z" + "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { "description": "Next generation provisioning... and git", @@ -3201,7 +3203,7 @@ "name": "publicDashboards", "resourceVersion": "1720021873452", "creationTimestamp": "2022-04-07T18:30:19Z", - "deletionTimestamp": "2024-11-15T16:38:53Z", + "deletionTimestamp": "2024-11-20T14:36:19Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3265,7 +3267,7 @@ "metadata": { "name": "queryLibraryDashboards", "resourceVersion": "1736850377404", - "creationTimestamp": "2025-01-14T10:24:54Z", + "creationTimestamp": "2025-01-14T11:01:15Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-14 10:26:17.404592 +0000 UTC" } @@ -3394,7 +3396,7 @@ "metadata": { "name": "reloadDashboardsOnParamsChange", "resourceVersion": "1728903221522", - "creationTimestamp": "2024-10-14T10:53:41Z" + "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { "description": "Enables reload of dashboards on scopes, time range and variables changes", @@ -3434,7 +3436,7 @@ "metadata": { "name": "reportingUseRawTimeRange", "resourceVersion": "1735810729877", - "creationTimestamp": "2024-11-13T15:48:28Z", + "creationTimestamp": "2024-11-14T20:08:03Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-02 09:38:49.877519888 +0000 UTC" } @@ -3450,7 +3452,7 @@ "metadata": { "name": "rolePickerDrawer", "resourceVersion": "1727337187819", - "creationTimestamp": "2024-09-26T07:53:07Z" + "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { "description": "Enables the new role picker drawer design", @@ -3476,7 +3478,7 @@ "metadata": { "name": "scopeApi", "resourceVersion": "1732690644377", - "creationTimestamp": "2024-11-27T06:57:24Z" + "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { "description": "In-development feature flag for the scope api using the app platform.", @@ -3517,7 +3519,7 @@ "name": "singleTopNav", "resourceVersion": "1732104041490", "creationTimestamp": "2024-08-29T08:48:32Z", - "deletionTimestamp": "2024-12-13T11:25:25Z", + "deletionTimestamp": "2024-12-17T13:32:38Z", "annotations": { "grafana.app/updatedTimestamp": "2024-11-20 12:00:41.490792 +0000 UTC" } @@ -3560,7 +3562,7 @@ "metadata": { "name": "sqlQuerybuilderFunctionParameters", "resourceVersion": "1718487716739", - "creationTimestamp": "2024-06-15T21:41:56Z" + "creationTimestamp": "2024-11-04T16:13:35Z" }, "spec": { "description": "Enables SQL query builder function parameters", @@ -3676,7 +3678,7 @@ "metadata": { "name": "teamHttpHeadersMimir", "resourceVersion": "1736763800062", - "creationTimestamp": "2025-01-13T10:23:20Z" + "creationTimestamp": "2025-01-13T10:42:47Z" }, "spec": { "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", @@ -3688,7 +3690,7 @@ "metadata": { "name": "timeRangeProvider", "resourceVersion": "1728565214224", - "creationTimestamp": "2024-10-10T13:00:14Z" + "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { "description": "Enables time pickers sync", @@ -3717,7 +3719,7 @@ "name": "topnav", "resourceVersion": "1720021873452", "creationTimestamp": "2022-06-20T14:25:43Z", - "deletionTimestamp": "2024-10-15T15:00:51Z", + "deletionTimestamp": "2024-10-17T09:18:30Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } @@ -3785,7 +3787,7 @@ "metadata": { "name": "unifiedHistory", "resourceVersion": "1734085219453", - "creationTimestamp": "2024-12-13T10:20:19Z" + "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { "description": "Displays the navigation history so the user can navigate back to previous pages", @@ -3845,7 +3847,7 @@ "metadata": { "name": "unifiedStorageBigObjectsSupport", "resourceVersion": "1728994158474", - "creationTimestamp": "2024-10-15T12:09:18Z" + "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { "description": "Enables to save big objects in blob storage", @@ -3857,7 +3859,7 @@ "metadata": { "name": "unifiedStorageSearch", "resourceVersion": "1726771421439", - "creationTimestamp": "2024-09-19T18:43:41Z" + "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { "description": "Enable unified storage search", @@ -3871,7 +3873,7 @@ "metadata": { "name": "unifiedStorageSearch", "resourceVersion": "1726771421439", - "creationTimestamp": "2024-09-19T18:43:41Z", + "creationTimestamp": "2024-09-30T19:46:14Z", "deletionTimestamp": "2024-10-11T14:56:04Z" }, "spec": { @@ -3886,7 +3888,7 @@ "metadata": { "name": "unifiedStorageSearchPermissionFiltering", "resourceVersion": "1737489629408", - "creationTimestamp": "2025-01-21T20:00:29Z" + "creationTimestamp": "2025-01-22T11:38:37Z" }, "spec": { "description": "Enable permission filtering on unified storage search", @@ -3900,7 +3902,7 @@ "metadata": { "name": "unifiedStorageSearchSprinkles", "resourceVersion": "1734563607668", - "creationTimestamp": "2024-12-18T23:13:27Z" + "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { "description": "Enable sprinkles on unified storage search", @@ -3914,7 +3916,7 @@ "metadata": { "name": "unifiedStorageSearchUI", "resourceVersion": "1734563607668", - "creationTimestamp": "2024-12-10T21:28:55Z", + "creationTimestamp": "2024-12-19T18:21:48Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-18 23:13:27.66802 +0000 UTC" } @@ -3960,7 +3962,7 @@ "metadata": { "name": "useV2DashboardsAPI", "resourceVersion": "1732535420861", - "creationTimestamp": "2024-11-25T11:50:20Z" + "creationTimestamp": "2024-12-17T21:17:09Z" }, "spec": { "description": "Use the v2 kubernetes API in the frontend for dashboards", @@ -3973,7 +3975,7 @@ "metadata": { "name": "userStorageAPI", "resourceVersion": "1736438999910", - "creationTimestamp": "2024-10-29T12:18:41Z", + "creationTimestamp": "2024-11-12T11:56:41Z", "annotations": { "grafana.app/updatedTimestamp": "2025-01-09 16:09:59.910083 +0000 UTC" } @@ -4004,7 +4006,7 @@ "name": "vizAndWidgetSplit", "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-27T10:22:13Z", - "deletionTimestamp": "2024-10-30T14:21:33Z" + "deletionTimestamp": "2024-10-30T16:12:03Z" }, "spec": { "description": "Split panels between visualizations and widgets", @@ -4043,7 +4045,7 @@ "metadata": { "name": "zipkinBackendMigration", "resourceVersion": "1733846643829", - "creationTimestamp": "2024-11-06T15:39:38Z", + "creationTimestamp": "2024-11-07T09:35:53Z", "annotations": { "grafana.app/updatedTimestamp": "2024-12-10 16:04:03.82919 +0000 UTC" } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 8d86292c758..85664cda390 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -11,12 +11,13 @@ import ( "sync" "time" - "github.com/grafana/dskit/concurrency" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/exp/slices" + "github.com/grafana/dskit/concurrency" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/bus" @@ -315,10 +316,6 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol f.FullpathUIDs = f.UID // set full path to the folder UID } - if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) { - f, err = s.setFullpath(ctx, f, q.SignedInUser, true) - } - return f, err } @@ -784,13 +781,6 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm f.ParentUID = nestedFolder.ParentUID } - if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) { - f, err = s.setFullpath(ctx, f, user, true) - if err != nil { - return nil, err - } - } - return f, nil } diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 5ec969918c4..98b713cf0fa 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -200,7 +200,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { } featuresArr := []any{ - featuremgmt.FlagKubernetesFolders, featuremgmt.FlagKubernetesFoldersServiceV2} features := featuremgmt.WithFeatures(featuresArr...) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 2fe65677c64..4b30d9428cf 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -49,10 +49,8 @@ func TestIntegrationFoldersApp(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). }) t.Run("Check discovery client", func(t *testing.T) { @@ -125,10 +123,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -143,10 +139,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, }, - // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). - // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -161,9 +155,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -179,9 +172,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -197,9 +189,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -215,9 +206,8 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, + featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, }, })) }) @@ -421,7 +411,7 @@ func doNestedCreateTest(t *testing.T, helper *apis.K8sTestHelper) { // creating a folder with a known parent should succeed require.Equal(t, parentUID, childCreate.Result.ParentUID) require.Equal(t, parentUID, parent.UID) - require.Equal(t, "Test\\/parent", parent.Title) + require.Equal(t, "Test/parent", parent.Title) require.Equal(t, parentCreate.Result.URL, parent.URL) } @@ -502,6 +492,7 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + t.Skip("not working yet") folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" @@ -585,9 +576,8 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -627,6 +617,7 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + t.Skip("not yet working") type testCase struct { description string @@ -687,9 +678,8 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -865,9 +855,8 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) @@ -1036,9 +1025,8 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagKubernetesFoldersServiceV2, }, }) From f6202f59d48eb0bb012b7009b5e0e95cd0b1db14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:38:27 +0000 Subject: [PATCH 08/29] Update dependency rollup to v4.31.0 (#99449) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 158 +++++++++++++++++++++++++++--------------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96111fd4d26..1f0d402feb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6551,135 +6551,135 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.28.1" +"@rollup/rollup-android-arm-eabi@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.31.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm64@npm:4.28.1" +"@rollup/rollup-android-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-android-arm64@npm:4.31.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.28.1" +"@rollup/rollup-darwin-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.31.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.28.1" +"@rollup/rollup-darwin-x64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.31.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.1" +"@rollup/rollup-freebsd-arm64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.31.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.28.1" +"@rollup/rollup-freebsd-x64@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.31.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.31.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.31.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.28.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.31.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.28.1" +"@rollup/rollup-linux-arm64-musl@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.31.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.31.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.31.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.28.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.31.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.28.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.31.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.28.1" +"@rollup/rollup-linux-x64-gnu@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.31.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.28.1" +"@rollup/rollup-linux-x64-musl@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.31.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.28.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.31.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.28.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.31.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.28.1" +"@rollup/rollup-win32-x64-msvc@npm:4.31.0": + version: 4.31.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.31.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -26662,28 +26662,28 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.28.1 - resolution: "rollup@npm:4.28.1" + version: 4.31.0 + resolution: "rollup@npm:4.31.0" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.28.1" - "@rollup/rollup-android-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-x64": "npm:4.28.1" - "@rollup/rollup-freebsd-arm64": "npm:4.28.1" - "@rollup/rollup-freebsd-x64": "npm:4.28.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.28.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.28.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-musl": "npm:4.28.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.28.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.28.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.28.1" + "@rollup/rollup-android-arm-eabi": "npm:4.31.0" + "@rollup/rollup-android-arm64": "npm:4.31.0" + "@rollup/rollup-darwin-arm64": "npm:4.31.0" + "@rollup/rollup-darwin-x64": "npm:4.31.0" + "@rollup/rollup-freebsd-arm64": "npm:4.31.0" + "@rollup/rollup-freebsd-x64": "npm:4.31.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.31.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.31.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.31.0" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.31.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.31.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.31.0" + "@rollup/rollup-linux-x64-musl": "npm:4.31.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.31.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.31.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.31.0" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -26729,7 +26729,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/4337898d07e646835b52494b43b4ccd6929da87af2b0febc05ab217fd2425cfda05af5efaea6037c1641c90d803eb5b3e491eefdd47b28fda85af4f46a0dad34 + checksum: 10/4f5fac0a0df7878ca810512c283df0e81b21d42fed262943b412c488a30beceb0149a4be36dbf2750b6c5cbfa4d4cf5097a134266f1425a9e213c2a2a09853fc languageName: node linkType: hard From 5ca24fde024af57c04682f4d1c0bccd05af2b783 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:48:06 +0100 Subject: [PATCH 09/29] AutoSizeInput: Improve performance when typing (#99443) * AutoSizeInput: Fix performance issue * Add comments * Fix a little oopsie --- packages/grafana-ui/src/components/Input/Input.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 1240569cb57..09b113cf76b 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -64,12 +64,17 @@ export const Input = forwardRef((props, ref) => { const theme = useTheme2(); - const styles = getInputStyles({ theme, invalid: !!invalid, width: finalWidth }); + // Don't pass the width prop, as this causes an unnecessary amount of Emotion calls when auto sizing + const styles = getInputStyles({ theme, invalid: !!invalid }); const suffix = suffixProp || (loading && ); return ( -
+
{!!addonBefore &&
{addonBefore}
}
{prefix && ( @@ -125,7 +130,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: css({ label: 'input-wrapper', display: 'flex', - width: width ? theme.spacing(width) : '100%', + width: width ? theme.spacing(width) : '100%', // Not used in Input, as this causes performance issues with auto sizing height: theme.spacing(theme.components.height.md), borderRadius: theme.shape.radius.default, '&:hover': { From 05015a57b3de236b672221b6ddb09063117b9c7a Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 23 Jan 2025 15:52:03 +0100 Subject: [PATCH 10/29] Chore: Upgrade authlib (#99447) --- go.mod | 2 +- go.sum | 4 ++-- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 ++-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e4140a19173..d57b3cb3d61 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 2198c1a9589..166125e556b 100644 --- a/go.sum +++ b/go.sum @@ -1500,8 +1500,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 12e69bb5cd9..3c8273750a3 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.1 require ( - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.0 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 1dc1eff7033..5957167466c 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -32,8 +32,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 1936abf4697..bb4398f1a66 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -171,7 +171,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // indirect - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // indirect + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index b82db37d6e4..478fa431e69 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -549,8 +549,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 554dbe9c4ca..8f0adf19785 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -11,7 +11,7 @@ replace ( require ( github.com/fullstorydev/grpchan v1.1.1 github.com/google/uuid v1.6.0 - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 65f2d1de918..ef047967788 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -405,8 +405,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= From 7d2eb83cbdbe362f0d473c3019cd75c9ecc6a338 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:11:05 +0000 Subject: [PATCH 11/29] Update dependency @playwright/test to v1.50.0 (#99452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 21488de372d..d43c76f4903 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@grafana/plugin-e2e": "^1.11.0", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", - "@playwright/test": "1.49.1", + "@playwright/test": "1.50.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@react-types/button": "3.10.2", "@react-types/menu": "3.9.14", diff --git a/yarn.lock b/yarn.lock index 1f0d402feb9..765a1f74a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5929,14 +5929,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:1.49.1": - version: 1.49.1 - resolution: "@playwright/test@npm:1.49.1" +"@playwright/test@npm:1.50.0": + version: 1.50.0 + resolution: "@playwright/test@npm:1.50.0" dependencies: - playwright: "npm:1.49.1" + playwright: "npm:1.50.0" bin: playwright: cli.js - checksum: 10/bb0d5eda58ee0b5bbca732d2aa57782fadf420d101e08e16d5760179459c667907bd8d224ee3d6f43f3088378e377ef63d32ed605fec37605debf217c3efe8da + checksum: 10/1fec2ed986205b57b03f24392bb01c6454c1f0a5c14204ce921afd51c3f5d61f20eddb3a18d36a02b19b3e3d731c7ff6bb7ba3c622aabc8fa3802021aef7d21b languageName: node linkType: hard @@ -17388,7 +17388,7 @@ __metadata: "@opentelemetry/api": "npm:1.9.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.28.0" - "@playwright/test": "npm:1.49.1" + "@playwright/test": "npm:1.50.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.5.15" "@popperjs/core": "npm:2.11.8" "@react-aria/dialog": "npm:3.5.21" @@ -23732,27 +23732,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.49.1": - version: 1.49.1 - resolution: "playwright-core@npm:1.49.1" +"playwright-core@npm:1.50.0": + version: 1.50.0 + resolution: "playwright-core@npm:1.50.0" bin: playwright-core: cli.js - checksum: 10/baa39a53024ec7744708410f2b952ac3aa2e1a6d311dabfa303523712848eba142fce5c20f1b2ed2a66fbd9a415d22ea8642b0f70423360aaebd4b41c47d364e + checksum: 10/0d27e52164bcc37ed5aeaa0c7efa6a0b3616cfbb01e206c26572bff8b8e5f0923a993369c826056cd7bee4b975508a1ec257b533098ee9db7bc5b75832110e4d languageName: node linkType: hard -"playwright@npm:1.49.1": - version: 1.49.1 - resolution: "playwright@npm:1.49.1" +"playwright@npm:1.50.0": + version: 1.50.0 + resolution: "playwright@npm:1.50.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.49.1" + playwright-core: "npm:1.50.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10/49fb063f4a107b8090f66d2d351ebd51fbb66843a8f95a161fa0c0e0b5156515961e75cc10f4249f61b9d2af51f762dda505c62b096d8f61cd47d1ff73ab39d2 + checksum: 10/53521f05c48ab51a37d6fa280a7c1e6486e2879f9997e877227517945faf195ce16829cf144709bba292c3023bcd07cf44a4dd965458c9adc30ea6fbe1f0f74a languageName: node linkType: hard From b066a6313173405eb865648804518092051043b7 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 23 Jan 2025 16:19:50 +0100 Subject: [PATCH 12/29] Grafana Advisor: Datasource checks (#99313) --- apps/advisor/kinds/check.cue | 6 + apps/advisor/pkg/apis/advisor_manifest.go | 10 +- apps/advisor/pkg/app/app.go | 46 +++++++ .../pkg/app/checkregistry/checkregistry.go | 42 ++++++ .../pkg/app/checks/datasourcecheck/check.go | 96 ++++++++++++++ .../app/checks/datasourcecheck/check_test.go | 114 ++++++++++++++++ apps/advisor/pkg/app/checks/ifaces.go | 13 ++ apps/advisor/pkg/app/utils.go | 95 ++++++++++++++ apps/advisor/pkg/app/utils_test.go | 124 ++++++++++++++++++ pkg/registry/apps/advisor/register.go | 6 +- pkg/registry/apps/wireset.go | 3 + 11 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 apps/advisor/pkg/app/checkregistry/checkregistry.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/check.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/check_test.go create mode 100644 apps/advisor/pkg/app/checks/ifaces.go create mode 100644 apps/advisor/pkg/app/utils.go create mode 100644 apps/advisor/pkg/app/utils_test.go diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index 4ab33fb05df..cf1cfd58d39 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -10,6 +10,12 @@ check: { frontend: false backend: true } + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } schema: { spec: { // Generic data input that a check can receive diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 2e8bb06fb65..f62ea3addb8 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -27,7 +27,15 @@ var appManifestData = app.ManifestData{ Conversion: false, Versions: []app.ManifestKindVersion{ { - Name: "v0alpha1", + Name: "v0alpha1", + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, Schema: &versionSchemaCheckv0alpha1, }, }, diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index e4ccafbcaaf..42848758d5a 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -2,16 +2,44 @@ package app import ( "context" + "fmt" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" ) +const ( + typeLabel = "advisor.grafana.app/type" + statusAnnotation = "advisor.grafana.app/status" +) + func New(cfg app.Config) (app.App, error) { + // Read config + checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + + // Prepare storage client + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) + client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckKind()) + if err != nil { + return nil, err + } + + // Initialize checks + checkMap := map[string]checks.Check{} + for _, c := range checkRegistry.Checks() { + checkMap[c.Type()] = c + } + simpleConfig := simple.AppConfig{ Name: "advisor", KubeConfig: cfg.KubeConfig, @@ -23,6 +51,24 @@ func New(cfg app.Config) (app.App, error) { ManagedKinds: []simple.AppManagedKind{ { Kind: advisorv0alpha1.CheckKind(), + Validator: &simple.Validator{ + ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + if req.Object != nil { + _, err := getCheck(req.Object, checkMap) + return err + } + return nil + }, + }, + Watcher: &simple.Watcher{ + AddFunc: func(ctx context.Context, obj resource.Object) error { + check, err := getCheck(obj, checkMap) + if err != nil { + return err + } + return processCheck(ctx, client, obj, check) + }, + }, }, }, } diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go new file mode 100644 index 00000000000..0c375f035c5 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -0,0 +1,42 @@ +package checkregistry + +import ( + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type CheckService interface { + Checks() []checks.Check +} + +type Service struct { + datasourceSvc datasources.DataSourceService + pluginStore pluginstore.Store + pluginContextProvider datasource.PluginContextWrapper + pluginClient plugins.Client +} + +func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client) *Service { + return &Service{ + datasourceSvc: datasourceSvc, + pluginStore: pluginStore, + pluginContextProvider: pluginContextProvider, + pluginClient: pluginClient, + } +} + +func (s *Service) Checks() []checks.Check { + return []checks.Check{ + datasourcecheck.New( + s.datasourceSvc, + s.pluginStore, + s.pluginContextProvider, + s.pluginClient, + ), + } +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go new file mode 100644 index 00000000000..7aede0c65ec --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -0,0 +1,96 @@ +package datasourcecheck + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/util" + "k8s.io/klog/v2" +) + +func New( + datasourceSvc datasources.DataSourceService, + pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, + pluginClient plugins.Client, +) checks.Check { + return &check{ + DatasourceSvc: datasourceSvc, + PluginStore: pluginStore, + PluginContextProvider: pluginContextProvider, + PluginClient: pluginClient, + } +} + +type check struct { + DatasourceSvc datasources.DataSourceService + PluginStore pluginstore.Store + PluginContextProvider datasource.PluginContextWrapper + PluginClient plugins.Client +} + +func (c *check) Type() string { + return "datasource" +} + +func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { + // Optionally read the check input encoded in the object + // fmt.Println(obj.Data) + + dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + if err != nil { + return nil, err + } + + dsErrs := []advisor.CheckV0alpha1StatusReportErrors{} + for _, ds := range dss { + // Data source UID validation + err := util.ValidateUID(ds.UID) + if err != nil { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityLow, + Reason: fmt.Sprintf("Invalid UID: %s", ds.UID), + Action: "Change UID", + }) + } + + // Health check execution + pCtx, err := c.PluginContextProvider.PluginContextForDataSource(ctx, &backend.DataSourceInstanceSettings{ + Type: ds.Type, + UID: ds.UID, + APIVersion: ds.APIVersion, + }) + if err != nil { + klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name) + continue + } + req := &backend.CheckHealthRequest{ + PluginContext: pCtx, + Headers: map[string]string{}, + } + resp, err := c.PluginClient.CheckHealth(ctx, req) + if err != nil { + fmt.Println("Error checking health", err) + continue + } + if resp.Status != backend.HealthStatusOk { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityHigh, + Reason: fmt.Sprintf("Health check failed: %s", ds.Name), + Action: "Check datasource", + }) + } + } + + return &advisor.CheckV0alpha1StatusReport{ + Count: int64(len(dss)), + Errors: dsErrs, + }, nil +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go new file mode 100644 index 00000000000..ee489483bd2 --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -0,0 +1,114 @@ +package datasourcecheck + +import ( + "context" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/stretchr/testify/assert" +) + +func TestCheck_Run(t *testing.T) { + t.Run("should return no errors when all datasources are healthy", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + {UID: "valid-uid-2", Type: "mysql", Name: "MySQL"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(2), report.Count) + assert.Empty(t, report.Errors) + }) + + t.Run("should return errors when datasource UID is invalid", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "invalid uid", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Invalid UID: invalid uid", report.Errors[0].Reason) + }) + + t.Run("should return errors when datasource health check fails", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Health check failed: Prometheus", report.Errors[0].Reason) + }) +} + +type MockDatasourceSvc struct { + datasources.DataSourceService + + dss []*datasources.DataSource +} + +func (m *MockDatasourceSvc) GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) { + return m.dss, nil +} + +type MockPluginContextProvider struct { + datasource.PluginContextWrapper + + pCtx backend.PluginContext +} + +func (m *MockPluginContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { + return m.pCtx, nil +} + +type MockPluginClient struct { + plugins.Client + + res *backend.CheckHealthResult +} + +func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return m.res, nil +} diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go new file mode 100644 index 00000000000..008f5656298 --- /dev/null +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -0,0 +1,13 @@ +package checks + +import ( + "context" + + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" +) + +// Check defines the methods that a check must implement to be executed. +type Check interface { + Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) + Type() string +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go new file mode 100644 index 00000000000..4eae090198b --- /dev/null +++ b/apps/advisor/pkg/app/utils.go @@ -0,0 +1,95 @@ +package app + +import ( + "context" + "errors" + "fmt" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/user" +) + +func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check, error) { + labels := obj.GetLabels() + objTypeLabel, ok := labels[typeLabel] + if !ok { + return nil, errors.New("missing check type as label") + } + c, ok := checks[objTypeLabel] + if !ok { + supportedTypes := "" + for k := range checks { + supportedTypes += k + ", " + } + return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes) + } + + return c, nil +} + +func getStatusAnnotation(obj resource.Object) string { + return obj.GetAnnotations()[statusAnnotation] +} + +func setStatusAnnotation(ctx context.Context, client resource.Client, obj resource.Object, status string) error { + annotations := obj.GetAnnotations() + annotations[statusAnnotation] = status + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/metadata/annotations", + Value: annotations, + }}, + }, resource.PatchOptions{}, obj) +} + +func processCheck(ctx context.Context, client resource.Client, obj resource.Object, check checks.Check) error { + status := getStatusAnnotation(obj) + if status != "" { + // Check already processed + return nil + } + c, ok := obj.(*advisorv0alpha1.Check) + if !ok { + return fmt.Errorf("invalid object type") + } + // Populate ctx with the user that created the check + meta, err := utils.MetaAccessor(obj) + if err != nil { + return err + } + createdBy := meta.GetCreatedBy() + typ, uid, err := claims.ParseTypeID(createdBy) + if err != nil { + return err + } + ctx = identity.WithRequester(ctx, &user.SignedInUser{ + UserUID: uid, + FallbackType: typ, + }) + // Run the checks + report, err := check.Run(ctx, &c.Spec) + if err != nil { + setErr := setStatusAnnotation(ctx, client, obj, "error") + if setErr != nil { + return setErr + } + return err + } + err = setStatusAnnotation(ctx, client, obj, "processed") + if err != nil { + return err + } + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/status/report", + Value: *report, + }}, + }, resource.PatchOptions{}, obj) +} diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go new file mode 100644 index 00000000000..6626f645eb7 --- /dev/null +++ b/apps/advisor/pkg/app/utils_test.go @@ -0,0 +1,124 @@ +package app + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/stretchr/testify/assert" +) + +func TestGetCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "testType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + check, err := getCheck(obj, checkMap) + assert.NoError(t, err) + assert.NotNil(t, check) +} + +func TestGetCheck_MissingLabel(t *testing.T) { + obj := &advisorv0alpha1.Check{} + checkMap := map[string]checks.Check{} + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Equal(t, "missing check type as label", err.Error()) +} + +func TestGetCheck_UnknownType(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "unknownType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown check type unknownType") +} + +func TestSetStatusAnnotation(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + client := &mockClient{} + ctx := context.TODO() + + err := setStatusAnnotation(ctx, client, obj, "processed") + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err = processCheck(ctx, client, obj, check) + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck_AlreadyProcessed(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err := processCheck(ctx, client, obj, check) + assert.NoError(t, err) +} + +func TestProcessCheck_RunError(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + + check := &mockCheck{ + err: errors.New("run error"), + } + + err = processCheck(ctx, client, obj, check) + assert.Error(t, err) + assert.Equal(t, "error", obj.GetAnnotations()[statusAnnotation]) +} + +type mockClient struct { + resource.Client +} + +func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { + return nil +} + +type mockCheck struct { + checks.Check + err error +} + +func (m *mockCheck) Run(ctx context.Context, spec *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) { + return &advisorv0alpha1.CheckV0alpha1StatusReport{}, m.err +} diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index 1e9c8b30f53..a680dcc0880 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/apps/advisor/pkg/apis" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/pkg/services/apiserver/builder/runner" ) @@ -13,11 +14,14 @@ type AdvisorAppProvider struct { app.Provider } -func RegisterApp() *AdvisorAppProvider { +func RegisterApp( + checkRegistry checkregistry.CheckService, +) *AdvisorAppProvider { provider := &AdvisorAppProvider{} appCfg := &runner.AppBuilderConfig{ OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions, ManagedKinds: advisorapp.GetKinds(), + CustomConfig: any(checkRegistry), } provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New) return provider diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index b4da6459d3f..62397bebe51 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -3,6 +3,7 @@ package appregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" "github.com/grafana/grafana/pkg/registry/apps/advisor" "github.com/grafana/grafana/pkg/registry/apps/investigation" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -13,4 +14,6 @@ var WireSet = wire.NewSet( playlist.RegisterApp, investigation.RegisterApp, advisor.RegisterApp, + checkregistry.ProvideService, + wire.Bind(new(checkregistry.CheckService), new(*checkregistry.Service)), ) From a540c2fe7c80de387bf6f89f4d10b44dde3dee58 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 23 Jan 2025 15:31:34 +0000 Subject: [PATCH 13/29] MultiCombobox: Refactor open state (#99453) * Make downshift useCombobox own the isOpen state again * oopsie, removed my console logs --- .../src/components/Combobox/MultiCombobox.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index a56cf853f1d..8099fb595d6 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -74,11 +74,6 @@ export const MultiCombobox = (props: MultiComboboxPro return newItems; }, [baseItems, inputValue, enableAllOption, allOptionItem]); - const [isOpen, setIsOpen] = useState(false); - - const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, @@ -108,17 +103,41 @@ export const MultiCombobox = (props: MultiComboboxPro break; } }, + stateReducer: (state, actionAndChanges) => { + const { changes } = actionAndChanges; + return { + ...changes, + + /** + * TODO: Fix Hack! + * This prevents the menu from closing when the user unselects an item in the dropdown at the expense + * of breaking keyboard navigation. + * + * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item + * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. + * This only seems to happen when you deselect the last item in the selectedItems list. + * + * Check out: + * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 + * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 + * + * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) + * and prevents the if statement in useMultipleSelection from focusing anything. + */ + activeIndex: -999, + }; + }, }); const { //getToggleButtonProps, //getLabelProps, + isOpen, + highlightedIndex, getMenuProps, getInputProps, - highlightedIndex, getItemProps, } = useCombobox({ - isOpen, items, itemToString, inputValue, @@ -135,7 +154,6 @@ export const MultiCombobox = (props: MultiComboboxPro }; case useCombobox.stateChangeTypes.InputBlur: setInputValue(''); - setIsOpen(false); default: return changes; } @@ -175,14 +193,15 @@ export const MultiCombobox = (props: MultiComboboxPro case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); break; - case useCombobox.stateChangeTypes.InputClick: - setIsOpen(true); default: break; } }, }); + const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); + const virtualizerOptions = { count: items.length, getScrollElement: () => scrollRef.current, @@ -241,7 +260,6 @@ export const MultiCombobox = (props: MultiComboboxPro disabled, preventKeyAction: isOpen, placeholder: selectedItems.length > 0 ? undefined : placeholder, - onFocus: () => !disabled && setIsOpen(true), }) )} /> From 3993d691f43244c0532a854fee3133ed8072c02e Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 23 Jan 2025 17:00:02 +0100 Subject: [PATCH 14/29] Advisor: Implement authorizer (#99440) --- apps/advisor/pkg/app/authorizer.go | 31 ++++++++++ apps/advisor/pkg/app/authorizer_test.go | 78 +++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- pkg/registry/apps/advisor/register.go | 1 + 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 apps/advisor/pkg/app/authorizer.go create mode 100644 apps/advisor/pkg/app/authorizer_test.go diff --git a/apps/advisor/pkg/app/authorizer.go b/apps/advisor/pkg/app/authorizer.go new file mode 100644 index 00000000000..67defbc85d9 --- /dev/null +++ b/apps/advisor/pkg/app/authorizer.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + u, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + return authorizer.DecisionDeny, "forbidden", nil + }) +} diff --git a/apps/advisor/pkg/app/authorizer_test.go b/apps/advisor/pkg/app/authorizer_test.go new file mode 100644 index 00000000000..84414362fbd --- /dev/null +++ b/apps/advisor/pkg/app/authorizer_test.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestGetAuthorizer(t *testing.T) { + tests := []struct { + name string + ctx context.Context + attr authorizer.Attributes + expectedDecision authorizer.Decision + expectedReason string + expectedErr error + }{ + { + name: "non-resource request", + ctx: context.TODO(), + attr: &mockAttributes{resourceRequest: false}, + expectedDecision: authorizer.DecisionNoOpinion, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: true}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionAllow, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is not admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: false}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionDeny, + expectedReason: "forbidden", + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth := GetAuthorizer() + decision, reason, err := auth.Authorize(tt.ctx, tt.attr) + assert.Equal(t, tt.expectedDecision, decision) + assert.Equal(t, tt.expectedReason, reason) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +type mockAttributes struct { + authorizer.Attributes + resourceRequest bool +} + +func (m *mockAttributes) IsResourceRequest() bool { + return m.resourceRequest +} + +// Implement other methods of authorizer.Attributes as needed + +type mockUser struct { + identity.Requester + isGrafanaAdmin bool +} + +func (m *mockUser) GetIsGrafanaAdmin() bool { + return m.isGrafanaAdmin +} + +// Implement other methods of identity.Requester as needed diff --git a/go.mod b/go.mod index d57b3cb3d61..d0282c33045 100644 --- a/go.mod +++ b/go.mod @@ -198,7 +198,7 @@ require ( ) require ( - github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f // @grafana/plugins-platform-backend + github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d // @grafana/alerting-backend github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index 166125e556b..7285649b56c 100644 --- a/go.sum +++ b/go.sum @@ -1532,8 +1532,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.262.0 h1:R2DV6lwBQE5zaogxX3PorD9Seo8CXA8YuStf84oqwkk= github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f h1:c8IkbxPvM6+lscVOLgtbt8Gnro4Liltd+E2eqkoAeZA= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d h1:oNc/aDfDucQxLbRZK25yz3Cwc+dGo1C0Xmm2LaliWUQ= diff --git a/pkg/registry/apps/advisor/register.go b/pkg/registry/apps/advisor/register.go index a680dcc0880..c989d82262e 100644 --- a/pkg/registry/apps/advisor/register.go +++ b/pkg/registry/apps/advisor/register.go @@ -21,6 +21,7 @@ func RegisterApp( appCfg := &runner.AppBuilderConfig{ OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions, ManagedKinds: advisorapp.GetKinds(), + Authorizer: advisorapp.GetAuthorizer(), CustomConfig: any(checkRegistry), } provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New) From ec9f59fe9e29a7d83917e96c1d920355682e2190 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 23 Jan 2025 17:06:14 +0100 Subject: [PATCH 15/29] Chore: Bump promlib to v.0.0.8 (#99458) * bump promlib * make update-workspace --- go.mod | 2 +- go.sum | 4 ++-- pkg/storage/unified/apistore/go.sum | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index d0282c33045..e330b0e2e5f 100644 --- a/go.mod +++ b/go.mod @@ -209,7 +209,7 @@ require ( // This needs to be here for other projects that import grafana/grafana // For local development grafana/grafana will always use the local files // Check go.work file for details - github.com/grafana/grafana/pkg/promlib v0.0.7 // @grafana/oss-big-tent + github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage diff --git a/go.sum b/go.sum index 7285649b56c..a48895f596d 100644 --- a/go.sum +++ b/go.sum @@ -1546,8 +1546,8 @@ github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d h1:vVaLOibSj5lDwCb+pa0fly9uods8nTq1NJPYrTuPAGE= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d/go.mod h1:srJ6H0DL7c9Cz4CmK0h4AOlC1sm8UDwn9jBZ+RHxBt4= -github.com/grafana/grafana/pkg/promlib v0.0.7 h1:BdpanKOKnID/l1BJZLhE7TRNtmq7aOVdou1LBFWaMmU= -github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= +github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= +github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d h1:4w4x8g6xCSRj06er81atNSQVL0fTSwLekDYsRxpz+M0= diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 478fa431e69..c6bf0abfdee 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -569,8 +569,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.262.0 h1:R2DV6lwBQE5zaogxX3PorD9Seo8 github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= -github.com/grafana/grafana/pkg/promlib v0.0.7 h1:BdpanKOKnID/l1BJZLhE7TRNtmq7aOVdou1LBFWaMmU= -github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= +github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= +github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= From ef3a53f85cd4af1d941e75e9b9fcbc5e0ac9cbbc Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 23 Jan 2025 13:06:48 -0300 Subject: [PATCH 16/29] Share: Add tracking to invite user button (#99376) --- public/app/features/users/UsersActionBar.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 48ef3a81e83..5a4fa4b2e8b 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -1,5 +1,6 @@ import { connect, ConnectedProps } from 'react-redux'; +import { reportInteraction } from '@grafana/runtime'; import { RadioButtonGroup, LinkButton, FilterInput, InlineField } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; @@ -52,6 +53,13 @@ export const UsersActionBarUnconnected = ({ // 2) new basic auth users can be created for this instance (!config.disableLoginForm). const showInviteButton: boolean = canAddToOrg && !(config.disableLoginForm && config.externalUserMngInfo); + const onExternalUserMngClick = () => { + reportInteraction('users_admin_actions_clicked', { + category: 'org_users', + item: 'manage_users_external', + }); + }; + return (
@@ -68,7 +76,12 @@ export const UsersActionBarUnconnected = ({ )} {showInviteButton && Invite} {externalUserMngLinkUrl && ( - + {externalUserMngLinkName} )} From 59b246dbeaa09ae0521510b607622f04cdd67321 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:10:59 +0000 Subject: [PATCH 17/29] Update dependency stylelint to v16.13.2 (#99455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 102 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index d43c76f4903..8cbbdc5d5d0 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", - "stylelint": "16.12.0", + "stylelint": "16.13.2", "stylelint-config-sass-guidelines": "12.1.0", "terser-webpack-plugin": "5.3.11", "testing-library-selector": "0.3.1", diff --git a/yarn.lock b/yarn.lock index 765a1f74a0e..9abbcd7ada9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4457,6 +4457,15 @@ __metadata: languageName: node linkType: hard +"@keyv/serialize@npm:^1.0.2": + version: 1.0.2 + resolution: "@keyv/serialize@npm:1.0.2" + dependencies: + buffer: "npm:^6.0.3" + checksum: 10/6a42a5778a6b4542f6903ba7e6a17c5bd116441798d75c95fba9908c76c7606db527fad710b5c54abc6175e49b1bbaaafe3b836ad4b91e1af701394134f1d504 + languageName: node + linkType: hard + "@kusto/language-service-next@npm:11.5.3": version: 11.5.3 resolution: "@kusto/language-service-next@npm:11.5.3" @@ -11980,6 +11989,16 @@ __metadata: languageName: node linkType: hard +"cacheable@npm:^1.8.7": + version: 1.8.7 + resolution: "cacheable@npm:1.8.7" + dependencies: + hookified: "npm:^1.6.0" + keyv: "npm:^5.2.3" + checksum: 10/dce96e947c5b879a58ce024fd2d08a4c44ee328e8406cd6243da2d0e3a17579d63108c80e9272f64190d2d2f00e26a307b3e3ff65b6665abc70956714442897b + languageName: node + linkType: hard + "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -13453,7 +13472,7 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^3.0.1": +"css-tree@npm:^3.1.0": version: 3.1.0 resolution: "css-tree@npm:3.1.0" dependencies: @@ -16299,6 +16318,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^10.0.5": + version: 10.0.5 + resolution: "file-entry-cache@npm:10.0.5" + dependencies: + flat-cache: "npm:^6.1.5" + checksum: 10/57439b39635e75aa900ccdaad9167b85a500a559dd8f0902509189849784e9255d43cd3d8307f918dbd19f48b02562b5e354bcf7bfa23afe3985485504eb7a5e + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -16308,15 +16336,6 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^9.1.0": - version: 9.1.0 - resolution: "file-entry-cache@npm:9.1.0" - dependencies: - flat-cache: "npm:^5.0.0" - checksum: 10/fd67a9552f272ac4a1731c545e1350bd135e208659144cc5311baac6b8bbf55da7c8c3a0bf25c71ed78eff2bdd26d2a3a8f9ba3d8bec968fe8d1eeba6ab14a96 - languageName: node - linkType: hard - "file-saver@npm:2.0.5": version: 2.0.5 resolution: "file-saver@npm:2.0.5" @@ -16485,13 +16504,14 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^5.0.0": - version: 5.0.0 - resolution: "flat-cache@npm:5.0.0" +"flat-cache@npm:^6.1.5": + version: 6.1.5 + resolution: "flat-cache@npm:6.1.5" dependencies: - flatted: "npm:^3.3.1" - keyv: "npm:^4.5.4" - checksum: 10/42570762052b17a1dec221d73a1e417d0ba07137de6debaabb51389cac265a12a027a895dc84e1725bc5cdde04fe8b706ad836860b05488e9a04bda9301d2529 + cacheable: "npm:^1.8.7" + flatted: "npm:^3.3.2" + hookified: "npm:^1.6.0" + checksum: 10/c98d7635316ceee08b74c1dd3f05eda6a5f2f073688af927de68193ca992ab09eb9f0ee7d5fac40f53280844f11cf368b90dc43f73fee358165dc84e13828674 languageName: node linkType: hard @@ -16504,10 +16524,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9, flatted@npm:^3.3.1": - version: 3.3.1 - resolution: "flatted@npm:3.3.1" - checksum: 10/7b8376061d5be6e0d3658bbab8bde587647f68797cf6bfeae9dea0e5137d9f27547ab92aaff3512dd9d1299086a6d61be98e9d48a56d17531b634f77faadbc49 +"flatted@npm:^3.2.9, flatted@npm:^3.3.2": + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 languageName: node linkType: hard @@ -17645,7 +17665,7 @@ __metadata: slate-react: "npm:0.22.10" smtp-tester: "npm:^2.1.0" style-loader: "npm:4.0.0" - stylelint: "npm:16.12.0" + stylelint: "npm:16.13.2" stylelint-config-sass-guidelines: "npm:12.1.0" swagger-ui-react: "npm:5.18.2" symbol-observable: "npm:4.0.0" @@ -17947,6 +17967,13 @@ __metadata: languageName: node linkType: hard +"hookified@npm:^1.6.0": + version: 1.7.0 + resolution: "hookified@npm:1.7.0" + checksum: 10/87fb8f2ae170f28b1e0b903f2b0b40fb2a92a0364baab7c54121db1c871a2e81c589d778b48e1a35462181b8840764caf6cac3744330e5687a05a4b4d2ad729d + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -18477,10 +18504,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^6.0.2": - version: 6.0.2 - resolution: "ignore@npm:6.0.2" - checksum: 10/af39e49996cd989763920e445eff897d0ae1e36b5f27b0e09e14a4fd2df89b362f92e720ecf06ef729056842366527db8561d310e904718810b92ffbcd23056d +"ignore@npm:^7.0.1": + version: 7.0.3 + resolution: "ignore@npm:7.0.3" + checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365 languageName: node linkType: hard @@ -20515,6 +20542,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:^5.2.3": + version: 5.2.3 + resolution: "keyv@npm:5.2.3" + dependencies: + "@keyv/serialize": "npm:^1.0.2" + checksum: 10/47b4e9deb33e6a80e5ea79f3022ed3a14bc9fe553b7527ffff0a70b10c7a6c1a5d7e49b9bcfdbd8e8b9fb4632d68baa19d09e82628bcf853103e750e56d49a9e + languageName: node + linkType: hard + "kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -28494,9 +28530,9 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:16.12.0, stylelint@npm:^16.8.2": - version: 16.12.0 - resolution: "stylelint@npm:16.12.0" +"stylelint@npm:16.13.2, stylelint@npm:^16.8.2": + version: 16.13.2 + resolution: "stylelint@npm:16.13.2" dependencies: "@csstools/css-parser-algorithms": "npm:^3.0.4" "@csstools/css-tokenizer": "npm:^3.0.3" @@ -28507,16 +28543,16 @@ __metadata: colord: "npm:^2.9.3" cosmiconfig: "npm:^9.0.0" css-functions-list: "npm:^3.2.3" - css-tree: "npm:^3.0.1" + css-tree: "npm:^3.1.0" debug: "npm:^4.3.7" - fast-glob: "npm:^3.3.2" + fast-glob: "npm:^3.3.3" fastest-levenshtein: "npm:^1.0.16" - file-entry-cache: "npm:^9.1.0" + file-entry-cache: "npm:^10.0.5" global-modules: "npm:^2.0.0" globby: "npm:^11.1.0" globjoin: "npm:^0.1.4" html-tags: "npm:^3.3.1" - ignore: "npm:^6.0.2" + ignore: "npm:^7.0.1" imurmurhash: "npm:^0.1.4" is-plain-object: "npm:^5.0.0" known-css-properties: "npm:^0.35.0" @@ -28538,7 +28574,7 @@ __metadata: write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 10/8ab174441f3909a79b84efe62b99061db42ef232262abf0af50ab2458822c39781886171912362eb018a293b70c33bd20b8ff7194ef64af4aee4541787aee2e9 + checksum: 10/98385b53d3c822b3b764fe8ff2f7212717127ab40ca9fd34a83bc6e27b5240d4ea02f959e01d4eaf91c87480a0c787b07b837883d4b3ec44133cc7ca03c79b47 languageName: node linkType: hard From 192a81d07f077b43d178a01e6dac5eb11d42961c Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 23 Jan 2025 09:30:14 -0700 Subject: [PATCH 18/29] Folders: Fix guardian to use folder service (#99339) --- pkg/api/dashboard_snapshot_test.go | 3 +- pkg/api/dashboard_test.go | 6 +- pkg/api/folder_bench_test.go | 2 +- .../dashboards/service/dashboard_service.go | 20 ++- .../dashboard_service_integration_test.go | 3 +- pkg/services/folder/folderimpl/folder.go | 14 +- .../guardian/accesscontrol_guardian.go | 141 ++++++++++-------- .../guardian/accesscontrol_guardian_test.go | 3 +- pkg/services/guardian/guardian.go | 28 ++-- pkg/services/guardian/provider.go | 20 +-- pkg/services/libraryelements/guard.go | 13 +- .../libraryelements_get_all_test.go | 4 +- .../libraryelements_patch_test.go | 6 +- .../libraryelements_permissions_test.go | 21 +-- .../libraryelements/libraryelements_test.go | 62 ++++---- .../librarypanels/librarypanels_test.go | 8 +- 16 files changed, 207 insertions(+), 147 deletions(-) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 2f27909020e..24dcd3ef4ec 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -40,7 +41,7 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) { hs.DashboardService = svc hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 5d130a1ddd3..0efb1a167ee 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -151,7 +151,7 @@ func TestHTTPServer_GetDashboard_AccessControl(t *testing.T) { hs.starService = startest.NewStarServiceFake() hs.dashboardProvisioningService = mockDashboardProvisioningService{} - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } @@ -279,7 +279,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { license.On("FeatureEnabled", publicdashboardModels.FeaturePublicDashboardsEmailSharing).Return(false) hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg, license) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } deleteDashboard := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) { @@ -330,7 +330,7 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, } - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) }) } diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 0ca06bdd658..8192ff6fef5 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -495,7 +495,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog } hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) + guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) m.Get("/api/folders", hs.GetFolders) m.Get("/api/search", hs.Search) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index f68ed03495d..d5af358d86f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -56,6 +56,7 @@ var ( provisionerPermissions = []accesscontrol.Permission{ {Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll}, {Action: datasources.ActionRead, Scope: datasources.ScopeAll}, @@ -611,13 +612,28 @@ func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashbo if newDashboard { // if it's a new dashboard/folder check the parent folder permissions metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.FolderID, // nolint:staticcheck + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } return guard, nil } + + if d.IsFolder { + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.ID, // nolint:staticcheck + UID: d.UID, + OrgID: d.OrgID, + }, d.OrgID, user) + if err != nil { + return nil, err + } + return guard, nil + } + guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) if err != nil { return nil, err diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 860e0be4cf6..95f840294c9 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -896,7 +897,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc ) dashboardService.RegisterDashboardPermissions(dashboardPermissions) require.NoError(t, err) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService) + guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.NewNopLogger()) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.UID, sqlStore) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 85664cda390..b9f8c31b9a9 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -392,7 +392,7 @@ func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQu // we only need to check access to the folder // if the parent is accessible then the subfolders are accessible as well (due to inheritance) - g, err := guardian.NewByUID(ctx, q.UID, q.OrgID, q.SignedInUser) + g, err := guardian.NewByFolderUID(ctx, q.UID, q.OrgID, q.SignedInUser) if err != nil { return nil, err } @@ -941,7 +941,7 @@ func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderComm return folder.ErrBadRequest.Errorf("invalid orgID") } - guard, err := guardian.NewByUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser) + guard, err := guardian.NewByFolderUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser) if err != nil { return err } @@ -1414,13 +1414,19 @@ func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashbo // if it's a new dashboard/folder check the parent folder permissions metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() // nolint:staticcheck - guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: d.FolderID, // nolint:staticcheck + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } return guard, nil } - guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) + guard, err := guardian.NewByFolder(ctx, &folder.Folder{ + UID: d.UID, + OrgID: d.OrgID, + }, d.OrgID, user) if err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index de32d370ae2..c521fa9221e 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -18,6 +18,7 @@ var _ DashboardGuardian = new(accessControlDashboardGuardian) func NewAccessControlDashboardGuardian( ctx context.Context, cfg *setting.Cfg, dashboardId int64, user identity.Requester, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + foldersService folder.Service, logger log.Logger, ) (DashboardGuardian, error) { var dashboard *dashboards.Dashboard if dashboardId != 0 { @@ -37,6 +38,7 @@ func NewAccessControlDashboardGuardian( } if dashboard != nil && dashboard.IsFolder { + logger.Info("using dashboard guardian for folder", "folder", dashboard.UID) return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -58,68 +60,22 @@ func NewAccessControlDashboardGuardian( user: user, ac: ac, dashboardService: dashboardService, - }, - dashboard: dashboard, - }, nil -} - -// NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboardUID. -func NewAccessControlDashboardGuardianByUID( - ctx context.Context, cfg *setting.Cfg, dashboardUID string, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, -) (DashboardGuardian, error) { - var dashboard *dashboards.Dashboard - if dashboardUID != "" { - q := &dashboards.GetDashboardQuery{ - UID: dashboardUID, - OrgID: user.GetOrgID(), - } - - qResult, err := dashboardService.GetDashboard(ctx, q) - if err != nil { - if errors.Is(err, dashboards.ErrDashboardNotFound) { - return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) - } - return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) - } - dashboard = qResult - } - - if dashboard != nil && dashboard.IsFolder { - return &accessControlFolderGuardian{ - accessControlBaseGuardian: accessControlBaseGuardian{ - ctx: ctx, - cfg: cfg, - log: log.New("folder.permissions"), - user: user, - ac: ac, - dashboardService: dashboardService, - }, - folder: dashboards.FromDashboard(dashboard), - }, nil - } - - return &accessControlDashboardGuardian{ - accessControlBaseGuardian: accessControlBaseGuardian{ - cfg: cfg, - ctx: ctx, - log: log.New("dashboard.permissions"), - user: user, - ac: ac, - dashboardService: dashboardService, + folderService: foldersService, }, dashboard: dashboard, }, nil } // NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard. -// This constructor should be preferred over the other two if the dashboard in available +// This constructor should be preferred over the other two if the dashboard is available // since it avoids querying the database for fetching the dashboard. func NewAccessControlDashboardGuardianByDashboard( ctx context.Context, cfg *setting.Cfg, dashboard *dashboards.Dashboard, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, folderService folder.Service, + logger log.Logger, ) (DashboardGuardian, error) { if dashboard != nil && dashboard.IsFolder { + logger.Info("using by dashboard guardian for folder", "folder", dashboard.UID) return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -128,6 +84,7 @@ func NewAccessControlDashboardGuardianByDashboard( user: user, ac: ac, dashboardService: dashboardService, + folderService: folderService, }, folder: dashboards.FromDashboard(dashboard), }, nil @@ -141,16 +98,35 @@ func NewAccessControlDashboardGuardianByDashboard( user: user, ac: ac, dashboardService: dashboardService, + folderService: folderService, }, dashboard: dashboard, }, nil } -// NewAccessControlFolderGuardian creates a folder guardian by the provided folder. -func NewAccessControlFolderGuardian( - ctx context.Context, cfg *setting.Cfg, f *folder.Folder, user identity.Requester, - ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, +// NewAccessControlFolderGuardianByUID creates a folder guardian by the provided folderUID. +func NewAccessControlFolderGuardianByUID( + ctx context.Context, cfg *setting.Cfg, folderUID string, user identity.Requester, + ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, foldersService folder.Service, ) (DashboardGuardian, error) { + var f *folder.Folder + if folderUID != "" { + q := &folder.GetFolderQuery{ + UID: &folderUID, + OrgID: user.GetOrgID(), + SignedInUser: user, + } + + qResult, err := foldersService.Get(ctx, q) + if err != nil { + if errors.Is(err, dashboards.ErrFolderNotFound) { + return nil, ErrGuardianFolderNotFound.Errorf("failed to get folder by UID: %w", err) + } + return nil, ErrGuardianGetFolderFailure.Errorf("failed to get folder by UID: %w", err) + } + f = qResult + } + return &accessControlFolderGuardian{ accessControlBaseGuardian: accessControlBaseGuardian{ ctx: ctx, @@ -159,6 +135,44 @@ func NewAccessControlFolderGuardian( user: user, ac: ac, dashboardService: dashboardService, + folderService: foldersService, + }, + folder: f, + }, nil +} + +// NewAccessControlFolderGuardian creates a folder guardian by the provided folder. +func NewAccessControlFolderGuardian( + ctx context.Context, cfg *setting.Cfg, f *folder.Folder, user identity.Requester, + ac accesscontrol.AccessControl, orgID int64, dashboardService dashboards.DashboardService, + folderService folder.Service, +) (DashboardGuardian, error) { + if f.UID == "" { // nolint:staticcheck + query := &folder.GetFolderQuery{ + ID: &f.ID, // nolint:staticcheck + OrgID: orgID, + SignedInUser: user, + } + + folder, err := folderService.Get(ctx, query) + if err != nil { + if errors.Is(err, dashboards.ErrFolderNotFound) { + return nil, ErrGuardianFolderNotFound.Errorf("failed to get folder: %w", err) + } + return nil, ErrGuardianGetFolderFailure.Errorf("failed to get folder: %w", err) + } + f = folder + } + + return &accessControlFolderGuardian{ + accessControlBaseGuardian: accessControlBaseGuardian{ + ctx: ctx, + cfg: cfg, + log: log.New("folder.permissions"), + user: user, + ac: ac, + dashboardService: dashboardService, + folderService: folderService, }, folder: f, }, nil @@ -171,6 +185,7 @@ type accessControlBaseGuardian struct { user identity.Requester ac accesscontrol.AccessControl dashboardService dashboards.DashboardService + folderService folder.Service } type accessControlDashboardGuardian struct { @@ -353,24 +368,24 @@ func (a *accessControlFolderGuardian) evaluate(evaluator accesscontrol.Evaluator return ok, err } -func (a *accessControlDashboardGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { +func (a *accessControlDashboardGuardian) loadParentFolder(folderID int64) (*folder.Folder, error) { if folderID == 0 { - return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil + return &folder.Folder{UID: accesscontrol.GeneralFolderUID, OrgID: a.user.GetOrgID()}, nil } - folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.GetOrgID()} - folderQueryResult, err := a.dashboardService.GetDashboard(a.ctx, folderQuery) + folderQuery := &folder.GetFolderQuery{ID: &folderID, OrgID: a.user.GetOrgID(), SignedInUser: a.user} + folderQueryResult, err := a.folderService.Get(a.ctx, folderQuery) if err != nil { return nil, err } return folderQueryResult, nil } -func (a *accessControlFolderGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { +func (a *accessControlFolderGuardian) loadParentFolder(folderID int64) (*folder.Folder, error) { if folderID == 0 { - return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil + return &folder.Folder{UID: accesscontrol.GeneralFolderUID, OrgID: a.user.GetOrgID()}, nil } - folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.GetOrgID()} - folderQueryResult, err := a.dashboardService.GetDashboard(a.ctx, folderQuery) + folderQuery := &folder.GetFolderQuery{ID: &folderID, OrgID: a.user.GetOrgID(), SignedInUser: a.user} + folderQueryResult, err := a.folderService.Get(a.ctx, folderQuery) if err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 8d24b282395..59ac7d49828 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -976,7 +977,7 @@ func setupAccessControlGuardianTest( userPermissions[orgID][p.Action] = append(userPermissions[orgID][p.Action], p.Scope) } - g, err := NewAccessControlDashboardGuardianByDashboard(context.Background(), cfg, d, &user.SignedInUser{OrgID: orgID, Permissions: userPermissions}, ac, fakeDashboardService) + g, err := NewAccessControlDashboardGuardianByDashboard(context.Background(), cfg, d, &user.SignedInUser{OrgID: orgID, Permissions: userPermissions}, ac, fakeDashboardService, folderSvc, log.NewNopLogger()) require.NoError(t, err) return g } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index e094674d07e..7100b7ffacc 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -15,6 +15,7 @@ var ( ErrGuardianGetDashboardFailure = errutil.Internal("guardian.getDashboardFailure", errutil.WithPublicMessage("Failed to get dashboard")) ErrGuardianDashboardNotFound = errutil.NotFound("guardian.dashboardNotFound") ErrGuardianFolderNotFound = errutil.NotFound("guardian.folderNotFound") + ErrGuardianGetFolderFailure = errutil.Internal("guardian.getFolderFailure", errutil.WithPublicMessage("Failed to get folder")) ) // DashboardGuardian to be used for guard against operations without access on dashboard and acl @@ -33,18 +34,18 @@ var New = func(ctx context.Context, dashId int64, orgId int64, user identity.Req panic("no guardian factory implementation provided") } -// NewByUID factory for creating a new dashboard guardian instance -// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned -var NewByUID = func(ctx context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - panic("no guardian factory implementation provided") -} - // NewByDashboard factory for creating a new dashboard guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned var NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { panic("no guardian factory implementation provided") } +// NewByFolderUID factory for creating a new folder guardian instance +// When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned +var NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + panic("no guardian factory implementation provided") +} + // NewByFolder factory for creating a new folder guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned var NewByFolder = func(ctx context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { @@ -107,14 +108,6 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { mock.User = user return mock, nil } - - NewByUID = func(_ context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - mock.OrgID = orgId - mock.DashUID = dashUID - mock.User = user - return mock, nil - } - NewByDashboard = func(_ context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { mock.OrgID = orgId mock.DashUID = dash.UID @@ -123,6 +116,13 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { return mock, nil } + NewByFolderUID = func(_ context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + mock.OrgID = orgId + mock.DashUID = folderUID + mock.User = user + return mock, nil + } + NewByFolder = func(_ context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { mock.OrgID = orgId mock.DashUID = f.UID diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index ad07caf0695..7e3902f1e5a 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" @@ -16,28 +17,29 @@ type Provider struct{} func ProvideService( cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, teamService team.Service, + folderService folder.Service, ) *Provider { // TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935 - InitAccessControlGuardian(cfg, ac, dashboardService) + InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.New("guardian")) return &Provider{} } func InitAccessControlGuardian( - cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, + cfg *setting.Cfg, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, folderService folder.Service, logger log.Logger, ) { New = func(ctx context.Context, dashId int64, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardian(ctx, cfg, dashId, user, ac, dashboardService) - } - - NewByUID = func(ctx context.Context, dashUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardianByUID(ctx, cfg, dashUID, user, ac, dashboardService) + return NewAccessControlDashboardGuardian(ctx, cfg, dashId, user, ac, dashboardService, folderService, logger) } NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlDashboardGuardianByDashboard(ctx, cfg, dash, user, ac, dashboardService) + return NewAccessControlDashboardGuardianByDashboard(ctx, cfg, dash, user, ac, dashboardService, folderService, logger) + } + + NewByFolderUID = func(ctx context.Context, folderUID string, orgId int64, user identity.Requester) (DashboardGuardian, error) { + return NewAccessControlFolderGuardianByUID(ctx, cfg, folderUID, user, ac, dashboardService, folderService) } NewByFolder = func(ctx context.Context, f *folder.Folder, orgId int64, user identity.Requester) (DashboardGuardian, error) { - return NewAccessControlFolderGuardian(ctx, cfg, f, user, ac, dashboardService) + return NewAccessControlFolderGuardian(ctx, cfg, f, user, ac, orgId, dashboardService, folderService) } } diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go index aa4f5d484fd..737d961f8c8 100644 --- a/pkg/services/libraryelements/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -41,7 +42,7 @@ func (l *LibraryElementService) requireEditPermissionsOnFolderUID(ctx context.Co return dashboards.ErrFolderAccessDenied } - g, err := guardian.NewByUID(ctx, folderUID, user.GetOrgID(), user) + g, err := guardian.NewByFolderUID(ctx, folderUID, user.GetOrgID(), user) if err != nil { return err } @@ -67,7 +68,10 @@ func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Conte return dashboards.ErrFolderAccessDenied } - g, err := guardian.New(ctx, folderID, user.GetOrgID(), user) + g, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: folderID, + OrgID: user.GetOrgID(), + }, user.GetOrgID(), user) if err != nil { return err } @@ -88,7 +92,10 @@ func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Conte return nil } - g, err := guardian.New(ctx, folderID, user.GetOrgID(), user) + g, err := guardian.NewByFolder(ctx, &folder.Folder{ + ID: folderID, + OrgID: user.GetOrgID(), + }, user.GetOrgID(), user) if err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 29cc7819fca..5be5ff4dede 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -539,7 +539,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) @@ -608,7 +608,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to a nonexistent folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index e59a19e29b6..8e7dce686ea 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -24,7 +24,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -91,7 +91,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -335,7 +335,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder") + newFolder := createFolder(t, sc, "NewFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index a07092269ad..f9af4ed15ad 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -38,7 +38,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -56,7 +56,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) command := getCreatePanelCommand(0, "", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -178,7 +178,7 @@ func TestLibraryElementCreatePermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) sc.reqContext.SignedInUser.Permissions = map[int64]map[string][]string{ 1: testCase.permissions, } @@ -235,14 +235,14 @@ func TestLibraryElementPatchPermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - fromFolder := createFolder(t, sc, "FromFolder") + fromFolder := createFolder(t, sc, "FromFolder", nil) // nolint:staticcheck command := getCreatePanelCommand(fromFolder.ID, fromFolder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) - toFolder := createFolder(t, sc, "ToFolder") + toFolder := createFolder(t, sc, "ToFolder", nil) sc.reqContext.SignedInUser.Permissions = map[int64]map[string][]string{ 1: testCase.permissions, @@ -268,6 +268,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can delete library elements when granted write access to the correct folder", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Folder")}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Folder")}, }, status: http.StatusOK, }, @@ -275,6 +276,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can delete library elements when granted write access to all folders", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, status: http.StatusOK, }, @@ -282,6 +284,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { desc: "can't delete library elements when granted write access to the wrong folder", permissions: map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, }, status: http.StatusForbidden, }, @@ -297,7 +300,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", sc.service.folderService) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -327,7 +330,7 @@ func TestLibraryElementsWithMissingFolders(t *testing.T) { testScenario(t, "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) @@ -368,7 +371,7 @@ func TestLibraryElementsGetPermissions(t *testing.T) { for _, testCase := range getCases { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder") + folder := createFolder(t, sc, "Folder", nil) // nolint:staticcheck cmd := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel") sc.reqContext.Req.Body = mockRequestBody(cmd) @@ -419,7 +422,7 @@ func TestLibraryElementsGetAllPermissions(t *testing.T) { testScenario(t, testCase.desc, func(t *testing.T, sc scenarioContext) { for i := 1; i <= 2; i++ { - folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i)) + folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i), nil) // nolint:staticcheck cmd := getCreatePanelCommand(folder.ID, folder.UID, fmt.Sprintf("Library Panel %d", i)) sc.reqContext.Req.Body = mockRequestBody(cmd) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 6dcdacb026c..9debfc717d5 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -32,7 +32,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -99,7 +98,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a folder uid that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID+"xxxx") - require.EqualError(t, err, dashboards.ErrFolderNotFound.Error()) + require.EqualError(t, err, guardian.ErrGuardianFolderNotFound.Errorf("failed to get folder by UID: %w", dashboards.ErrFolderNotFound).Error()) }) scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", @@ -300,17 +299,19 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash require.NoError(t, err) ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - var expectedFolder *folder.Folder - if dash.FolderUID != "" || dash.FolderID != 0 { // nolint:staticcheck - expectedFolder = &folder.Folder{ID: folderID, UID: folderUID} - } + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + _, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"}) + require.NoError(t, err) service, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - &foldertest.FakeService{ExpectedFolder: expectedFolder}, + folderSvc, folder.NewFakeStore(), nil, nil, @@ -327,22 +328,24 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash return dashboard } -func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder { +func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc folder.Service) *folder.Folder { t.Helper() - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) - require.NoError(t, err) + if folderSvc == nil { + features := featuremgmt.WithFeatures() + cfg := setting.NewCfg() + ac := actest.FakeAccessControl{ExpectedEvaluate: true} + dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) + require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - store := folderimpl.ProvideStore(sc.sqlStore) - s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, - features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) - t.Logf("Creating folder with title and UID %q", title) + folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) + store := folderimpl.ProvideStore(sc.sqlStore) + folderSvc = folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, + features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + t.Logf("Creating folder with title and UID %q", title) + } ctx := identity.WithRequester(context.Background(), &sc.user) - folder, err := s.Create(ctx, &folder.CreateFolderCommand{ + folder, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{ OrgID: sc.user.OrgID, Title: title, UID: title, SignedInUser: &sc.user, }) require.NoError(t, err) @@ -399,15 +402,18 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, svcErr) dashboardService.RegisterDashboardPermissions(dashboardPermissions) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService) + guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderSvc, log.NewNopLogger()) testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck @@ -462,23 +468,23 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) + fStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, dashSvcErr) dashService.RegisterDashboardPermissions(dashboardPermissions) - guardian.InitAccessControlGuardian(cfg, ac, dashService) - fStore := folderimpl.ProvideStore(sqlStore) - folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, - features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest()) + guardian.InitAccessControlGuardian(cfg, ac, dashService, folderSvc, log.NewNopLogger()) service := LibraryElementService{ Cfg: cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderSrv, + folderService: folderSvc, } // deliberate difference between signed in user and user in db to make it crystal clear @@ -510,7 +516,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo }, } - sc.folder = createFolder(t, sc, "ScenarioFolder") + sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc) fn(t, sc) }) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 80baeead8d2..9c030cc9061 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/kinds/librarypanel" @@ -823,18 +824,19 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ac := actest.FakeAccessControl{ExpectedEvaluate: true} dashStore := &dashboards.FakeDashboardStore{} - dashStore.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashPermissionService := acmock.NewMockedPermissionsService() + folderSvc := foldertest.NewFakeService() + folderSvc.ExpectedFolder = &folder.Folder{ID: 1} dashService, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashStore, folderStore, features, acmock.NewMockedPermissionsService(), ac, - foldertest.NewFakeService(), folder.NewFakeStore(), + folderSvc, folder.NewFakeStore(), nil, nil, nil, nil, quotaService, nil, ) require.NoError(t, err) dashService.RegisterDashboardPermissions(dashPermissionService) - guardian.InitAccessControlGuardian(cfg, ac, dashService) + guardian.InitAccessControlGuardian(cfg, ac, dashService, folderSvc, log.NewNopLogger()) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) require.NoError(t, err) From a4ef1f76e47abc69fa5ea6264de80e4768850757 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:40:22 -0500 Subject: [PATCH 19/29] Dashboards: Backend schema version migration (#99392) --- pkg/apis/dashboard/migration/migrate.go | 30 ++++ pkg/apis/dashboard/migration/migrate_test.go | 104 ++++++++++++++ .../migration/schemaversion/errors.go | 40 ++++++ .../migration/schemaversion/migrations.go | 31 ++++ .../schemaversion/migrations_test.go | 57 ++++++++ .../dashboard/migration/schemaversion/v40.go | 9 ++ .../migration/schemaversion/v40_test.go | 70 +++++++++ .../testdata/input/39.refresh_true.json | 134 ++++++++++++++++++ .../testdata/output/39.refresh_true.40.json | 134 ++++++++++++++++++ pkg/apis/dashboard/v1alpha1/conversion.go | 23 ++- 10 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 pkg/apis/dashboard/migration/migrate.go create mode 100644 pkg/apis/dashboard/migration/migrate_test.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/errors.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/migrations.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/migrations_test.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v40.go create mode 100644 pkg/apis/dashboard/migration/schemaversion/v40_test.go create mode 100644 pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json create mode 100644 pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json diff --git a/pkg/apis/dashboard/migration/migrate.go b/pkg/apis/dashboard/migration/migrate.go new file mode 100644 index 00000000000..36a4679f191 --- /dev/null +++ b/pkg/apis/dashboard/migration/migrate.go @@ -0,0 +1,30 @@ +package migration + +import "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + +func Migrate(dash map[string]interface{}, targetVersion int) error { + if dash == nil { + dash = map[string]interface{}{} + } + inputVersion := schemaversion.GetSchemaVersion(dash) + dash["schemaVersion"] = inputVersion + + if inputVersion < schemaversion.MINIUM_VERSION { + return schemaversion.NewMinimumVersionError(inputVersion) + } + + for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ { + if migration, ok := schemaversion.Migrations[nextVersion]; ok { + if err := migration(dash); err != nil { + return schemaversion.NewMigrationError("migration failed", inputVersion, nextVersion) + } + dash["schemaVersion"] = nextVersion + } + } + + if schemaversion.GetSchemaVersion(dash) != targetVersion { + return schemaversion.NewMigrationError("schema version not migrated to target version", inputVersion, targetVersion) + } + + return nil +} diff --git a/pkg/apis/dashboard/migration/migrate_test.go b/pkg/apis/dashboard/migration/migrate_test.go new file mode 100644 index 00000000000..af6a1305ea3 --- /dev/null +++ b/pkg/apis/dashboard/migration/migrate_test.go @@ -0,0 +1,104 @@ +package migration_test + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration" + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" +) + +const INPUT_DIR = "testdata/input" +const OUTPUT_DIR = "testdata/output" + +func TestMigrate(t *testing.T) { + files, err := os.ReadDir(INPUT_DIR) + require.NoError(t, err) + + t.Run("minimum version check", func(t *testing.T) { + err := migration.Migrate(map[string]interface{}{ + "schemaVersion": schemaversion.MINIUM_VERSION - 1, + }, schemaversion.MINIUM_VERSION) + + var minVersionErr = schemaversion.NewMinimumVersionError(schemaversion.MINIUM_VERSION - 1) + require.ErrorAs(t, err, &minVersionErr) + }) + + for _, f := range files { + if f.IsDir() { + continue + } + + inputDash, inputVersion, name := load(t, filepath.Join(INPUT_DIR, f.Name())) + + t.Run("input check "+f.Name(), func(t *testing.T) { + // use input version as the target version to ensure there are no changes + require.NoError(t, migration.Migrate(inputDash, inputVersion), "input check migration failed") + outBytes, err := json.MarshalIndent(inputDash, "", " ") + require.NoError(t, err, "failed to marshal migrated dashboard") + // We can ignore gosec G304 here since it's a test + // nolint:gosec + expectedDash, err := os.ReadFile(filepath.Join(INPUT_DIR, f.Name())) + require.NoError(t, err, "failed to read expected output file") + require.JSONEq(t, string(expectedDash), string(outBytes), "%s input check did not match", f.Name()) + }) + + for targetVersion := range schemaversion.Migrations { + testName := fmt.Sprintf("%s v%d to v%d", name, inputVersion, targetVersion) + t.Run(testName, func(t *testing.T) { + testMigration(t, f, targetVersion) + }) + } + } +} + +func testMigration(t *testing.T, file fs.DirEntry, targetVersion int) { + t.Helper() + dash, inputVersion, name := load(t, filepath.Join(INPUT_DIR, file.Name())) + require.NoError(t, migration.Migrate(dash, targetVersion), "%d migration failed", targetVersion) + + outPath := filepath.Join(OUTPUT_DIR, fmt.Sprintf("%d.%s.%d.json", inputVersion, name, targetVersion)) + outBytes, err := json.MarshalIndent(dash, "", " ") + require.NoError(t, err, "failed to marshal migrated dashboard") + + if _, err := os.Stat(outPath); os.IsNotExist(err) { + err = os.WriteFile(outPath, outBytes, 0644) + require.NoError(t, err, "failed to write new output file", outPath) + return + } + + // We can ignore gosec G304 here since it's a test + // nolint:gosec + existingBytes, err := os.ReadFile(outPath) + require.NoError(t, err, "failed to read existing output file") + require.JSONEq(t, string(existingBytes), string(outBytes), "%s did not match", outPath) +} + +func parseInputName(t *testing.T, name string) (int, string) { + t.Helper() + parts := strings.SplitN(filepath.Base(name), ".", 3) + if len(parts) < 3 { + t.Fatalf("invalid input filename: %s", name) + } + iv, err := strconv.Atoi(parts[0]) + require.NoError(t, err, "failed to parse input version") + return iv, parts[1] +} + +func load(t *testing.T, path string) (dash map[string]interface{}, inputVersion int, name string) { + // We can ignore gosec G304 here since it's a test + // nolint:gosec + inputBytes, err := os.ReadFile(path) + require.NoError(t, err, "failed to read embedded input file") + require.NoError(t, json.Unmarshal(inputBytes, &dash), "failed to unmarshal dashboard JSON") + inputVersion, name = parseInputName(t, path) + return dash, inputVersion, name +} diff --git a/pkg/apis/dashboard/migration/schemaversion/errors.go b/pkg/apis/dashboard/migration/schemaversion/errors.go new file mode 100644 index 00000000000..110a596a1ad --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/errors.go @@ -0,0 +1,40 @@ +package schemaversion + +import "fmt" + +var _ error = &MinimumVersionError{} +var _ error = &MigrationError{} + +// MinimumVersionError is an error that is returned when the schema version is below the minimum version. +func NewMinimumVersionError(inputVersion int) *MinimumVersionError { + return &MinimumVersionError{inputVersion: inputVersion} +} + +// MinimumVersionError is an error type for minimum version errors. +type MinimumVersionError struct { + inputVersion int +} + +func (e *MinimumVersionError) Error() string { + return fmt.Errorf("input schema version is below minimum version. input: %d minimum: %d", e.inputVersion, MINIUM_VERSION).Error() +} + +// ErrMigrationFailed is an error that is returned when a migration fails. +func NewMigrationError(msg string, currentVersion, targetVersion int) *MigrationError { + return &MigrationError{ + msg: msg, + targetVersion: targetVersion, + currentVersion: currentVersion, + } +} + +// MigrationError is an error type for migration errors. +type MigrationError struct { + msg string + targetVersion int + currentVersion int +} + +func (e *MigrationError) Error() string { + return fmt.Errorf("schema migration from version %d to %d failed: %v", e.currentVersion, e.targetVersion, e.msg).Error() +} diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations.go b/pkg/apis/dashboard/migration/schemaversion/migrations.go new file mode 100644 index 00000000000..1b8925e2885 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/migrations.go @@ -0,0 +1,31 @@ +package schemaversion + +import "strconv" + +type SchemaVersionMigrationFunc func(map[string]interface{}) error + +const ( + MINIUM_VERSION = 39 + LATEST_VERSION = 40 +) + +var Migrations = map[int]SchemaVersionMigrationFunc{ + 40: V40, +} + +func GetSchemaVersion(dash map[string]interface{}) int { + if v, ok := dash["schemaVersion"]; ok { + switch v := v.(type) { + case int: + return v + case float64: + return int(v) + case string: + if version, err := strconv.Atoi(v); err == nil { + return version + } + return 0 + } + } + return 0 +} diff --git a/pkg/apis/dashboard/migration/schemaversion/migrations_test.go b/pkg/apis/dashboard/migration/schemaversion/migrations_test.go new file mode 100644 index 00000000000..79be968d23b --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/migrations_test.go @@ -0,0 +1,57 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + "github.com/stretchr/testify/require" +) + +func TestGetSchemaVersion(t *testing.T) { + tests := []struct { + name string + dash map[string]interface{} + expected int + }{ + { + name: "schemaVersion as int", + dash: map[string]interface{}{ + "schemaVersion": 16, + }, + expected: 16, + }, + { + name: "schemaVersion as float64", + dash: map[string]interface{}{ + "schemaVersion": 40.2345, + }, + expected: 40, + }, + { + name: "schemaVersion is not set", + dash: map[string]interface{}{}, + expected: 0, + }, + { + name: "schemaVersion as string int", + dash: map[string]interface{}{ + "schemaVersion": "5", + }, + expected: 5, + }, + { + name: "schemaVersion as invalid string", + dash: map[string]interface{}{ + "schemaVersion": "foo", + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := schemaversion.GetSchemaVersion(tt.dash) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v40.go b/pkg/apis/dashboard/migration/schemaversion/v40.go new file mode 100644 index 00000000000..3336ee1c1fd --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v40.go @@ -0,0 +1,9 @@ +package schemaversion + +func V40(dash map[string]interface{}) error { + dash["schemaVersion"] = int(40) + if _, ok := dash["refresh"].(string); !ok { + dash["refresh"] = "" + } + return nil +} diff --git a/pkg/apis/dashboard/migration/schemaversion/v40_test.go b/pkg/apis/dashboard/migration/schemaversion/v40_test.go new file mode 100644 index 00000000000..19e6c90aa79 --- /dev/null +++ b/pkg/apis/dashboard/migration/schemaversion/v40_test.go @@ -0,0 +1,70 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" + "github.com/stretchr/testify/require" +) + +func TestV40(t *testing.T) { + tests := []migrationTestCase{ + { + name: "refresh not set", + input: map[string]interface{}{ + "title": "Test Dashboard", + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "", + }, + }, + { + name: "boolean refresh value is converted to an empty string", + input: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 39, + "refresh": true, + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "", + }, + }, + { + name: "string refresh value is not converted", + input: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 39, + "refresh": "1m", + }, + expected: map[string]interface{}{ + "title": "Test Dashboard", + "schemaVersion": 40, + "refresh": "1m", + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V40) +} + +type migrationTestCase struct { + name string + input map[string]interface{} + expected map[string]interface{} +} + +func runMigrationTests(t *testing.T, testCases []migrationTestCase, migrationFunc schemaversion.SchemaVersionMigrationFunc) { + t.Helper() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + err := migrationFunc(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, tt.input) + }) + } +} diff --git a/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json new file mode 100644 index 00000000000..4ea2be531f9 --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/input/39.refresh_true.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "", + "refresh": true, + "schemaVersion": 39 + } \ No newline at end of file diff --git a/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json new file mode 100644 index 00000000000..a8c67a1e80d --- /dev/null +++ b/pkg/apis/dashboard/migration/testdata/output/39.refresh_true.40.json @@ -0,0 +1,134 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0-81438", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "New dashboard", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/pkg/apis/dashboard/v1alpha1/conversion.go b/pkg/apis/dashboard/v1alpha1/conversion.go index cc512e7c38c..8e21a1c62c5 100644 --- a/pkg/apis/dashboard/v1alpha1/conversion.go +++ b/pkg/apis/dashboard/v1alpha1/conversion.go @@ -1,26 +1,35 @@ package v1alpha1 import ( + "errors" + conversion "k8s.io/apimachinery/pkg/conversion" klog "k8s.io/klog/v2" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apis/dashboard/migration" + "github.com/grafana/grafana/pkg/apis/dashboard/migration/schemaversion" ) func Convert_v0alpha1_Unstructured_To_v1alpha1_DashboardSpec(in *common.Unstructured, out *DashboardSpec, s conversion.Scope) error { - out.Unstructured = *in - - t, ok := in.Object["title"] - if !ok { - return nil // skip setting the title if it's not in the unstructured object + err := migration.Migrate(in.Object, schemaversion.LATEST_VERSION) + if err != nil { + minErr := &schemaversion.MinimumVersionError{} + if errors.As(err, &minErr) { + in.Object["__migrationError"] = err.Error() + } else { + return err + } } - title, ok := t.(string) + out.Unstructured = *in + + t, ok := in.Object["title"].(string) if !ok { klog.V(5).Infof("unstructured dashboard title field is not a string %v", t) return nil // skip setting the title if it's not a string in the unstructured object } - out.Title = title + out.Title = t return nil } From 75e72366876a7f751a042fb6e298361b692c25b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:41:27 +0000 Subject: [PATCH 20/29] Update dependency type-fest to v4.33.0 (#99463) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9abbcd7ada9..74a61b40636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29564,9 +29564,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.26.1": - version: 4.30.2 - resolution: "type-fest@npm:4.30.2" - checksum: 10/c5168b159c366e4fd5b74c7f7b786bed9248c03f67e6e07d52dd5d51354447468fa7c92b9f2142c7fe9279814031f783959370242c3520de848931b65ddb48bb + version: 4.33.0 + resolution: "type-fest@npm:4.33.0" + checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 languageName: node linkType: hard From 6b227bb3742ac79b33a4d7eedf4f5bce36c1ca11 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 23 Jan 2025 18:05:23 +0100 Subject: [PATCH 21/29] Prometheus: Handle non-json errors in a better way (#99342) * handle json errors in a better way * update comments * update unit tests * Update pkg/promlib/converter/prom.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * Update pkg/promlib/querydata/response_test.go Co-authored-by: Arve Knudsen * update import --------- Co-authored-by: Arve Knudsen --- pkg/promlib/converter/prom.go | 5 +- pkg/promlib/querydata/response.go | 66 +++++++++++++++++++------- pkg/promlib/querydata/response_test.go | 51 +++++++++++++++++--- 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/pkg/promlib/converter/prom.go b/pkg/promlib/converter/prom.go index 2d0a35524ca..09cd20086df 100644 --- a/pkg/promlib/converter/prom.go +++ b/pkg/promlib/converter/prom.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" sdkjsoniter "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + "github.com/grafana/grafana-plugin-sdk-go/experimental/status" jsoniter "github.com/json-iterator/go" ) @@ -23,7 +24,7 @@ type Options struct { } func rspErr(e error) backend.DataResponse { - return backend.DataResponse{Error: e} + return backend.DataResponse{Error: e, ErrorSource: status.SourceDownstream} } // ReadPrometheusStyleResult will read results from a prometheus or loki server and return data frames @@ -39,7 +40,7 @@ func ReadPrometheusStyleResult(jIter *jsoniter.Iterator, opt Options) backend.Da l1Fields: for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() { if err != nil { - return rspErr(err) + return rspErr(fmt.Errorf("response from prometheus couldn't be parsed. it is non-json: %w", err)) } switch l1Field { case "status": diff --git a/pkg/promlib/querydata/response.go b/pkg/promlib/querydata/response.go index 93cff2080dc..853510916d5 100644 --- a/pkg/promlib/querydata/response.go +++ b/pkg/promlib/querydata/response.go @@ -3,6 +3,7 @@ package querydata import ( "context" "fmt" + "io" "net/http" "sort" "strings" @@ -28,28 +29,59 @@ func (s *QueryData) parseResponse(ctx context.Context, q *models.Query, res *htt ctx, endSpan := utils.StartTrace(ctx, s.tracer, "datasource.prometheus.parseResponse") defer endSpan() - iter := jsoniter.Parse(jsoniter.ConfigDefault, res.Body, 1024) - r := converter.ReadPrometheusStyleResult(iter, converter.Options{Dataplane: true}) - r.Status = backend.Status(res.StatusCode) + statusCode := res.StatusCode - // Add frame to attach metadata - if len(r.Frames) == 0 && !q.ExemplarQuery { - r.Frames = append(r.Frames, data.NewFrame("")) - } + switch { + // Status codes that Prometheus might return + // so we want to parse the response + // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview + case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices, + statusCode == http.StatusBadRequest, + statusCode == http.StatusUnprocessableEntity, + statusCode == http.StatusServiceUnavailable: - // The ExecutedQueryString can be viewed in QueryInspector in UI - for i, frame := range r.Frames { - addMetadataToMultiFrame(q, frame) - if i == 0 { - frame.Meta.ExecutedQueryString = executedQueryString(q) + iter := jsoniter.Parse(jsoniter.ConfigDefault, res.Body, 1024) + r := converter.ReadPrometheusStyleResult(iter, converter.Options{Dataplane: true}) + r.Status = backend.Status(res.StatusCode) + + // Add frame to attach metadata + if len(r.Frames) == 0 && !q.ExemplarQuery { + r.Frames = append(r.Frames, data.NewFrame("")) } - } - if r.Error == nil { - r = s.processExemplars(ctx, q, r) - } + // The ExecutedQueryString can be viewed in QueryInspector in UI + for i, frame := range r.Frames { + addMetadataToMultiFrame(q, frame) + if i == 0 { + frame.Meta.ExecutedQueryString = executedQueryString(q) + } + } - return r + if r.Error == nil { + r = s.processExemplars(ctx, q, r) + } + + return r + default: + // Unknown status code. We don't want to parse the response. + const maxBodySize = 1024 + lr := io.LimitReader(res.Body, maxBodySize) + tb, _ := io.ReadAll(lr) + + s.log.FromContext(ctx).Error("Unexpected response received", "status", statusCode, "body", tb) + + errResp := backend.DataResponse{ + Error: fmt.Errorf("unexpected response with status code %d: %s", statusCode, tb), + ErrorSource: backend.ErrorSourceFromHTTPStatus(statusCode), + } + + f := data.NewFrame("") + addMetadataToMultiFrame(q, f) + f.Meta.ExecutedQueryString = executedQueryString(q) + errResp.Frames = append(errResp.Frames, f) + + return errResp + } } func (s *QueryData) processExemplars(ctx context.Context, q *models.Query, dr backend.DataResponse) backend.DataResponse { diff --git a/pkg/promlib/querydata/response_test.go b/pkg/promlib/querydata/response_test.go index 6d12465fee8..44264641324 100644 --- a/pkg/promlib/querydata/response_test.go +++ b/pkg/promlib/querydata/response_test.go @@ -7,7 +7,9 @@ import ( "net/http" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/promlib/models" "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" @@ -18,7 +20,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is before result the field must parsed normally", func(t *testing.T) { resBody := `{"data":{"resultType":"vector", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) @@ -26,7 +28,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is after the result field must parsed normally", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":"vector"},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) @@ -34,7 +36,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("no resultType is existed in the data", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "no resultType found") @@ -42,7 +44,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is set as empty string before result", func(t *testing.T) { resBody := `{"data":{"resultType":"", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "unknown result type: ") @@ -50,7 +52,7 @@ func TestQueryData_parseResponse(t *testing.T) { t.Run("resultType is set as empty string after result", func(t *testing.T) { resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":""},"status":"success"}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Error(t, result.Error) assert.Equal(t, result.Error.Error(), "unknown result type: ") @@ -61,10 +63,47 @@ func TestAddMetadataToMultiFrame(t *testing.T) { t.Run("when you have native histogram result", func(t *testing.T) { qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler} resBody := `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"rpc_durations_native_histogram_seconds","instance":"nativehisto:8080","job":"prometheus"},"histograms":[[1729529685,{"count":"7243102","sum":"72460202.93145595","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}],[1729529700,{"count":"7243490","sum":"72464056.03309634","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}],[1729529715,{"count":"7243880","sum":"72467935.35871512","buckets":[[0,"1.8340080864093422","2","10"],[0,"2","2.1810154653305154","68"]]}]]}]}}` - res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody)), StatusCode: 200} result := qd.parseResponse(context.Background(), &models.Query{}, res) assert.Nil(t, result.Error) assert.Len(t, result.Frames, 1) assert.Equal(t, "yMin", result.Frames[0].Fields[1].Name) }) } + +// Helper function to create mock HTTP response. +func createMockResponse(statusCode int, body string) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(bytes.NewReader([]byte(body))), + } +} + +func TestParseResponse_ErrorCases(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + statusCode int + body string + }{ + {"500 Internal Server Error", http.StatusInternalServerError, `{"error":"internal server error"}`}, + {"404 Not Found", http.StatusNotFound, `{"error":"not found"}`}, + {"401 Unauthorized", http.StatusUnauthorized, `{"error":"unauthorized"}`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := createMockResponse(tt.statusCode, tt.body) + q := &models.Query{} + qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler} + qd.log = log.New() + resp := qd.parseResponse(ctx, q, res) + + require.Error(t, resp.Error) + assert.Contains(t, resp.Error.Error(), "unexpected response") + assert.Len(t, resp.Frames, 1) + assert.NoError(t, res.Body.Close()) + }) + } +} From d24e7c126ddb107515e9a36ab1285c05cc639fe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:21:11 +0000 Subject: [PATCH 22/29] Update dependency yaml to v2.7.0 (#99466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 74a61b40636..c85beb509fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31128,11 +31128,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.6.1 - resolution: "yaml@npm:2.6.1" + version: 2.7.0 + resolution: "yaml@npm:2.7.0" bin: yaml: bin.mjs - checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d + checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec languageName: node linkType: hard From e9d9b152954deb5571bab368655186facf5a24f6 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 23 Jan 2025 12:21:24 -0600 Subject: [PATCH 23/29] StateTimeline / StatusHistory: Add axis visibility and width controls (#98548) * Allow setting the y axis width * Add to docs * Add to status history as well * Add to status history docs and schema * Change config to come from generic axis builder * keep axis * Change overridden label * Update docs/sources/panels-visualizations/visualizations/status-history/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/state-timeline/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Remove the category/label override * Move axis to its own section in docs as well * clean * rename to addAxisWidth * Apply suggestions from code review Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Move sections to match UI order * Update docs/sources/shared/visualizations/axis-options-all.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Change other axis options doc to be consistent. * Fix linter * add AxisPlacement * Add new placement option to docs * change some wording * Apply suggestions from code review Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Leon Sorokin Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../visualizations/state-timeline/index.md | 4 + .../visualizations/status-history/index.md | 4 + .../visualizations/time-series/index.md | 16 +--- .../shared/visualizations/axis-options-all.md | 21 +++++ .../axis-options-state-status.md | 10 +++ .../x/StateTimelinePanelCfg_types.gen.ts | 2 +- .../x/StatusHistoryPanelCfg_types.gen.ts | 2 +- .../grafana-ui/src/options/builder/axis.tsx | 88 +++++++++++-------- .../core/components/TimelineChart/utils.ts | 14 ++- public/app/plugins/panel/barchart/module.tsx | 2 +- .../state-timeline/StateTimelinePanel.tsx | 20 +++-- .../plugins/panel/state-timeline/module.tsx | 7 +- .../plugins/panel/state-timeline/panelcfg.cue | 1 + .../panel/state-timeline/panelcfg.gen.ts | 2 +- .../status-history/StatusHistoryPanel.tsx | 27 ++++-- .../plugins/panel/status-history/module.tsx | 7 +- .../plugins/panel/status-history/panelcfg.cue | 1 + .../panel/status-history/panelcfg.gen.ts | 2 +- 18 files changed, 149 insertions(+), 81 deletions(-) create mode 100644 docs/sources/shared/visualizations/axis-options-all.md create mode 100644 docs/sources/shared/visualizations/axis-options-state-status.md diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index 1ef99435336..a6916eb1f83 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -149,6 +149,10 @@ The **Page size** option lets you paginate the state timeline visualization to l {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" leveloffset="+1" >}} +### Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ### Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md index b7825b94b82..3ca5df864e6 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -131,6 +131,10 @@ Controls the opacity of state regions. {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} +## Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ## Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index e7f21000554..e54509d5aeb 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -209,21 +209,7 @@ The following example shows three series: Min, Max, and Value. The Min and Max s ### Axis options -Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. - -| Option | Description | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Time zone | Set the desired time zones to display along the x-axis. | -| [Placement](#placement) | Select the placement of the y-axis. | -| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | -| Width | Set a fixed width of the axis. By default, Grafana dynamically calculates the width of an axis. By setting the width of the axis, data with different axes types can share the same display proportions. This setting makes it easier for you to compare more than one graph’s worth of data because the axes aren't shifted or stretched within visual proximity to each other. | -| Show grid lines | Set the axis grid line visibility.
| -| Color | Set the color of the axis. | -| Show border | Set the axis border visibility. | -| Scale | Set the y-axis values scale.
| -| Centered zero | Set the y-axis so it's centered on zero. | -| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | -| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | +{{< docs/shared lookup="visualizations/axis-options-all.md" source="grafana" version="" leveloffset="+1" >}} #### Placement diff --git a/docs/sources/shared/visualizations/axis-options-all.md b/docs/sources/shared/visualizations/axis-options-all.md new file mode 100644 index 00000000000..e2c833ab9e2 --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-all.md @@ -0,0 +1,21 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: time series. +--- + +Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. + +| Option | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Time zone | Set the desired time zones to display along the x-axis. | +| [Placement](#placement) | Select the placement of the y-axis. | +| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | +| Show grid lines | Set the axis grid line visibility.
| +| Color | Set the color of the axis. | +| Show border | Set the axis border visibility. | +| Scale | Set the y-axis values scale.
| +| Centered zero | Set the y-axis so it's centered on zero. | +| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | +| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | diff --git a/docs/sources/shared/visualizations/axis-options-state-status.md b/docs/sources/shared/visualizations/axis-options-state-status.md new file mode 100644 index 00000000000..8b1bacfd78c --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-state-status.md @@ -0,0 +1,10 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: state timeline, status history. +--- + +| Option | Description | +| --------- | ------------------------------------------------------------------------------------------------ | +| Placement | Control the visibility of series names along the y-axis or time values along the x-axis. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index 72c3097c014..a62b8181e42 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -43,7 +43,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 8b0b21a3723..1efeeb3dec4 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -33,7 +33,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-ui/src/options/builder/axis.tsx b/packages/grafana-ui/src/options/builder/axis.tsx index 2e08654298c..304dc78268a 100644 --- a/packages/grafana-ui/src/options/builder/axis.tsx +++ b/packages/grafana-ui/src/options/builder/axis.tsx @@ -14,49 +14,32 @@ import { Stack } from '../../components/Layout/Stack/Stack'; import { Select } from '../../components/Select/Select'; import { graphFieldOptions } from '../../components/uPlot/config'; +const category = ['Axis']; + /** * @alpha */ -export function addAxisConfig( - builder: FieldConfigEditorBuilder, - defaultConfig: AxisConfig, - hideScale?: boolean -) { - const category = ['Axis']; - +export function addAxisConfig(builder: FieldConfigEditorBuilder, defaultConfig: AxisConfig) { // options for axis appearance + addAxisPlacement(builder); + + builder.addTextInput({ + path: 'axisLabel', + name: 'Label', + category, + defaultValue: '', + settings: { + placeholder: 'Optional text', + expandTemplateVars: true, + }, + showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, + // Do not apply default settings to time and string fields which are used as x-axis fields in Time series and Bar chart panels + shouldApply: (f) => f.type !== FieldType.time && f.type !== FieldType.string, + }); + + addAxisWidth(builder); + builder - .addRadio({ - path: 'axisPlacement', - name: 'Placement', - category, - defaultValue: graphFieldOptions.axisPlacement[0].value, - settings: { - options: graphFieldOptions.axisPlacement, - }, - }) - .addTextInput({ - path: 'axisLabel', - name: 'Label', - category, - defaultValue: '', - settings: { - placeholder: 'Optional text', - expandTemplateVars: true, - }, - showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, - // Do not apply default settings to time and string fields which are used as x-axis fields in Time series and Bar chart panels - shouldApply: (f) => f.type !== FieldType.time && f.type !== FieldType.string, - }) - .addNumberInput({ - path: 'axisWidth', - name: 'Width', - category, - settings: { - placeholder: 'Auto', - }, - showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, - }) .addRadio({ path: 'axisGridShow', name: 'Show grid lines', @@ -209,3 +192,32 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps ); }; + +/** @internal */ +export function addAxisWidth(builder: FieldConfigEditorBuilder) { + builder.addNumberInput({ + path: 'axisWidth', + name: 'Width', + category, + settings: { + placeholder: 'Auto', + }, + showIf: (c) => c.axisPlacement !== AxisPlacement.Hidden, + }); +} + +/** @internal */ +export function addAxisPlacement( + builder: FieldConfigEditorBuilder, + optionsFilter = (placement: AxisPlacement) => true +) { + builder.addRadio({ + path: 'axisPlacement', + name: 'Placement', + category, + defaultValue: graphFieldOptions.axisPlacement[0].value, + settings: { + options: graphFieldOptions.axisPlacement.filter((placement) => optionsFilter(placement.value!)), + }, + }); +} diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 0057b21331d..5a727990631 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -51,6 +51,7 @@ interface UPlotConfigOptions { mergeValues?: boolean; getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string; hoverMulti: boolean; + axisWidth?: number; } /** @@ -161,25 +162,32 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( range: coreConfig.yRange, }); + const xAxisHidden = frame.fields[0].config.custom.axisPlacement === AxisPlacement.Hidden; + builder.addAxis({ + show: !xAxisHidden, scaleKey: xScaleKey, isTime: true, splits: coreConfig.xSplits!, placement: AxisPlacement.Bottom, timeZone: timeZones[0], theme, - grid: { show: true }, }); + const yCustomConfig = frame.fields[1].config.custom; + const yAxisWidth = yCustomConfig.axisWidth; + const yAxisHidden = yCustomConfig.axisPlacement === AxisPlacement.Hidden; + builder.addAxis({ scaleKey: FIXED_UNIT, // y isTime: false, placement: AxisPlacement.Left, splits: coreConfig.ySplits, - values: coreConfig.yValues, + values: yAxisHidden ? (u, splits) => splits.map((v) => null) : coreConfig.yValues, grid: { show: false }, ticks: { show: false }, - gap: 16, + gap: yAxisHidden ? 0 : 16, + size: yAxisHidden ? 0 : yAxisWidth, theme, }); diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 35d1f095828..5b5ae542531 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -103,7 +103,7 @@ export const plugin = new PanelPlugin(BarChartPanel) shouldApply: () => true, }); - commonOptionsBuilder.addAxisConfig(builder, cfg, false); + commonOptionsBuilder.addAxisConfig(builder, cfg); commonOptionsBuilder.addHideFrom(builder); }, }) diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 81e272a6b98..4c57d757a05 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -4,6 +4,7 @@ import { useMeasure } from 'react-use'; import { DashboardCursorSync, DataFrame, PanelProps } from '@grafana/data'; import { + AxisPlacement, EventBusPlugin, Pagination, TooltipDisplayMode, @@ -209,15 +210,16 @@ export const StateTimelinePanel = ({ maxWidth={options.tooltip.maxWidth} /> )} - {/* Renders annotations */} - + {alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && ( + + )} ); diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 972194fc52d..024d1e883ac 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -5,7 +5,7 @@ import { identityOverrideProcessor, PanelPlugin, } from '@grafana/data'; -import { VisibilityMode } from '@grafana/schema'; +import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { InsertNullsEditor } from '../timeseries/InsertNullsEditor'; @@ -76,6 +76,11 @@ export const plugin = new PanelPlugin(StateTimelinePanel) }); commonOptionsBuilder.addHideFrom(builder); + commonOptionsBuilder.addAxisPlacement( + builder, + (placement) => placement === AxisPlacement.Auto || placement === AxisPlacement.Hidden + ); + commonOptionsBuilder.addAxisWidth(builder); }, }) .setPanelOptions((builder) => { diff --git a/public/app/plugins/panel/state-timeline/panelcfg.cue b/public/app/plugins/panel/state-timeline/panelcfg.cue index a186f5e12e8..5a9c34e2a28 100644 --- a/public/app/plugins/panel/state-timeline/panelcfg.cue +++ b/public/app/plugins/panel/state-timeline/panelcfg.cue @@ -41,6 +41,7 @@ composableKinds: PanelCfg: { perPage?: number & >=1 | *20 } @cuetsy(kind="interface") FieldConfig: { + ui.AxisConfig ui.HideableFieldConfig lineWidth?: uint32 & <=10 | *0 fillOpacity?: uint32 & <=100 | *70 diff --git a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts index 0fc25fe65b8..5ea1b58af54 100644 --- a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts +++ b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts @@ -41,7 +41,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index fd1164c85f3..237f1469723 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -1,7 +1,14 @@ import { useMemo, useState } from 'react'; import { DashboardCursorSync, PanelProps } from '@grafana/data'; -import { EventBusPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext, useTheme2 } from '@grafana/ui'; +import { + AxisPlacement, + EventBusPlugin, + TooltipDisplayMode, + TooltipPlugin2, + usePanelContext, + useTheme2, +} from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { @@ -141,14 +148,16 @@ export const StatusHistoryPanel = ({ maxWidth={options.tooltip.maxWidth} /> )} - + {alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && ( + + )} ); diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 8d42b1c4d1b..58ff47be783 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -1,5 +1,5 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; -import { VisibilityMode } from '@grafana/schema'; +import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { StatusHistoryPanel } from './StatusHistoryPanel'; @@ -42,6 +42,11 @@ export const plugin = new PanelPlugin(StatusHistoryPanel) }); commonOptionsBuilder.addHideFrom(builder); + commonOptionsBuilder.addAxisPlacement( + builder, + (placement) => placement === AxisPlacement.Auto || placement === AxisPlacement.Hidden + ); + commonOptionsBuilder.addAxisWidth(builder); }, }) .setPanelOptions((builder) => { diff --git a/public/app/plugins/panel/status-history/panelcfg.cue b/public/app/plugins/panel/status-history/panelcfg.cue index ec5ab6c4fc7..55a96b501d2 100644 --- a/public/app/plugins/panel/status-history/panelcfg.cue +++ b/public/app/plugins/panel/status-history/panelcfg.cue @@ -37,6 +37,7 @@ composableKinds: PanelCfg: { colWidth?: float & <=1 | *0.9 } @cuetsy(kind="interface") FieldConfig: { + ui.AxisConfig ui.HideableFieldConfig lineWidth?: uint32 & <=10 | *1 fillOpacity?: uint32 & <=100 | *70 diff --git a/public/app/plugins/panel/status-history/panelcfg.gen.ts b/public/app/plugins/panel/status-history/panelcfg.gen.ts index 8bc46e363d0..fac1c7e19df 100644 --- a/public/app/plugins/panel/status-history/panelcfg.gen.ts +++ b/public/app/plugins/panel/status-history/panelcfg.gen.ts @@ -31,7 +31,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } From 572be19f760640a8d3463b0aa1b1b50e60d76c34 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:47:42 -0600 Subject: [PATCH 24/29] DataLinks: Add one-click option (#98563) Co-authored-by: Leon Sorokin --- .../grafana-data/src/field/fieldOverrides.ts | 2 + packages/grafana-data/src/types/dataLink.ts | 3 ++ .../components/DataLinks/DataLinkEditor.tsx | 16 +++++- .../DataLinksInlineEditor.tsx | 9 ++++ .../DataLinksListItem.tsx | 11 +++- .../VizTooltip/VizTooltipFooter.tsx | 34 ++++++++++-- .../uPlot/plugins/TooltipPlugin2.tsx | 54 ++++++++++++++----- .../features/actions/ActionsInlineEditor.tsx | 2 +- .../app/plugins/panel/status-history/utils.ts | 2 + .../panel/timeseries/TimeSeriesTooltip.tsx | 14 +++-- public/locales/en-US/grafana.json | 11 +++- public/locales/pseudo-LOCALE/grafana.json | 11 +++- 12 files changed, 139 insertions(+), 30 deletions(-) diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 0924a417231..7e9aa1052aa 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -501,6 +501,7 @@ export const getLinksSupplier = }); }, origin: field, + oneClick: link.oneClick ?? false, }; } else { linkModel = { @@ -508,6 +509,7 @@ export const getLinksSupplier = title: replaceVariables(link.title || '', dataLinkScopedVars), target: link.targetBlank ? '_blank' : undefined, origin: field, + oneClick: link.oneClick ?? false, }; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 476f2441ed7..14ee3ad4301 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -55,6 +55,8 @@ export interface DataLink { correlationData?: ExploreCorrelationHelperData; transformations?: DataLinkTransformationConfig[]; }; + + oneClick?: boolean; } /** @@ -98,6 +100,7 @@ export interface LinkModel { // When a click callback exists, this is passed the raw mouse|react event onClick?: (e: any, origin?: any) => void; + oneClick?: boolean; } /** diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 98f5d67227f..cdd5e64eddf 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -5,7 +5,7 @@ import { VariableSuggestion, GrafanaTheme2, DataLink } from '@grafana/data'; import { useStyles2 } from '../../themes/index'; import { isCompactUrl } from '../../utils/dataLinks'; -import { Trans } from '../../utils/i18n'; +import { t, Trans } from '../../utils/i18n'; import { Field } from '../Forms/Field'; import { Input } from '../Input/Input'; import { Switch } from '../Switch/Switch'; @@ -45,6 +45,10 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas onChange(index, { ...value, targetBlank: !value.targetBlank }); }; + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; + return (
@@ -63,6 +67,16 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas + + + + {isLast && (
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index a431e241479..fa4e61589f3 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -51,6 +51,15 @@ export const DataLinksInlineEditor = ({ setIsNew(false); } } + + if (link.oneClick === true) { + linksSafe.forEach((link) => { + if (link.oneClick) { + link.oneClick = false; + } + }); + } + const update = cloneDeep(linksSafe); update[index] = link; onChange(update); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 939a4ec15b7..1c12a41dd2a 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -5,6 +5,8 @@ import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { isCompactUrl } from '../../../utils'; +import { t } from '../../../utils/i18n'; +import { Badge } from '../../Badge/Badge'; import { Icon } from '../../Icon/Icon'; import { IconButton } from '../../IconButton/IconButton'; import { Tooltip } from '../../Tooltip/Tooltip'; @@ -22,7 +24,7 @@ export interface DataLinksListItemProps { export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { const styles = useStyles2(getDataLinkListItemStyles); - const { title = '', url = '' } = link; + const { title = '', url = '', oneClick = false } = link; const hasTitle = title.trim() !== ''; const hasUrl = url.trim() !== ''; @@ -52,6 +54,13 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
+ {oneClick && ( + + )}
diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index 05b05f7518d..c3c4a34eb64 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; -import { Button, DataLinkButton, Stack } from '..'; +import { Button, DataLinkButton, Icon, Stack } from '..'; import { useStyles2 } from '../../themes'; import { Trans } from '../../utils/i18n'; import { ActionButton } from '../Actions/ActionButton'; @@ -16,6 +16,21 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType) => { + const oneClickLink = dataLinks.find((link) => link.oneClick === true); + + if (oneClickLink != null) { + return ( + + + + + Click to open {{ linkTitle: oneClickLink.title }} + + + + ); + } + return ( {dataLinks.map((link, i) => ( @@ -35,14 +50,15 @@ const renderActions = (actions: ActionModel[]) => { ); }; -export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFooterProps) => { +export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); + const hasOneClickLink = dataLinks.some((link) => link.oneClick === true); return (
- {dataLinks?.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} - {actions && actions.length > 0 &&
{renderActions(actions)}
} - {annotate != null && ( + {dataLinks.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} + {!hasOneClickLink && actions.length > 0 &&
{renderActions(actions)}
} + {!hasOneClickLink && annotate != null && (