Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2025-02-04 12:42:22 +03:00
commit e1f075bdde
99 files changed
+1622 -353

No files matched your search

+71 -41
View File
@@ -18,7 +18,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
@@ -69,7 +69,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- go install github.com/bazelbuild/buildtools/buildifier@latest
@@ -112,7 +112,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -170,7 +170,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -309,7 +309,7 @@ steps:
path: /github-app
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -339,6 +339,16 @@ steps:
- yarn-install
image: node:22-bookworm
name: verify-i18n
- commands:
- yarn generate-apis
- "\n file_diff=$(git diff ':!conf')\n if [ -n \"$file_diff\"
]; then\n echo $file_diff\n echo \"\nAPI client
generation has not been committed. Please run 'yarn generate-apis', commit the
changes and push again.\"\n exit 1\n fi\n "
depends_on:
- yarn-install
image: node:22-bookworm
name: verify-api-clients
trigger:
event:
- pull_request
@@ -417,7 +427,7 @@ steps:
path: /github-app
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -506,7 +516,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
@@ -630,7 +640,7 @@ steps:
path: /github-app
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- mkdir -p bin
@@ -699,7 +709,7 @@ steps:
-a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu
-a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.5
--tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{
.version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD
> packages.txt
@@ -756,7 +766,7 @@ steps:
GF_APP_MODE: development
GF_SERVER_HTTP_PORT: "3001"
GF_SERVER_ROUTER_LOGGING: "1"
image: alpine:3.20.3
image: alpine:3.20.5
name: grafana-server
- commands:
- ./bin/build e2e-tests --port 3001 --suite dashboards-suite
@@ -1100,7 +1110,7 @@ steps:
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1266,7 +1276,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -1623,7 +1633,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -1694,7 +1704,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -1752,7 +1762,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -1782,6 +1792,16 @@ steps:
- yarn-install
image: node:22-bookworm
name: verify-i18n
- commands:
- yarn generate-apis
- "\n file_diff=$(git diff ':!conf')\n if [ -n \"$file_diff\"
]; then\n echo $file_diff\n echo \"\nAPI client
generation has not been committed. Please run 'yarn generate-apis', commit the
changes and push again.\"\n exit 1\n fi\n "
depends_on:
- yarn-install
image: node:22-bookworm
name: verify-api-clients
trigger:
branch: main
event:
@@ -1818,7 +1838,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1898,7 +1918,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
@@ -1964,7 +1984,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -2039,7 +2059,7 @@ steps:
path: /github-app
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- mkdir -p bin
@@ -2107,7 +2127,7 @@ steps:
-a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu
-a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.5
--tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{
.version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD
> packages.txt
@@ -2168,7 +2188,7 @@ steps:
GF_APP_MODE: development
GF_SERVER_HTTP_PORT: "3001"
GF_SERVER_ROUTER_LOGGING: "1"
image: alpine:3.20.3
image: alpine:3.20.5
name: grafana-server
- commands:
- ./bin/build e2e-tests --port 3001 --suite dashboards-suite
@@ -2585,7 +2605,7 @@ steps:
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -2830,7 +2850,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -2886,7 +2906,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -2916,6 +2936,16 @@ steps:
- yarn-install
image: node:22-bookworm
name: verify-i18n
- commands:
- yarn generate-apis
- "\n file_diff=$(git diff ':!conf')\n if [ -n \"$file_diff\"
]; then\n echo $file_diff\n echo \"\nAPI client
generation has not been committed. Please run 'yarn generate-apis', commit the
changes and push again.\"\n exit 1\n fi\n "
depends_on:
- yarn-install
image: node:22-bookworm
name: verify-api-clients
trigger:
branch:
- instant
@@ -2950,7 +2980,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -3028,7 +3058,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
@@ -3133,7 +3163,7 @@ steps:
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -3336,7 +3366,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- mkdir -p bin
@@ -3468,7 +3498,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- mkdir -p bin
@@ -3958,7 +3988,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4033,7 +4063,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4195,7 +4225,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4297,7 +4327,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- yarn install --immutable || yarn install --immutable
@@ -4351,7 +4381,7 @@ services: []
steps:
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -4432,7 +4462,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4576,7 +4606,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4704,7 +4734,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
ALPINE_BASE: alpine:3.20.3
ALPINE_BASE: alpine:3.20.5
CDN_DESTINATION:
from_secret: rgm_cdn_destination
DESTINATION:
@@ -4843,7 +4873,7 @@ steps:
name: grabpl
- commands:
- echo $DRONE_RUNNER_NAME
image: alpine:3.20.3
image: alpine:3.20.5
name: identify-runner
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -5267,7 +5297,7 @@ steps:
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.3
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.5
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack
@@ -5305,7 +5335,7 @@ steps:
- trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm
- trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0
- trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3
- trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.3
- trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.5
- trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04
- trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8
- trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack
@@ -5569,6 +5599,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 154c1f072294cef09ba4f579eed30b1e29bc5c263897c862f4574432d321502c
hmac: abd48dfdec08a719b414fba06b06dfc97560df31d81751cd833b5fdf5b7f9937
...
@@ -15,9 +15,9 @@ title: Migrate from Grafana OSS/Enterprise to Grafana Cloud
When you decide to migrate from your self-managed Grafana instance to Grafana Cloud, you can benefit from the convenience of a managed observability platform, additional cloud-only features, and robust security. There are a couple of key approaches to help you transition to Grafana Cloud.
| Migration type | Tools used | Availability | Migratable resources |
| :------------- | :-------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Manual | <ul><li>Command line utilities</li><li>The Grafana HTTP API</li></ul> | Generally available in all versions of Grafana OSS/Enterprise | The entire Grafana instance |
| Automated | The Grafana Cloud Migration Assistant | Available in public preview from Grafana v11.2 using the `OnPremToCloudMigration` feature toggle. This toggle is enabled by default in Grafana v11.5 and later. | <ul><li>Dashboards</li><li>Folders</li>Data sources</li><li>App Plugins</li>Panel Plugins</li><li>Library Panels</li><li>Grafana Alerting resources</li></ul> |
| Migration type | Tools used | Availability | Migratable resources |
| :------------- | :-------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Manual | <ul><li>Command line utilities</li><li>The Grafana HTTP API</li></ul> | Generally available in all versions of Grafana OSS/Enterprise | The entire Grafana instance |
| Automated | The Grafana Cloud Migration Assistant | Available in public preview from Grafana v11.2 using the `onPremToCloudMigrations` feature toggle. This toggle is enabled by default in Grafana v11.5 and later. | <ul><li>Dashboards</li><li>Folders</li><li>Data sources</li><li>App Plugins</li><li>Panel Plugins</li><li>Library Panels</li><li>Grafana Alerting resources</li></ul> |
Our detailed [migration guide](https://www.grafana.com/docs/grafana-cloud/account-management/migration-guide/manually-migrate-to-grafana-cloud/) explains the key steps and scripts to manually migrate your resources to Grafana Cloud, covering a comprehensive set of resources in your Grafana instance. Alternatively, the [Grafana Cloud Migration Assistant](https://www.grafana.com/docs/grafana-cloud/account-management/migration-guide/cloud-migration-assistant/), available in public preview in Grafana v11.2 and later, automates the migration process across a broad range of Grafana resources. You can use the migration assistant to migrate a large proportion of your Grafana resources and then, if needed, leverage the migration guide to migrate the rest.
@@ -58,7 +58,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi
### Grant access in Grafana Enterprise
{{< admonition type="impotrtant">}}
{{< admonition type="important">}}
You must [configure RBAC](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant.
{{< /admonition >}}
@@ -87,7 +87,7 @@ In [Configure no data and error handling](ref:no-data-and-error-handling), you c
Note that `No Data` and `Error` states are supported only for Grafana-managed alert rules.
{{< figure src="/media/docs/alerting/alert-rule-configure-no-data-and-error.png" alt="A screenshot of the `Configure no data and error handling` option in Grafana Alerting." max-width="500px" >}}
{{< figure src="/media/docs/alerting/alert-rule-configure-no-data-and-error-v2.png" alt="A screenshot of the `Configure no data and error handling` option in Grafana Alerting." max-width="500px" >}}
{{< docs/shared lookup="alerts/table-configure-no-data-and-error.md" source="grafana" version="<GRAFANA_VERSION>" >}}
@@ -1,6 +1,6 @@
{
"name": "@test-plugins/extensions-test-app",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"private": true,
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
@@ -1,6 +1,6 @@
{
"name": "@test-plugins/grafana-e2etest-datasource",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"private": true,
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
@@ -11,7 +11,7 @@
"author": "Grafana",
"license": "Apache-2.0",
"devDependencies": {
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/plugin-configs": "workspace:*",
"@types/lodash": "4.17.7",
"@types/node": "22.10.2",
"@types/prismjs": "1.26.4",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"npmClient": "yarn",
"version": "11.5.0-pre"
"version": "11.6.0-pre"
}
+2 -2
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"build": "NODE_ENV=production nx exec --verbose -- webpack --config scripts/webpack/webpack.prod.js --progress",
@@ -62,7 +62,7 @@
"plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'",
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
"generate-icons": "nx run grafana-icons:generate",
"process-specs": "npx tsx scripts/process-specs.ts",
"process-specs": "node --experimental-strip-types scripts/process-specs.ts",
"generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts"
},
"grafana": {
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -36,7 +36,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",
"@grafana/schema": "11.5.0-pre",
"@grafana/schema": "11.6.0-pre",
"@types/d3-interpolate": "^3.0.0",
"@types/string-hash": "1.1.3",
"d3-interpolate": "3.0.1",
@@ -251,6 +251,7 @@ export interface FeatureToggles {
queryLibraryDashboards?: boolean;
grafanaAdvisor?: boolean;
elasticsearchImprovedParsing?: boolean;
exploreMetricsUseExternalAppPlugin?: boolean;
datasourceConnectionsTab?: boolean;
fetchRulesUsingPost?: boolean;
alertingAlertmanagerExtraDedupStage?: boolean;
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@grafana/eslint-plugin",
"description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"main": "./index.cjs",
"author": "Grafana Labs",
"license": "Apache-2.0",
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/flamegraph",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana flamegraph visualization component",
"keywords": [
"grafana",
@@ -44,8 +44,8 @@
],
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"@leeoniya/ufuzzy": "1.0.18",
"d3": "^7.8.5",
"lodash": "4.17.21",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@grafana/saga-icons",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"private": true,
"description": "Icons for Grafana",
"author": "Grafana Labs",
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"name": "@grafana/o11y-ds-frontend",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Library to manage traces in Grafana.",
"sideEffects": false,
"repository": {
@@ -18,12 +18,12 @@
},
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"react-select": "5.10.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@grafana/plugin-configs",
"description": "Shared dependencies and files for core plugins",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"tslib": "2.8.1"
},
+6 -6
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "AGPL-3.0-only",
"name": "@grafana/prometheus",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana Prometheus Library",
"keywords": [
"typescript"
@@ -38,13 +38,13 @@
"dependencies": {
"@emotion/css": "11.13.5",
"@floating-ui/react": "0.27.3",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/faro-web-sdk": "1.12.3",
"@grafana/llm": "0.12.0",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"@hello-pangea/dnd": "17.0.0",
"@leeoniya/ufuzzy": "1.0.18",
"@lezer/common": "1.2.3",
@@ -77,7 +77,7 @@
},
"devDependencies": {
"@emotion/eslint-plugin": "11.12.0",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/tsconfig": "^2.0.0",
"@rollup/plugin-image": "3.0.3",
"@rollup/plugin-node-resolve": "16.0.0",
+5 -5
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -37,11 +37,11 @@
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"history": "4.10.1",
"lodash": "4.17.21",
"react-use": "17.6.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
limit: number;
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting {
/**
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.OptionsWithLegend, common.SingleStatBaseOptions {
displayMode: common.BarGaugeDisplayMode;
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export enum VizDisplayMode {
Candles = 'candles',
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export enum HorizontalConstraint {
Center = 'center',
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface MetricStat {
/**
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
/**
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
selectedSeries: number;
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export type UpdateConfig = {
render: boolean,
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.SingleStatBaseOptions {
minVizHeight: number;
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
basemap: ui.MapLayerOptions;
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
/**
* Controls the color mode of the heatmap
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
/**
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
dedupStrategy: common.LogsDedupStrategy;
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export enum QueryEditorMode {
Builder = 'builder',
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
/**
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface ArcOption {
/**
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
/**
* Select the pie chart display style.
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.SingleStatBaseOptions {
colorMode: common.BigValueColorMode;
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
/**
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones {
/**
@@ -10,7 +10,7 @@
import * as ui from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options {
/**
@@ -8,7 +8,7 @@
//
// Run 'make gen-cue' from repository root to regenerate.
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export enum TextMode {
Code = 'code',
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export interface Options extends common.OptionsWithTimezones {
legend: common.VizLegendOptions;
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
/**
* Identical to timeseries... except it does not have timezone settings
@@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "11.5.0-pre";
export const pluginVersion = "11.6.0-pre";
export enum PointShape {
Circle = 'circle',
+5 -5
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "@grafana/sql",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git",
@@ -15,11 +15,11 @@
},
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"@react-awesome-query-builder/ui": "6.6.4",
"immutable": "5.0.3",
"lodash": "4.17.21",
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -50,10 +50,10 @@
"@emotion/react": "11.14.0",
"@emotion/serialize": "1.3.3",
"@floating-ui/react": "0.27.3",
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
"@grafana/schema": "11.5.0-pre",
"@grafana/schema": "11.6.0-pre",
"@hello-pangea/dnd": "17.0.0",
"@leeoniya/ufuzzy": "1.0.18",
"@monaco-editor/react": "4.6.0",
@@ -2,6 +2,7 @@ package ossaccesscontrol
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -106,6 +107,17 @@ func ProvideFolderPermissions(
})
if err != nil {
// if the folder is not found, this may be on the create path,
// where the write path to legacy will then go through the read
// path and try to read from both legacy & unified before it exists on both
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) && errors.Is(err, dashboards.ErrFolderNotFound) {
_, err = folderService.GetLegacy(ctx, &folder.GetFolderQuery{
UID: &resourceID,
OrgID: orgID,
SignedInUser: ident,
})
return err
}
return err
}
+10 -1
View File
@@ -912,7 +912,7 @@ var (
Stage: FeatureStageGeneralAvailability,
Expression: "true", // enabled by default
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
Owner: grafanaObservabilityMetricsSquad,
},
{
Name: "alertingSimplifiedRouting",
@@ -1742,6 +1742,15 @@ var (
Stage: FeatureStageExperimental,
Owner: awsDatasourcesSquad,
},
{
Name: "exploreMetricsUseExternalAppPlugin",
Description: "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
FrontendOnly: true,
RequiresRestart: true,
HideFromDocs: true,
},
{
Name: "datasourceConnectionsTab",
Description: "Shows defined connections for a data source in the plugins detail page",
+2 -1
View File
@@ -120,7 +120,7 @@ pdfTables,preview,@grafana/sharing-squad,false,false,false
ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
exploreMetrics,GA,@grafana/dashboards-squad,false,false,true
exploreMetrics,GA,@grafana/observability-metrics,false,false,true
alertingSimplifiedRouting,GA,@grafana/alerting-squad,false,false,false
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false
@@ -232,6 +232,7 @@ ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false
queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false
elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false
exploreMetricsUseExternalAppPlugin,experimental,@grafana/observability-metrics,false,true,true
datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,false,true
fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false
alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
120 ssoSettingsApi GA @grafana/identity-access-team false false false
121 canvasPanelPanZoom preview @grafana/dataviz-squad false false true
122 logsInfiniteScrolling GA @grafana/observability-logs false false true
123 exploreMetrics GA @grafana/dashboards-squad @grafana/observability-metrics false false true
124 alertingSimplifiedRouting GA @grafana/alerting-squad false false false
125 logRowsPopoverMenu GA @grafana/observability-logs false false true
126 pluginsSkipHostEnvVars experimental @grafana/plugins-platform-backend false false false
232 queryLibraryDashboards experimental @grafana/grafana-frontend-platform false false false
233 grafanaAdvisor experimental @grafana/plugins-platform-backend false false false
234 elasticsearchImprovedParsing experimental @grafana/aws-datasources false false false
235 exploreMetricsUseExternalAppPlugin experimental @grafana/observability-metrics false true true
236 datasourceConnectionsTab experimental @grafana/plugins-platform-backend false false true
237 fetchRulesUsingPost experimental @grafana/alerting-squad false false false
238 alertingAlertmanagerExtraDedupStage experimental @grafana/alerting-squad false true false
+4
View File
@@ -939,6 +939,10 @@ const (
// Enables less memory intensive Elasticsearch result parsing
FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing"
// FlagExploreMetricsUseExternalAppPlugin
// Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin
FlagExploreMetricsUseExternalAppPlugin = "exploreMetricsUseExternalAppPlugin"
// FlagDatasourceConnectionsTab
// Shows defined connections for a data source in the plugins detail page
FlagDatasourceConnectionsTab = "datasourceConnectionsTab"
+21 -3
View File
@@ -1529,16 +1529,16 @@
{
"metadata": {
"name": "exploreMetrics",
"resourceVersion": "1720021873452",
"resourceVersion": "1737658563230",
"creationTimestamp": "2024-04-09T18:15:18Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC"
"grafana.app/updatedTimestamp": "2025-01-23 18:56:03.23086 +0000 UTC"
}
},
"spec": {
"description": "Enables the new Explore Metrics core app",
"stage": "GA",
"codeowner": "@grafana/dashboards-squad",
"codeowner": "@grafana/observability-metrics",
"frontend": true,
"expression": "true"
}
@@ -1557,6 +1557,24 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "exploreMetricsUseExternalAppPlugin",
"resourceVersion": "1738596266973",
"creationTimestamp": "2025-01-21T23:24:50Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-02-03 15:24:26.973231 +0000 UTC"
}
},
"spec": {
"description": "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin",
"stage": "experimental",
"codeowner": "@grafana/observability-metrics",
"frontend": true,
"requiresRestart": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "expressionParser",
@@ -36,11 +36,13 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// interface to allow for testing
type folderK8sHandler interface {
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
getNamespace(orgID int64) string
getSearcher(ctx context.Context) resource.ResourceClient
}
@@ -688,7 +690,7 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
// if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later
if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
// We need a list of dashboard uids inside the folder to delete related public dashboards
// We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards
var dashboardUIDs []string
// we cannot use the dashboard service directly due to circular dependencies,
// so either use the search client if the feature is enabled or use the dashboard store
@@ -722,10 +724,17 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
if err != nil {
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
}
dashboardUIDs = make([]string, len(hits.Hits))
k8sDeleteClient, created := s.k8sclient.getDashboardClient(ctx, cmd.OrgID)
if !created {
return folder.ErrInternal.Errorf("failed to create client to get dashboards")
}
for i, dashboard := range hits.Hits {
dashboardUIDs[i] = dashboard.Name
err = k8sDeleteClient.Delete(ctx, dashboard.Name, metav1.DeleteOptions{})
if err != nil {
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
}
}
} else {
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folders, OrgId: cmd.OrgID})
@@ -735,6 +744,13 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
dashboardUIDs = make([]string, len(dashes))
for i, dashboard := range dashes {
dashboardUIDs[i] = dashboard.UID
err = s.dashboardStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
UID: dashboard.UID,
OrgID: cmd.OrgID,
})
if err != nil {
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
}
}
}
// Delete all public dashboards in the folders
@@ -986,6 +1002,20 @@ func (fk8s *foldk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic
return dyn.Resource(fk8s.gvr).Namespace(fk8s.getNamespace(orgID)), true
}
func (fk8s *foldk8sHandler) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
cfg := fk8s.restConfigProvider(ctx)
if cfg == nil {
return nil, false
}
dyn, err := dynamic.NewForConfig(cfg)
if err != nil {
return nil, false
}
return dyn.Resource(dashboardv0.DashboardResourceInfo.GroupVersionResource()).Namespace(fk8s.getNamespace(orgID)), true
}
func (fk8s *foldk8sHandler) getNamespace(orgID int64) string {
return fk8s.namespacer(orgID)
}
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
clientrest "k8s.io/client-go/rest"
@@ -682,6 +683,46 @@ func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSea
}, nil
}
if len(in.Options.Fields) > 0 &&
in.Options.Fields[0].Key == resource.SEARCH_FIELD_FOLDER &&
in.Options.Fields[0].Operator == "in" &&
len(in.Options.Fields[0].Values) > 0 {
rows := []*resource.ResourceTableRow{}
for i, row := range in.Options.Fields[0].Values {
rows = append(rows, &resource.ResourceTableRow{
Key: &resource.ResourceKey{
Name: row,
Resource: "folders",
},
Cells: [][]byte{
[]byte(fmt.Sprintf("%d", i)), // set legacy id as the row id
[]byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id
[]byte(""),
},
})
}
return &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "_id",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: rows,
},
TotalHits: int64(len(rows)),
}, nil
}
// not found
return &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{},
@@ -716,6 +757,11 @@ func (m *mockFoldersK8sCli) getClient(ctx context.Context, orgID int64) (dynamic
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
}
func (m *mockFoldersK8sCli) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
args := m.Called(ctx, orgID)
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
}
func (m *mockFoldersK8sCli) getNamespace(orgID int64) string {
if orgID == 1 {
return "default"
@@ -834,3 +880,105 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
require.Equal(t, expectedResult, result)
})
}
type mockDashboardCli struct {
mock.Mock
dynamic.ResourceInterface
}
func (c *mockDashboardCli) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
args := c.Called(ctx, name, options)
return args.Error(0)
}
func TestDeleteFoldersFromApiServer(t *testing.T) {
fakeK8sClient := new(mockFoldersK8sCli)
fakeFolderStore := folder.NewFakeStore()
dashboardStore := dashboards.NewFakeDashboardStore(t)
publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
service := Service{
k8sclient: fakeK8sClient,
unifiedStore: fakeFolderStore,
dashboardStore: dashboardStore,
publicDashboardService: publicDashboardFakeService,
registry: make(map[string]folder.RegistryService),
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
}
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
user := &user.SignedInUser{OrgID: 1}
ctx := identity.WithRequester(context.Background(), user)
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanSaveValue: true,
CanViewValue: true,
})
db, cfg := sqlstore.InitTestDB(t)
alertingStore := ngstore.DBstore{
SQLStore: db,
Cfg: cfg.UnifiedAlerting,
Logger: log.New("test-alerting-store"),
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
}
require.NoError(t, service.RegisterService(alertingStore))
t.Run("Should delete folder", func(t *testing.T) {
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
UID: "uid",
OrgID: 1,
SignedInUser: user,
})
require.NoError(t, err)
})
t.Run("Should delete dashboards and public dashboards within the folder", func(t *testing.T) {
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
{
UID: "test",
OrgID: 1,
},
{
UID: "test2",
OrgID: 1,
},
}, nil).Once()
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
UID: "test",
OrgID: 1,
}).Return(nil).Once()
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
UID: "test2",
OrgID: 1,
}).Return(nil).Once()
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"test", "test2"}).Return(nil).Once()
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
UID: "uid",
OrgID: 1,
SignedInUser: user,
})
require.NoError(t, err)
dashboardStore.AssertExpectations(t)
publicDashboardFakeService.AssertExpectations(t)
})
// enable k8s ff for dashboards, retest
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagKubernetesCliDashboards)
t.Run("Should delete dashboards and public dashboards within the folder through k8s if the ff is enabled", func(t *testing.T) {
dashboardK8sCli := mockDashboardCli{}
dashboardK8sCli.On("Delete", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(nil).Once()
fakeK8sClient.On("getDashboardClient", mock.Anything, mock.Anything).Return(&dashboardK8sCli, true)
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"uid1"}).Return(nil).Once()
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
UID: "uid1",
OrgID: 1,
SignedInUser: user,
})
require.NoError(t, err)
dashboardStore.AssertExpectations(t)
publicDashboardFakeService.AssertExpectations(t)
dashboardK8sCli.AssertExpectations(t)
})
}
+7 -3
View File
@@ -286,9 +286,9 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"},
"grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"},
"grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"},
"grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Logs"},
"grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Traces"},
"grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Profiles"},
"grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Logs"},
"grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"},
"grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"},
"grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"},
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"},
"grafana-irm-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "IRM"},
@@ -309,6 +309,10 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-csp-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCloudServiceProviders, Icon: "cloud"},
}
if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) {
s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"}
}
s.navigationAppPathConfig = map[string]NavigationAppConfig{
"/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2},
}
+1 -1
View File
@@ -562,7 +562,7 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n
func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink {
exploreChildNavs := []*navtree.NavLink{}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) {
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) {
exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{
Text: "Metrics",
SubTitle: "Queryless exploration of your metrics",
+25
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"slices"
"sort"
"strings"
"time"
@@ -330,6 +331,30 @@ func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string
return response.JSON(http.StatusOK, result)
}
func (srv RulerSrv) RouteGetRuleVersionsByUID(c *contextmodel.ReqContext, ruleUID string) response.Response {
ctx := c.Req.Context()
// make sure the user has access to the current version of the rule. Also, check if it exists
_, err := srv.getAuthorizedRuleByUid(ctx, c, ruleUID)
if err != nil {
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
return response.Empty(http.StatusNotFound)
}
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule by UID", err)
}
rules, err := srv.store.GetAlertRuleVersions(ctx, ngmodels.AlertRuleKey{OrgID: c.OrgID, UID: ruleUID})
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule history", err)
}
sort.Slice(rules, func(i, j int) bool { return rules[i].ID > rules[j].ID })
result := make(apimodels.GettableRuleVersions, 0, len(rules))
for _, rule := range rules {
// do not provide provenance status because we do not have historical changes for it
result = append(result, toGettableExtendedRuleNode(*rule, map[string]ngmodels.Provenance{}, srv.resolveUserIdToNameFn(ctx)))
}
return response.JSON(http.StatusOK, result)
}
func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceUID string) response.Response {
namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), namespaceUID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
+103
View File
@@ -448,6 +448,109 @@ func TestRouteGetRuleByUID(t *testing.T) {
})
}
func TestRouteGetRuleHistoryByUID(t *testing.T) {
orgID := rand.Int63()
f := randFolder()
groupKey := models.GenerateGroupKey(orgID)
groupKey.NamespaceUID = f.UID
gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey), models.RuleGen.WithUniqueID())
t.Run("rule history is successfully fetched with the correct UID", func(t *testing.T) {
ruleStore := fakes.NewRuleStore(t)
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f)
rule := gen.GenerateRef()
history := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3)
// simulate order of the history
rule.ID = 100
for i, alertRule := range history {
alertRule.ID = rule.ID - int64(i) - 1
}
ruleStore.PutRule(context.Background(), rule)
ruleStore.History[rule.GetKey()] = append(ruleStore.History[rule.GetKey()], history...)
perms := createPermissionsForRules([]*models.AlertRule{rule}, orgID)
req := createRequestContextWithPerms(orgID, perms, nil)
svc := createService(ruleStore)
response := svc.RouteGetRuleVersionsByUID(req, rule.UID)
require.Equal(t, http.StatusOK, response.Status())
var result apimodels.GettableRuleVersions
require.NoError(t, json.Unmarshal(response.Body(), &result))
require.NotNil(t, result)
require.Len(t, result, len(history)+1) // history + current version
t.Run("should be in correct order", func(t *testing.T) {
expectedHistory := append([]*models.AlertRule{rule}, history...)
for i, rul := range expectedHistory {
assert.Equal(t, rul.UID, result[i].GrafanaManagedAlert.UID)
}
})
})
t.Run("NotFound when rule does not exist", func(t *testing.T) {
ruleStore := fakes.NewRuleStore(t)
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f)
ruleKey := models.AlertRuleKey{
OrgID: orgID,
UID: "test",
}
history := gen.With(gen.WithKey(ruleKey)).GenerateManyRef(3)
ruleStore.History[ruleKey] = append(ruleStore.History[ruleKey], history...) // even if history is full of records
perms := createPermissionsForRules(history, orgID)
req := createRequestContextWithPerms(orgID, perms, nil)
response := createService(ruleStore).RouteGetRuleVersionsByUID(req, ruleKey.UID)
require.Equal(t, http.StatusNotFound, response.Status())
})
t.Run("Empty result when rule history is empty", func(t *testing.T) {
ruleStore := fakes.NewRuleStore(t)
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f)
ruleKey := models.AlertRuleKey{
OrgID: orgID,
UID: "test",
}
rule := gen.With(gen.WithKey(ruleKey)).GenerateRef()
ruleStore.PutRule(context.Background(), rule)
ruleStore.History[ruleKey] = nil
perms := createPermissionsForRules([]*models.AlertRule{rule}, orgID)
req := createRequestContextWithPerms(orgID, perms, nil)
response := createService(ruleStore).RouteGetRuleVersionsByUID(req, ruleKey.UID)
require.Equal(t, http.StatusOK, response.Status())
var result apimodels.GettableRuleVersions
require.NoError(t, json.Unmarshal(response.Body(), &result))
require.Empty(t, result)
})
t.Run("Unauthorized if user does not have access to the current rule", func(t *testing.T) {
ruleStore := fakes.NewRuleStore(t)
anotherFolder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f, anotherFolder)
ruleKey := models.AlertRuleKey{
OrgID: orgID,
UID: "test",
}
rule := gen.With(gen.WithKey(ruleKey), gen.WithNamespaceUID(anotherFolder.UID)).GenerateRef()
ruleStore.PutRule(context.Background(), rule)
history := gen.With(gen.WithKey(ruleKey)).GenerateManyRef(3)
ruleStore.History[ruleKey] = history
perms := createPermissionsForRules(history, orgID) // grant permissions to all records in history but not the rule itself
req := createRequestContextWithPerms(orgID, perms, nil)
response := createService(ruleStore).RouteGetRuleVersionsByUID(req, ruleKey.UID)
require.Equal(t, http.StatusForbidden, response.Status())
})
}
func TestRouteGetRulesConfig(t *testing.T) {
gen := models.RuleGen
t.Run("fine-grained access is enabled", func(t *testing.T) {
+2 -1
View File
@@ -41,7 +41,8 @@ func (api *API) authorize(method, path string) web.Handler {
case http.MethodGet + "/api/ruler/grafana/api/v1/rules",
http.MethodGet + "/api/ruler/grafana/api/v1/export/rules":
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
case http.MethodGet + "/api/ruler/grafana/api/v1/rule/{RuleUID}":
case http.MethodGet + "/api/ruler/grafana/api/v1/rule/{RuleUID}",
http.MethodGet + "/api/ruler/grafana/api/v1/rule/{RuleUID}/versions":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
@@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 59)
require.Len(t, paths, 60)
ac := acmock.New()
api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()}
@@ -124,3 +124,7 @@ func (f *RulerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexRuler,
}
return f.LotexRuler, nil
}
func (f *RulerApiHandler) handleRouteGetRuleVersionsByUID(ctx *contextmodel.ReqContext, ruleUID string) response.Response {
return f.GrafanaRuler.RouteGetRuleVersionsByUID(ctx, ruleUID)
}
@@ -29,6 +29,7 @@ type RulerApi interface {
RouteGetNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response
RouteGetNamespaceRulesConfig(*contextmodel.ReqContext) response.Response
RouteGetRuleByUID(*contextmodel.ReqContext) response.Response
RouteGetRuleVersionsByUID(*contextmodel.ReqContext) response.Response
RouteGetRulegGroupConfig(*contextmodel.ReqContext) response.Response
RouteGetRulesConfig(*contextmodel.ReqContext) response.Response
RouteGetRulesForExport(*contextmodel.ReqContext) response.Response
@@ -86,6 +87,11 @@ func (f *RulerApiHandler) RouteGetRuleByUID(ctx *contextmodel.ReqContext) respon
ruleUIDParam := web.Params(ctx.Req)[":RuleUID"]
return f.handleRouteGetRuleByUID(ctx, ruleUIDParam)
}
func (f *RulerApiHandler) RouteGetRuleVersionsByUID(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
ruleUIDParam := web.Params(ctx.Req)[":RuleUID"]
return f.handleRouteGetRuleVersionsByUID(ctx, ruleUIDParam)
}
func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
@@ -243,6 +249,18 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApi, m *metrics.API) {
m,
),
)
group.Get(
toMacaronPath("/api/ruler/grafana/api/v1/rule/{RuleUID}/versions"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/ruler/grafana/api/v1/rule/{RuleUID}/versions"),
metrics.Instrument(
http.MethodGet,
"/api/ruler/grafana/api/v1/rule/{RuleUID}/versions",
api.Hooks.Wrap(srv.RouteGetRuleVersionsByUID),
m,
),
)
group.Get(
toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
+1 -1
View File
@@ -28,6 +28,6 @@ type RuleStore interface {
// IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids
IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error)
GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error)
accesscontrol.RuleUIDToNamespaceStore
}
+57 -3
View File
@@ -459,6 +459,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -498,10 +501,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type",
@@ -1166,6 +1174,14 @@
"CounterResetHint": {
"$ref": "#/definitions/CounterResetHint"
},
"CustomValues": {
"description": "Holds the custom (usually upper) bounds for bucket definitions, otherwise nil.\nThis slice is interned, to be treated as immutable and copied by reference.\nThese numbers should be strictly increasing. This field is only used when the\nschema is for custom buckets, and the ZeroThreshold, ZeroCount, NegativeSpans\nand NegativeBuckets fields are not used in that case.",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
},
"PositiveBuckets": {
"description": "Observation counts in buckets. Each represents an absolute count and\nmust be zero or positive.",
"items": {
@@ -1182,7 +1198,7 @@
"type": "array"
},
"Schema": {
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8. They are all for\nbase-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets. Or\nin other words, each bucket boundary is the previous boundary times\n2^(2^-n).",
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8 for exponential buckets.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in\neach case, and then each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times\n2^(2^-n). Another valid schema number is -53 for custom buckets, defined by\nthe CustomValues field.",
"format": "int32",
"type": "integer"
},
@@ -1628,6 +1644,9 @@
"format": "date-time",
"type": "string"
},
"updated_by": {
"$ref": "#/definitions/UserInfo"
},
"version": {
"format": "int64",
"type": "integer"
@@ -1712,6 +1731,12 @@
},
"type": "object"
},
"GettableRuleVersions": {
"items": {
"$ref": "#/definitions/GettableExtendedRuleNode"
},
"type": "array"
},
"GettableStatus": {
"properties": {
"cluster": {
@@ -3572,6 +3597,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -3593,10 +3621,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type"
@@ -3636,6 +3669,9 @@
"file": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"interval": {
"format": "double",
"type": "number"
@@ -3665,6 +3701,7 @@
"required": [
"name",
"file",
"folderUid",
"rules",
"interval"
],
@@ -3767,6 +3804,10 @@
"Sample": {
"description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.",
"properties": {
"DropName": {
"description": "DropName is used to indicate whether the __name__ label should be dropped\nas part of the query evaluation.",
"type": "boolean"
},
"F": {
"format": "double",
"type": "number"
@@ -4382,7 +4423,6 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4418,7 +4458,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4447,6 +4487,18 @@
},
"type": "object"
},
"UserInfo": {
"properties": {
"name": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"title": "UserInfo represents user-related information, including a unique identifier and a name.",
"type": "object"
},
"Userinfo": {
"description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.",
"type": "object"
@@ -4648,6 +4700,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -4934,6 +4987,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence",
"type": "object"
@@ -20,6 +20,18 @@ import (
// 403: ForbiddenError
// 404: description: Not found.
// swagger:route Get /ruler/grafana/api/v1/rule/{RuleUID}/versions ruler RouteGetRuleVersionsByUID
//
// Get rule versions by UID
//
// Produces:
// - application/json
//
// Responses:
// 202: GettableRuleVersions
// 403: ForbiddenError
// 404: description: Not found.
// swagger:route Get /ruler/grafana/api/v1/rules ruler RouteGetGrafanaRulesConfig
//
// List rule groups
@@ -218,7 +230,7 @@ type PathGetRulesParams struct {
PanelID int64
}
// swagger:parameters RouteGetRuleByUID
// swagger:parameters RouteGetRuleByUID RouteGetRuleVersionsByUID
type PathGetRuleByUIDParams struct {
// in: path
RuleUID string
@@ -290,6 +302,9 @@ func (c *PostableRuleGroupConfig) validate() error {
return nil
}
// swagger:model
type GettableRuleVersions []GettableExtendedRuleNode
// swagger:model
type GettableRuleGroupConfig struct {
Name string `yaml:"name" json:"name"`
+94 -1
View File
@@ -459,6 +459,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -498,10 +501,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type",
@@ -1166,6 +1174,14 @@
"CounterResetHint": {
"$ref": "#/definitions/CounterResetHint"
},
"CustomValues": {
"description": "Holds the custom (usually upper) bounds for bucket definitions, otherwise nil.\nThis slice is interned, to be treated as immutable and copied by reference.\nThese numbers should be strictly increasing. This field is only used when the\nschema is for custom buckets, and the ZeroThreshold, ZeroCount, NegativeSpans\nand NegativeBuckets fields are not used in that case.",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
},
"PositiveBuckets": {
"description": "Observation counts in buckets. Each represents an absolute count and\nmust be zero or positive.",
"items": {
@@ -1182,7 +1198,7 @@
"type": "array"
},
"Schema": {
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8. They are all for\nbase-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets. Or\nin other words, each bucket boundary is the previous boundary times\n2^(2^-n).",
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8 for exponential buckets.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in\neach case, and then each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times\n2^(2^-n). Another valid schema number is -53 for custom buckets, defined by\nthe CustomValues field.",
"format": "int32",
"type": "integer"
},
@@ -1628,6 +1644,9 @@
"format": "date-time",
"type": "string"
},
"updated_by": {
"$ref": "#/definitions/UserInfo"
},
"version": {
"format": "int64",
"type": "integer"
@@ -1712,6 +1731,12 @@
},
"type": "object"
},
"GettableRuleVersions": {
"items": {
"$ref": "#/definitions/GettableExtendedRuleNode"
},
"type": "array"
},
"GettableStatus": {
"properties": {
"cluster": {
@@ -3572,6 +3597,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -3593,10 +3621,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type"
@@ -3636,6 +3669,9 @@
"file": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"interval": {
"format": "double",
"type": "number"
@@ -3665,6 +3701,7 @@
"required": [
"name",
"file",
"folderUid",
"rules",
"interval"
],
@@ -3767,6 +3804,10 @@
"Sample": {
"description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.",
"properties": {
"DropName": {
"description": "DropName is used to indicate whether the __name__ label should be dropped\nas part of the query evaluation.",
"type": "boolean"
},
"F": {
"format": "double",
"type": "number"
@@ -4446,6 +4487,18 @@
},
"type": "object"
},
"UserInfo": {
"properties": {
"name": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"title": "UserInfo represents user-related information, including a unique identifier and a name.",
"type": "object"
},
"Userinfo": {
"description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.",
"type": "object"
@@ -4647,6 +4700,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -4808,6 +4862,7 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert",
"type": "object"
@@ -4932,6 +4987,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence",
"type": "object"
@@ -6515,6 +6571,43 @@
]
}
},
"/ruler/grafana/api/v1/rule/{RuleUID}/versions": {
"get": {
"description": "Get rule versions by UID",
"operationId": "RouteGetRuleVersionsByUID",
"parameters": [
{
"in": "path",
"name": "RuleUID",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "GettableRuleVersions",
"schema": {
"$ref": "#/definitions/GettableRuleVersions"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
},
"tags": [
"ruler"
]
}
},
"/ruler/grafana/api/v1/rules": {
"get": {
"description": "List rule groups",
+94 -1
View File
@@ -1343,6 +1343,43 @@
}
}
},
"/ruler/grafana/api/v1/rule/{RuleUID}/versions": {
"get": {
"description": "Get rule versions by UID",
"produces": [
"application/json"
],
"tags": [
"ruler"
],
"operationId": "RouteGetRuleVersionsByUID",
"parameters": [
{
"type": "string",
"name": "RuleUID",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "GettableRuleVersions",
"schema": {
"$ref": "#/definitions/GettableRuleVersions"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/ruler/grafana/api/v1/rules": {
"get": {
"description": "List rule groups",
@@ -4088,7 +4125,9 @@
"description": "adapted from cortex",
"type": "object",
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type",
@@ -4118,6 +4157,9 @@
"type": "number",
"format": "double"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -4157,6 +4199,9 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
@@ -4818,6 +4863,14 @@
"CounterResetHint": {
"$ref": "#/definitions/CounterResetHint"
},
"CustomValues": {
"description": "Holds the custom (usually upper) bounds for bucket definitions, otherwise nil.\nThis slice is interned, to be treated as immutable and copied by reference.\nThese numbers should be strictly increasing. This field is only used when the\nschema is for custom buckets, and the ZeroThreshold, ZeroCount, NegativeSpans\nand NegativeBuckets fields are not used in that case.",
"type": "array",
"items": {
"type": "number",
"format": "double"
}
},
"PositiveBuckets": {
"description": "Observation counts in buckets. Each represents an absolute count and\nmust be zero or positive.",
"type": "array",
@@ -4834,7 +4887,7 @@
}
},
"Schema": {
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8. They are all for\nbase-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets. Or\nin other words, each bucket boundary is the previous boundary times\n2^(2^-n).",
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8 for exponential buckets.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in\neach case, and then each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times\n2^(2^-n). Another valid schema number is -53 for custom buckets, defined by\nthe CustomValues field.",
"type": "integer",
"format": "int32"
},
@@ -5279,6 +5332,9 @@
"type": "string",
"format": "date-time"
},
"updated_by": {
"$ref": "#/definitions/UserInfo"
},
"version": {
"type": "integer",
"format": "int64"
@@ -5362,6 +5418,12 @@
}
}
},
"GettableRuleVersions": {
"type": "array",
"items": {
"$ref": "#/definitions/GettableExtendedRuleNode"
}
},
"GettableStatus": {
"type": "object",
"required": [
@@ -7220,7 +7282,9 @@
"description": "adapted from cortex",
"type": "object",
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type"
@@ -7230,6 +7294,9 @@
"type": "number",
"format": "double"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -7251,6 +7318,9 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
@@ -7283,6 +7353,7 @@
"required": [
"name",
"file",
"folderUid",
"rules",
"interval"
],
@@ -7294,6 +7365,9 @@
"file": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"interval": {
"type": "number",
"format": "double"
@@ -7419,6 +7493,10 @@
"description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.",
"type": "object",
"properties": {
"DropName": {
"description": "DropName is used to indicate whether the __name__ label should be dropped\nas part of the query evaluation.",
"type": "boolean"
},
"F": {
"type": "number",
"format": "double"
@@ -8097,6 +8175,18 @@
}
}
},
"UserInfo": {
"type": "object",
"title": "UserInfo represents user-related information, including a unique identifier and a name.",
"properties": {
"name": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
"Userinfo": {
"description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a [URL]. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.",
"type": "object"
@@ -8298,6 +8388,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",
@@ -8459,6 +8550,7 @@
}
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array",
"items": {
"type": "object",
@@ -8583,6 +8675,7 @@
}
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"type": "object",
+13
View File
@@ -550,6 +550,19 @@ func (a *AlertRuleMutators) WithUpdatedBy(uid *UserUID) AlertRuleMutator {
}
}
func (a *AlertRuleMutators) WithUID(uid string) AlertRuleMutator {
return func(r *AlertRule) {
r.UID = uid
}
}
func (a *AlertRuleMutators) WithKey(key AlertRuleKey) AlertRuleMutator {
return func(r *AlertRule) {
r.UID = key.UID
r.OrgID = key.OrgID
}
}
func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels {
count := max
if min > max {
+30
View File
@@ -118,6 +118,36 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler
return result, err
}
func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error) {
alertRules := make([]*ngmodels.AlertRule, 0)
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Desc("id").Rows(new(alertRuleVersion))
if err != nil {
return err
}
// Deserialize each rule separately in case any of them contain invalid JSON.
for rows.Next() {
rule := new(alertRuleVersion)
err = rows.Scan(rule)
if err != nil {
st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err)
continue
}
converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID)
continue
}
alertRules = append(alertRules, &converted)
}
return nil
})
if err != nil {
return nil, err
}
return alertRules, nil
}
// GetRuleByID retrieves models.AlertRule by ID.
// It returns models.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
func (st DBstore) GetRuleByID(ctx context.Context, query ngmodels.GetAlertRuleByIDQuery) (result *ngmodels.AlertRule, err error) {
+31
View File
@@ -204,3 +204,34 @@ func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion {
Metadata: rule.Metadata,
}
}
func alertRuleVersionToAlertRule(version alertRuleVersion) alertRule {
return alertRule{
ID: version.ID,
OrgID: version.RuleOrgID,
Title: version.Title,
Condition: version.Condition,
Data: version.Data,
Updated: version.Created,
UpdatedBy: version.CreatedBy,
IntervalSeconds: version.IntervalSeconds,
Version: version.Version,
UID: version.RuleUID,
NamespaceUID: version.RuleNamespaceUID,
// Versions do not store Dashboard\Panel as separate column.
// However, these fields are part of annotations and information in these fields is redundant
DashboardUID: nil,
PanelID: nil,
RuleGroup: version.RuleGroup,
RuleGroupIndex: version.RuleGroupIndex,
Record: version.Record,
NoDataState: version.NoDataState,
ExecErrState: version.ExecErrState,
For: version.For,
Annotations: version.Annotations,
Labels: version.Labels,
IsPaused: version.IsPaused,
NotificationSettings: version.NotificationSettings,
Metadata: version.Metadata,
}
}
+17
View File
@@ -43,3 +43,20 @@ func TestAlertRuleToModelsAlertRule(t *testing.T) {
require.Equal(t, ngmodels.ErrorErrState, converted.ExecErrState)
})
}
func TestAlertRuleVersionToAlertRule(t *testing.T) {
g := ngmodels.RuleGen
t.Run("make sure no data is lost between conversions", func(t *testing.T) {
for _, rule := range g.GenerateMany(100) {
// ignore fields
rule.DashboardUID = nil
rule.PanelID = nil
r, err := alertRuleFromModelsAlertRule(rule)
require.NoError(t, err)
r2 := alertRuleVersionToAlertRule(alertRuleToAlertRuleVersion(r))
require.Equal(t, r, r2)
}
})
}
+24 -5
View File
@@ -22,6 +22,7 @@ type RuleStore struct {
mtx sync.Mutex
// OrgID -> RuleGroup -> Namespace -> Rules
Rules map[int64][]*models.AlertRule
History map[models.AlertRuleKey][]*models.AlertRule
Hook func(cmd any) error // use Hook if you need to intercept some query and return an error
RecordedOps []any
Folders map[int64][]*folder.Folder
@@ -40,6 +41,7 @@ func NewRuleStore(t *testing.T) *RuleStore {
return nil
},
Folders: map[int64][]*folder.Folder{},
History: map[models.AlertRuleKey][]*models.AlertRule{},
}
}
@@ -50,6 +52,8 @@ func (f *RuleStore) PutRule(_ context.Context, rules ...*models.AlertRule) {
mainloop:
for _, r := range rules {
rgs := f.Rules[r.OrgID]
cp := models.CopyRule(r)
f.History[r.GetKey()] = append(f.History[r.GetKey()], cp)
for idx, rulePtr := range rgs {
if rulePtr.UID == r.UID {
rgs[idx] = r
@@ -131,11 +135,7 @@ func (f *RuleStore) GetAlertRuleByUID(_ context.Context, q *models.GetAlertRuleB
if err := f.Hook(*q); err != nil {
return nil, err
}
rules, ok := f.Rules[q.OrgID]
if !ok {
return nil, nil
}
rules := f.Rules[q.OrgID]
for _, rule := range rules {
if rule.UID == q.UID {
return rule, nil
@@ -372,3 +372,22 @@ func (f *RuleStore) GetNamespacesByRuleUID(ctx context.Context, orgID int64, uid
return namespacesMap, nil
}
func (f *RuleStore) GetAlertRuleVersions(_ context.Context, key models.AlertRuleKey) ([]*models.AlertRule, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
q := GenericRecordedQuery{
Name: "GetAlertRuleVersions",
Params: []any{key},
}
defer func() {
f.RecordedOps = append(f.RecordedOps, q)
}()
if err := f.Hook(key); err != nil {
return nil, err
}
return f.History[key], nil
}
+107
View File
@@ -241,6 +241,18 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Get versions of any rule", func(t *testing.T) {
for _, groups := range allRules { // random rule from each folder
group := groups[rand.Intn(len(groups))]
rule := group.Rules[rand.Intn(len(group.Rules))]
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, rule.GrafanaManagedAlert.UID)
if assert.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw) {
assert.NotEmpty(t, versions)
assert.Equal(t, rule, versions[0]) // the first version in the collection should always be the current
}
}
})
})
t.Run("when permissions for folder2 removed", func(t *testing.T) {
@@ -310,6 +322,12 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
require.Equal(t, http.StatusForbidden, status)
})
t.Run("Versions of rule", func(t *testing.T) {
uid := allRules["folder2"][0].Rules[0].GrafanaManagedAlert.UID
_, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusForbidden, status, "Expected status 403, got %d: %s", status, raw)
})
t.Run("when all permissions are revoked", func(t *testing.T) {
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
@@ -4315,6 +4333,95 @@ func TestIntegrationRuleUpdateAllDatabases(t *testing.T) {
})
}
func TestIntegrationRuleVersions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
EnableQuota: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-post.json"))
require.NoError(t, err)
var group1 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group1))
// Create rule under folder1
response := apiClient.PostRulesGroup(t, "folder1", &group1)
require.NotEmptyf(t, response.Created, "Expected created to be set")
uid := response.Created[0]
ruleV1 := apiClient.GetRuleByUID(t, uid)
t.Run("should return 1 version right after creation", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 1, "Expected 1 version, got %d", len(versions))
assert.Equal(t, ruleV1, versions[0])
})
group1Gettable := apiClient.GetRulesGroup(t, "folder1", group1.Name)
group1 = convertGettableRuleGroupToPostable(group1Gettable.GettableRuleGroupConfig)
group1.Rules[0].Annotations[util.GenerateShortUID()] = util.GenerateShortUID()
_ = apiClient.PostRulesGroup(t, "folder1", &group1)
ruleV2 := apiClient.GetRuleByUID(t, uid)
t.Run("should return previous versions after update", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 2, "Expected 2 versions, got %d", len(versions))
pathsToIgnore := []string{
"GrafanaManagedAlert.ID", // In versions ID has different value
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(apimodels.GettableRuleVersions{ruleV2, ruleV1}, versions, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
assert.Empty(t, diff)
})
_ = apiClient.PostRulesGroup(t, "folder1", &group1) // Noop update
t.Run("should not add new version if rule was not changed", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 2, "Expected 2 versions, got %d", len(versions))
})
apiClient.DeleteRulesGroup(t, "folder1", group1.Name)
t.Run("should NotFound after rule was deleted", func(t *testing.T) {
_, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusNotFound, status, "Expected status 404, got %d: %s", status, raw)
})
}
func newTestingRuleConfig(t *testing.T) apimodels.PostableRuleGroupConfig {
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
+23
View File
@@ -475,6 +475,13 @@ func (a apiClient) PostRulesGroupWithStatus(t *testing.T, folder string, group *
return m, resp.StatusCode, string(b)
}
func (a apiClient) PostRulesGroup(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig) apimodels.UpdateRuleGroupResponse {
t.Helper()
m, status, raw := a.PostRulesGroupWithStatus(t, folder, group)
requireStatusCode(t, http.StatusAccepted, status, raw)
return m
}
func (a apiClient) PostRulesExportWithStatus(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig, params *apimodels.ExportQueryParams) (int, string) {
t.Helper()
buf := bytes.Buffer{}
@@ -1048,6 +1055,22 @@ func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroup
return sendRequest[apimodels.AlertGroups](t, req, http.StatusOK)
}
func (a apiClient) GetRuleVersionsWithStatus(t *testing.T, ruleUID string) (apimodels.GettableRuleVersions, int, string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s/versions", a.url, ruleUID), nil)
require.NoError(t, err)
return sendRequest[apimodels.GettableRuleVersions](t, req, http.StatusOK)
}
func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.GettableExtendedRuleNode {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s", a.url, ruleUID), nil)
require.NoError(t, err)
rule, status, raw := sendRequest[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
return rule
}
func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
t.Helper()
client := &http.Client{}
+56 -1
View File
@@ -12861,7 +12861,9 @@
"description": "adapted from cortex",
"type": "object",
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type",
@@ -12891,6 +12893,9 @@
"type": "number",
"format": "double"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -12930,6 +12935,9 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
@@ -15392,6 +15400,14 @@
"CounterResetHint": {
"$ref": "#/definitions/CounterResetHint"
},
"CustomValues": {
"description": "Holds the custom (usually upper) bounds for bucket definitions, otherwise nil.\nThis slice is interned, to be treated as immutable and copied by reference.\nThese numbers should be strictly increasing. This field is only used when the\nschema is for custom buckets, and the ZeroThreshold, ZeroCount, NegativeSpans\nand NegativeBuckets fields are not used in that case.",
"type": "array",
"items": {
"type": "number",
"format": "double"
}
},
"PositiveBuckets": {
"description": "Observation counts in buckets. Each represents an absolute count and\nmust be zero or positive.",
"type": "array",
@@ -15408,7 +15424,7 @@
}
},
"Schema": {
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8. They are all for\nbase-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets. Or\nin other words, each bucket boundary is the previous boundary times\n2^(2^-n).",
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8 for exponential buckets.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in\neach case, and then each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times\n2^(2^-n). Another valid schema number is -53 for custom buckets, defined by\nthe CustomValues field.",
"type": "integer",
"format": "int32"
},
@@ -16048,6 +16064,9 @@
"type": "string",
"format": "date-time"
},
"updated_by": {
"$ref": "#/definitions/UserInfo"
},
"version": {
"type": "integer",
"format": "int64"
@@ -16131,6 +16150,12 @@
}
}
},
"GettableRuleVersions": {
"type": "array",
"items": {
"$ref": "#/definitions/GettableExtendedRuleNode"
}
},
"GettableStatus": {
"type": "object",
"required": [
@@ -19810,7 +19835,9 @@
"description": "adapted from cortex",
"type": "object",
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type"
@@ -19820,6 +19847,9 @@
"type": "number",
"format": "double"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -19841,6 +19871,9 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
@@ -19873,6 +19906,7 @@
"required": [
"name",
"file",
"folderUid",
"rules",
"interval"
],
@@ -19884,6 +19918,9 @@
"file": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"interval": {
"type": "number",
"format": "double"
@@ -20009,6 +20046,10 @@
"description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.",
"type": "object",
"properties": {
"DropName": {
"description": "DropName is used to indicate whether the __name__ label should be dropped\nas part of the query evaluation.",
"type": "boolean"
},
"F": {
"type": "number",
"format": "double"
@@ -22032,6 +22073,18 @@
}
}
},
"UserInfo": {
"type": "object",
"title": "UserInfo represents user-related information, including a unique identifier and a name.",
"properties": {
"name": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
"UserLookupDTO": {
"type": "object",
"properties": {
@@ -22437,6 +22490,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",
@@ -22766,6 +22820,7 @@
}
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"type": "object",
+34 -15
View File
@@ -4,26 +4,28 @@ import { getRootSectionForNode } from 'app/core/selectors/navModel';
import AppRootPage from 'app/features/plugins/components/AppRootPage';
import { getState } from 'app/store/store';
const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem => 'pluginId' in model && 'id' in model;
const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/');
function getPathForNavItem(navItem: PluginNavModelItem) {
const pluginNavSection = getRootSectionForNode(navItem);
const appPluginUrl = `/a/${navItem.pluginId}`;
const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId"
const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs
return {
path: `${path}/*`,
sensitive: isSensitive,
component: () => <AppRootPage pluginId={navItem.pluginId} pluginNavSection={pluginNavSection} />,
};
}
export function getAppPluginRoutes(): RouteDescriptor[] {
const state = getState();
const { navIndex } = state;
const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/');
const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem =>
'pluginId' in model && 'id' in model;
const explicitAppPluginRoutes = Object.values(navIndex)
.filter<PluginNavModelItem>(isPluginNavModelItem)
.map((navItem) => {
const pluginNavSection = getRootSectionForNode(navItem);
const appPluginUrl = `/a/${navItem.pluginId}`;
const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId"
const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs
return {
path: `${path}/*`,
sensitive: isSensitive,
component: () => <AppRootPage pluginId={navItem.pluginId} pluginNavSection={pluginNavSection} />,
};
});
.map(getPathForNavItem);
return [
...explicitAppPluginRoutes,
@@ -36,6 +38,23 @@ export function getAppPluginRoutes(): RouteDescriptor[] {
];
}
export function getRouteForAppPlugin(pluginId: string): RouteDescriptor {
const state = getState();
const { navIndex } = state;
const navItem = Object.values(navIndex)
.filter<PluginNavModelItem>(isPluginNavModelItem)
.find((navItem) => navItem.pluginId === pluginId);
if (!navItem) {
return {
path: '/a/:pluginId/*',
component: () => <AppRootPage pluginNavSection={navIndex.home} />,
};
}
return getPathForNavItem(navItem);
}
interface PluginNavModelItem extends Omit<NavModelItem, 'pluginId' | 'id'> {
pluginId: string;
id: string;
@@ -1,6 +1,143 @@
import { toDashboardResults, SearchHit, SearchAPIResponse } from './unified';
import { BackendSrv } from '@grafana/runtime';
import { GrafanaSearcher, SearchQuery } from './types';
import { toDashboardResults, SearchHit, SearchAPIResponse, UnifiedSearcher } from './unified';
beforeEach(() => {
jest.clearAllMocks();
});
const mockResults: SearchAPIResponse = {
hits: [],
totalHits: 0,
};
const mockFolders: SearchAPIResponse = {
hits: [],
totalHits: 0,
};
const mockFallbackSearcher = {
search: jest.fn(),
} as unknown as GrafanaSearcher;
const getResponse = (uri: string) => {
if (uri.endsWith('?type=folders')) {
return Promise.resolve(mockFolders);
}
return Promise.resolve(mockResults);
};
const mockSearcher = {
search: (uri: string) => getResponse(uri),
};
jest.mock('@grafana/runtime', () => {
const originalRuntime = jest.requireActual('@grafana/runtime');
return {
...originalRuntime,
getBackendSrv: () =>
({
get: (uri: string) => mockSearcher.search(uri),
}) as unknown as BackendSrv,
};
});
describe('Unified Storage Searcher', () => {
it('should perform search with basic query', async () => {
mockFolders.hits = [
{
name: 'folder1',
title: 'Folder 1',
resource: 'folders',
} as SearchHit,
];
mockResults.hits = [
{
name: 'dashboard1',
title: 'Dashboard 1',
resource: 'dashboards',
folder: 'folder1',
} as SearchHit,
];
const query: SearchQuery = {
query: 'test',
limit: 50,
};
const searcher = new UnifiedSearcher(mockFallbackSearcher);
const response = await searcher.search(query);
expect(response.view.length).toBe(1);
expect(response.view.get(0).title).toBe('Dashboard 1');
const df = response.view.dataFrame;
const locationInfo = df.meta?.custom?.locationInfo;
expect(locationInfo).toBeDefined();
expect(locationInfo?.folder1.name).toBe('Folder 1');
});
it('should perform search and sync folders with missing folder', async () => {
const mockFolders = {
hits: [
{
name: 'folder2',
title: 'Folder 2',
resource: 'folders',
} as SearchHit,
],
totalHits: 1,
};
const mockResults = {
hits: [
{
name: 'db1',
title: 'DB 1',
resource: 'dashboards',
folder: 'folder1',
} as SearchHit,
{
name: 'db2',
title: 'DB 2',
resource: 'dashboards',
folder: 'folder2',
} as SearchHit,
],
totalHits: 2,
};
jest
.spyOn(mockSearcher, 'search')
.mockResolvedValueOnce(mockFolders)
.mockResolvedValueOnce(mockResults)
.mockResolvedValueOnce(mockFolders);
const consoleWarn = jest.fn();
jest.spyOn(console, 'warn').mockImplementationOnce(consoleWarn);
const query: SearchQuery = {
query: 'test',
limit: 50,
};
const searcher = new UnifiedSearcher(mockFallbackSearcher);
const response = await searcher.search(query);
expect(response.view.length).toBe(1);
expect(response.view.get(0).title).toBe('DB 2');
const df = response.view.dataFrame;
const locationInfo = df.meta?.custom?.locationInfo;
expect(locationInfo).toBeDefined();
expect(locationInfo?.folder2.name).toBe('Folder 2');
expect(consoleWarn).toHaveBeenCalled();
expect(mockSearcher.search).toHaveBeenCalledTimes(3);
});
it('can create dashboard search results and set meta sortBy so column is added for sprinkles sort field', () => {
const mockHits: SearchHit[] = [
{
+36 -9
View File
@@ -112,7 +112,7 @@ export class UnifiedSearcher implements GrafanaSearcher {
async doSearchQuery(query: SearchQuery): Promise<QueryResponse> {
const uri = await this.newRequest(query);
const rsp = await getBackendSrv().get<SearchAPIResponse>(uri);
const rsp = await this.fetchResponse(uri);
const first = toDashboardResults(rsp, query.sort ?? '');
if (first.name === loadingFrameName) {
@@ -120,12 +120,6 @@ export class UnifiedSearcher implements GrafanaSearcher {
}
const meta = first.meta?.custom || ({} as SearchResultMeta);
const locationInfo = await this.locationInfo;
const hasMissing = rsp.hits.some((hit) => !locationInfo[hit.folder]);
if (hasMissing) {
// sync the location info ( folders )
this.locationInfo = loadLocationInfo();
}
meta.locationInfo = await this.locationInfo;
// Set the field name to a better display name
@@ -141,14 +135,13 @@ export class UnifiedSearcher implements GrafanaSearcher {
let loadMax = 0;
let pending: Promise<void> | undefined = undefined;
const getNextPage = async () => {
// TODO: implement this correctly
while (loadMax > view.dataFrame.length) {
const offset = view.dataFrame.length;
if (offset >= meta.count) {
return;
}
const nextPageUrl = `${uri}&offset=${offset}`;
const resp = await getBackendSrv().get<SearchAPIResponse>(nextPageUrl);
const resp = await this.fetchResponse(nextPageUrl);
const frame = toDashboardResults(resp, query.sort ?? '');
if (!frame) {
console.log('no results', frame);
@@ -198,6 +191,40 @@ export class UnifiedSearcher implements GrafanaSearcher {
};
}
async fetchResponse(uri: string) {
const rsp = await getBackendSrv().get<SearchAPIResponse>(uri);
const isFolderCacheStale = await this.isFolderCacheStale(rsp.hits);
if (!isFolderCacheStale) {
return rsp;
}
// sync the location info ( folders )
this.locationInfo = loadLocationInfo();
// recheck for missing folders
const hasMissing = await this.isFolderCacheStale(rsp.hits);
if (!hasMissing) {
return rsp;
}
// we still have results here with folders we can't find
// filter the results since we probably don't have access to that folder
const locationInfo = await this.locationInfo;
const hits = rsp.hits.filter((hit) => {
if (hit.folder === undefined || locationInfo[hit.folder] !== undefined) {
return true;
}
console.warn('Dropping search hit with missing folder', hit);
return false;
});
const totalHits = rsp.totalHits - (rsp.hits.length - hits.length);
return { ...rsp, hits, totalHits };
}
async isFolderCacheStale(hits: SearchHit[]): Promise<boolean> {
const locationInfo = await this.locationInfo;
return hits.some((hit) => {
return hit.folder !== undefined && locationInfo[hit.folder] === undefined;
});
}
private async newRequest(query: SearchQuery): Promise<string> {
query = await replaceCurrentFolderQuery(query);
@@ -2,14 +2,14 @@
"name": "@grafana-plugins/grafana-azure-monitor-datasource",
"description": "Grafana data source for Azure Monitor",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"@kusto/monaco-kusto": "^10.0.0",
"fast-deep-equal": "^3.1.3",
"i18next": "^24.0.0",
@@ -25,8 +25,8 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
@@ -2,15 +2,15 @@
"name": "@grafana-plugins/stackdriver",
"description": "Grafana data source for Google Cloud Monitoring",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/google-sdk": "0.1.2",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"debounce-promise": "3.1.2",
"fast-deep-equal": "^3.1.3",
"i18next": "^24.0.0",
@@ -26,8 +26,8 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
@@ -2,22 +2,22 @@
"name": "@grafana-plugins/grafana-postgresql-datasource",
"description": "PostgreSQL data source plugin",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/sql": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/sql": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"lodash": "4.17.21",
"react": "18.3.1",
"rxjs": "7.8.1",
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@@ -2,13 +2,13 @@
"name": "@grafana-plugins/grafana-pyroscope-datasource",
"description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"fast-deep-equal": "^3.1.3",
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
@@ -20,7 +20,7 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
@@ -2,13 +2,13 @@
"name": "@grafana-plugins/grafana-testdata-datasource",
"description": "Generates test data in different forms",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"d3-random": "^3.0.1",
"lodash": "4.17.21",
"micro-memoize": "^4.1.2",
@@ -21,8 +21,8 @@
"uuid": "11.0.5"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@@ -55,6 +55,10 @@ describe('graphiteDatasource', () => {
describe('convertResponseToDataFrames', () => {
it('should transform regular result', () => {
const refIDMap = {
refIDA: 'A',
refIDB: 'B',
};
const result = ctx.ds.convertResponseToDataFrames(
createFetchResponse({
meta: {
@@ -65,7 +69,7 @@ describe('graphiteDatasource', () => {
},
series: [
{
target: 'seriesA',
target: 'seriesA refIDA',
datapoints: [
[100, 200],
[101, 201],
@@ -85,7 +89,7 @@ describe('graphiteDatasource', () => {
],
},
{
target: 'seriesB',
target: 'seriesB refIDB',
meta: [
{
'aggnum-norm': 1,
@@ -105,7 +109,8 @@ describe('graphiteDatasource', () => {
],
},
],
})
}),
refIDMap
);
expect(result.data.length).toBe(2);
@@ -113,24 +118,28 @@ describe('graphiteDatasource', () => {
expect(getFrameDisplayName(result.data[1])).toBe('seriesB');
expect(result.data[0].length).toBe(2);
expect(result.data[0].meta.notices.length).toBe(1);
expect(result.data[0].refId).toBe('A');
expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');
expect(result.data[1].meta.notices).toBeUndefined();
expect(result.data[1].refId).toBe('B');
});
});
describe('When querying graphite with one target using query editor target spec', () => {
const query = {
panelId: 3,
dashboardId: 5,
range: { from: dateTime('2022-04-01T00:00:00'), to: dateTime('2022-07-01T00:00:00') },
targets: [{ target: 'prod1.count' }, { target: 'prod2.count' }],
maxDataPoints: 500,
};
let response: unknown;
let requestOptions: BackendSrvRequest;
beforeEach(() => {
const query = {
panelId: 3,
dashboardId: 5,
range: { from: dateTime('2022-04-01T00:00:00'), to: dateTime('2022-07-01T00:00:00') },
targets: [
{ target: 'prod1.count', refId: 'A' },
{ target: 'prod2.count', refId: 'B' },
],
maxDataPoints: 500,
};
fetchMock.mockImplementation((options) => {
requestOptions = options;
return of(
@@ -164,8 +173,8 @@ describe('graphiteDatasource', () => {
it('should query correctly', () => {
const params = requestOptions.data.split('&');
expect(params).toContain('target=prod1.count');
expect(params).toContain('target=prod2.count');
expect(params).toContain(`target=${encodeURIComponent(`aliasSub(prod1.count, "(^.*$)", "\\1 A")`)}`);
expect(params).toContain(`target=${encodeURIComponent(`aliasSub(prod2.count, "(^.*$)", "\\1 B")`)}`);
expect(params).toContain('from=1648789200');
expect(params).toContain('until=1656655200');
});
@@ -209,6 +209,17 @@ export class GraphiteDatasource
return merge(...streams);
}
// Use this object to map the original refID of the query to our sanitised one
const refIds: { [key: string]: string } = {};
for (const target of options.targets) {
// Sanitise the refID otherwise the Graphite query will fail
const formattedRefId = target.refId.replaceAll(' ', '_');
refIds[formattedRefId] = target.refId;
// Use aliasSub to include the refID in the response series name. This allows us to set the refID on the frame.
const updatedTarget = `aliasSub(${target.target}, "(^.*$)", "\\1 ${formattedRefId}")`;
target.target = updatedTarget;
}
// handle the queries here
const graphOptions = {
from: this.translateTime(options.range.from, false, options.timezone),
@@ -243,7 +254,7 @@ export class GraphiteDatasource
httpOptions.requestId = this.name + '.panelId.' + options.panelId;
}
return this.doGraphiteRequest(httpOptions).pipe(map(this.convertResponseToDataFrames));
return this.doGraphiteRequest(httpOptions).pipe(map((result) => this.convertResponseToDataFrames(result, refIds)));
}
addTracingHeaders(
@@ -267,7 +278,7 @@ export class GraphiteDatasource
}
}
convertResponseToDataFrames = (result: FetchResponse): DataQueryResponse => {
convertResponseToDataFrames = (result: FetchResponse, refIdMap: { [key: string]: string }): DataQueryResponse => {
const data: DataFrame[] = [];
if (!result || !result.data) {
return { data };
@@ -282,6 +293,9 @@ export class GraphiteDatasource
for (let i = 0; i < series.length; i++) {
const s = series[i];
// Retrieve the original refID of the query
const [target, refId] = s.target.split(' ');
s.target = target;
// Disables Grafana own series naming
s.title = s.target;
@@ -291,6 +305,8 @@ export class GraphiteDatasource
}
const frame = toDataFrame(s);
// Set the refID value on the frame
frame.refId = refIdMap[refId];
// Metrictank metadata
if (s.meta) {
@@ -2,7 +2,7 @@
"name": "@grafana-plugins/jaeger",
"description": "Jaeger plugin for Grafana",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "workspace:*",
@@ -2,22 +2,22 @@
"name": "@grafana-plugins/mssql",
"description": "MSSQL data source plugin",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/sql": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/sql": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"lodash": "4.17.21",
"react": "18.3.1",
"rxjs": "7.8.1",
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@@ -2,22 +2,22 @@
"name": "@grafana-plugins/mysql",
"description": "MySQL data source plugin",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "11.5.0-pre",
"@grafana/sql": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/sql": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"lodash": "4.17.21",
"react": "18.3.1",
"rxjs": "7.8.1",
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/e2e-selectors": "11.6.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@@ -2,13 +2,13 @@
"name": "@grafana-plugins/parca",
"description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "11.5.0-pre",
"@grafana/runtime": "11.5.0-pre",
"@grafana/schema": "11.5.0-pre",
"@grafana/ui": "11.5.0-pre",
"@grafana/data": "11.6.0-pre",
"@grafana/runtime": "11.6.0-pre",
"@grafana/schema": "11.6.0-pre",
"@grafana/ui": "11.6.0-pre",
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"react": "18.3.1",
@@ -18,7 +18,7 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@@ -2,7 +2,7 @@
"name": "@grafana-plugins/tempo",
"description": "Grafana plugin for the Tempo data source.",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "workspace:*",
@@ -39,7 +39,7 @@
"uuid": "11.0.5"
},
"devDependencies": {
"@grafana/plugin-configs": "11.5.0-pre",
"@grafana/plugin-configs": "11.6.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
@@ -2,7 +2,7 @@
"name": "@grafana-plugins/zipkin",
"description": "Zipkin plugin for Grafana",
"private": true,
"version": "11.5.0-pre",
"version": "11.6.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "workspace:*",
+11 -5
View File
@@ -15,7 +15,7 @@ import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components/ConfigureIRM';
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
import { getAppPluginRoutes } from 'app/features/plugins/routes';
import { getAppPluginRoutes, getRouteForAppPlugin } from 'app/features/plugins/routes';
import { getProfileRoutes } from 'app/features/profile/routes';
import { AccessControlAction, DashboardRoutes } from 'app/types';
@@ -516,11 +516,17 @@ export function getAppRoutes(): RouteDescriptor[] {
},
config.featureToggles.exploreMetrics && {
path: '/explore/metrics/*',
chromeless: false,
roles: () => contextSrv.evaluatePermission([AccessControlAction.DataSourcesExplore]),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage')
),
...(config.featureToggles.exploreMetricsUseExternalAppPlugin
? {
component: getRouteForAppPlugin('grafana-metricsdrilldown-app').component,
}
: {
chromeless: false,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage')
),
}),
},
{
path: '/bookmarks',
+56 -1
View File
@@ -2956,6 +2956,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -2995,10 +2998,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type",
@@ -5465,6 +5473,14 @@
"CounterResetHint": {
"$ref": "#/components/schemas/CounterResetHint"
},
"CustomValues": {
"description": "Holds the custom (usually upper) bounds for bucket definitions, otherwise nil.\nThis slice is interned, to be treated as immutable and copied by reference.\nThese numbers should be strictly increasing. This field is only used when the\nschema is for custom buckets, and the ZeroThreshold, ZeroCount, NegativeSpans\nand NegativeBuckets fields are not used in that case.",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
},
"PositiveBuckets": {
"description": "Observation counts in buckets. Each represents an absolute count and\nmust be zero or positive.",
"items": {
@@ -5481,7 +5497,7 @@
"type": "array"
},
"Schema": {
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8. They are all for\nbase-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets. Or\nin other words, each bucket boundary is the previous boundary times\n2^(2^-n).",
"description": "Currently valid schema numbers are -4 \u003c= n \u003c= 8 for exponential buckets.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in\neach case, and then each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times\n2^(2^-n). Another valid schema number is -53 for custom buckets, defined by\nthe CustomValues field.",
"format": "int32",
"type": "integer"
},
@@ -6122,6 +6138,9 @@
"format": "date-time",
"type": "string"
},
"updated_by": {
"$ref": "#/components/schemas/UserInfo"
},
"version": {
"format": "int64",
"type": "integer"
@@ -6206,6 +6225,12 @@
},
"type": "object"
},
"GettableRuleVersions": {
"items": {
"$ref": "#/components/schemas/GettableExtendedRuleNode"
},
"type": "array"
},
"GettableStatus": {
"properties": {
"cluster": {
@@ -9888,6 +9913,9 @@
"format": "double",
"type": "number"
},
"folderUid": {
"type": "string"
},
"health": {
"type": "string"
},
@@ -9909,10 +9937,15 @@
},
"type": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"required": [
"uid",
"name",
"folderUid",
"query",
"health",
"type"
@@ -9952,6 +9985,9 @@
"file": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"interval": {
"format": "double",
"type": "number"
@@ -9981,6 +10017,7 @@
"required": [
"name",
"file",
"folderUid",
"rules",
"interval"
],
@@ -10083,6 +10120,10 @@
"Sample": {
"description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.",
"properties": {
"DropName": {
"description": "DropName is used to indicate whether the __name__ label should be dropped\nas part of the query evaluation.",
"type": "boolean"
},
"F": {
"format": "double",
"type": "number"
@@ -12106,6 +12147,18 @@
},
"type": "object"
},
"UserInfo": {
"properties": {
"name": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"title": "UserInfo represents user-related information, including a unique identifier and a name.",
"type": "object"
},
"UserLookupDTO": {
"properties": {
"avatarUrl": {
@@ -12511,6 +12564,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/components/schemas/alertGroup"
},
@@ -12838,6 +12892,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/components/schemas/gettableSilence"
},
@@ -12,6 +12,7 @@ load(
"enterprise_setup_step",
"identify_runner_step",
"lint_frontend_step",
"verify_api_clients_step",
"verify_i18n_step",
"yarn_install_step",
)
@@ -35,6 +36,7 @@ def lint_frontend_pipeline(trigger, ver_mode):
init_steps = []
lint_step = lint_frontend_step()
i18n_step = verify_i18n_step()
api_clients_step = verify_api_clients_step()
volumes = []
@@ -54,6 +56,7 @@ def lint_frontend_pipeline(trigger, ver_mode):
test_steps = [
lint_step,
i18n_step,
api_clients_step,
]
return pipeline(
+22
View File
@@ -676,6 +676,28 @@ def verify_i18n_step():
],
}
def verify_api_clients_step():
uncommited_error_message = "\nAPI client generation has not been committed. Please run 'yarn generate-apis', commit the changes and push again."
return {
"name": "verify-api-clients",
"image": images["node_deb"],
"depends_on": [
"yarn-install",
],
"commands": [
"yarn generate-apis",
# Verify that client generation has been run and committed
'''
file_diff=$(git diff ':!conf')
if [ -n "$file_diff" ]; then
echo $file_diff
echo "{}"
exit 1
fi
'''.format(uncommited_error_message),
],
}
def test_a11y_frontend_step(ver_mode, port = 3001):
"""Runs automated accessiblity tests against the frontend.
+1 -1
View File
@@ -16,7 +16,7 @@ images = {
"node_deb": "node:{}-bookworm".format(nodejs_version[:2]),
"cloudsdk": "google/cloud-sdk:431.0.0",
"publish": "grafana/grafana-ci-deploy:1.3.3",
"alpine": "alpine:3.20.3",
"alpine": "alpine:3.20.5",
"ubuntu": "ubuntu:22.04",
"curl": "byrnedo/alpine-curl:0.1.8",
"plugins_slack": "plugins/slack",
+44 -27
View File
@@ -129,33 +129,50 @@ function simplifySchemaName(schemaName: string) {
}
}
const sourceDir = path.resolve(__dirname, '../pkg/tests/apis/openapi_snapshots');
const outputDir = path.resolve(__dirname, '../data/openapi');
// Create the output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const files = fs.readdirSync(sourceDir).filter((file: string) => file.endsWith('.json'));
for (const file of files) {
const inputPath = path.join(sourceDir, file);
const outputPath = path.join(outputDir, file);
console.log(`Processing file "${file}"...`);
const fileContent = fs.readFileSync(inputPath, 'utf-8');
let inputSpec;
try {
inputSpec = JSON.parse(fileContent);
} catch (err) {
console.error(`Invalid JSON file "${file}". Skipping this file.`);
continue;
/**
* Process all files in a source directory and write results to output directory
*/
function processDirectory(sourceDir: string, outputDir: string) {
// Skip if source directory doesn't exist
if (!fs.existsSync(sourceDir)) {
return;
}
const outputSpec = processOpenAPISpec(inputSpec);
fs.writeFileSync(outputPath, JSON.stringify(outputSpec, null, 2), 'utf-8');
console.log(`Processing completed for file "${file}".`);
// Create the output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const files = fs.readdirSync(sourceDir).filter((file: string) => file.endsWith('.json'));
for (const file of files) {
const inputPath = path.join(sourceDir, file);
const outputPath = path.join(outputDir, file);
console.log(`Processing file "${file}"...`);
const fileContent = fs.readFileSync(inputPath, 'utf-8');
let inputSpec;
try {
inputSpec = JSON.parse(fileContent);
} catch (err) {
console.error(`Invalid JSON file "${file}". Skipping this file.`);
continue;
}
const outputSpec = processOpenAPISpec(inputSpec);
fs.writeFileSync(outputPath, JSON.stringify(outputSpec, null, 2), 'utf-8');
console.log(`Processing completed for file "${file}".`);
}
}
const sourceDirs = [
path.resolve(import.meta.dirname, '../pkg/tests/apis/openapi_snapshots'),
path.resolve(import.meta.dirname, '../pkg/extensions/apiserver/tests/openapi_snapshots'),
];
const outputDir = path.resolve(import.meta.dirname, '../data/openapi');
for (const sourceDir of sourceDirs) {
processDirectory(sourceDir, outputDir);
}
+79 -79
View File
@@ -2704,13 +2704,13 @@ __metadata:
resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@kusto/monaco-kusto": "npm:^10.0.0"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/jest-dom": "npm:6.6.3"
@@ -2748,13 +2748,13 @@ __metadata:
resolution: "@grafana-plugins/grafana-postgresql-datasource@workspace:public/app/plugins/datasource/grafana-postgresql-datasource"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/sql": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/sql": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
@@ -2779,11 +2779,11 @@ __metadata:
resolution: "@grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/jest-dom": "npm:6.6.3"
"@testing-library/react": "npm:16.2.0"
@@ -2819,12 +2819,12 @@ __metadata:
resolution: "@grafana-plugins/grafana-testdata-datasource@workspace:public/app/plugins/datasource/grafana-testdata-datasource"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
@@ -2900,13 +2900,13 @@ __metadata:
resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/sql": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/sql": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
@@ -2931,13 +2931,13 @@ __metadata:
resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/sql": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/sql": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
@@ -2962,11 +2962,11 @@ __metadata:
resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
@@ -2994,14 +2994,14 @@ __metadata:
resolution: "@grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/google-sdk": "npm:0.1.2"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/jest-dom": "npm:6.6.3"
"@testing-library/react": "npm:16.2.0"
@@ -3046,7 +3046,7 @@ __metadata:
"@grafana/lezer-traceql": "npm:0.0.20"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/o11y-ds-frontend": "workspace:*"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/plugin-configs": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
@@ -3194,12 +3194,12 @@ __metadata:
languageName: node
linkType: hard
"@grafana/data@npm:11.5.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
"@grafana/data@npm:11.6.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data":
version: 0.0.0-use.local
resolution: "@grafana/data@workspace:packages/grafana-data"
dependencies:
"@braintree/sanitize-url": "npm:7.0.1"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@rollup/plugin-node-resolve": "npm:16.0.0"
"@types/d3-interpolate": "npm:^3.0.0"
@@ -3257,7 +3257,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/e2e-selectors@npm:11.5.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
"@grafana/e2e-selectors@npm:11.6.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors":
version: 0.0.0-use.local
resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors"
dependencies:
@@ -3360,9 +3360,9 @@ __metadata:
"@babel/preset-env": "npm:7.26.7"
"@babel/preset-react": "npm:7.26.3"
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@leeoniya/ufuzzy": "npm:1.0.18"
"@rollup/plugin-node-resolve": "npm:16.0.0"
"@testing-library/dom": "npm:10.4.0"
@@ -3468,13 +3468,13 @@ __metadata:
resolution: "@grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/jest-dom": "npm:^6.1.2"
"@testing-library/react": "npm:16.2.0"
@@ -3498,7 +3498,7 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/plugin-configs@npm:11.5.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
"@grafana/plugin-configs@npm:11.6.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
version: 0.0.0-use.local
resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs"
dependencies:
@@ -3599,15 +3599,15 @@ __metadata:
"@emotion/css": "npm:11.13.5"
"@emotion/eslint-plugin": "npm:11.12.0"
"@floating-ui/react": "npm:0.27.3"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/faro-web-sdk": "npm:1.12.3"
"@grafana/llm": "npm:0.12.0"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@hello-pangea/dnd": "npm:17.0.0"
"@leeoniya/ufuzzy": "npm:1.0.18"
"@lezer/common": "npm:1.2.3"
@@ -3702,16 +3702,16 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/runtime@npm:11.5.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
"@grafana/runtime@npm:11.6.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
version: 0.0.0-use.local
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
dependencies:
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/faro-web-sdk": "npm:^1.3.6"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@rollup/plugin-node-resolve": "npm:16.0.0"
"@rollup/plugin-terser": "npm:0.4.4"
"@testing-library/dom": "npm:10.4.0"
@@ -3851,7 +3851,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/schema@npm:11.5.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
"@grafana/schema@npm:11.6.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local
resolution: "@grafana/schema@workspace:packages/grafana-schema"
dependencies:
@@ -3869,17 +3869,17 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/sql@npm:11.5.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql":
"@grafana/sql@npm:11.6.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql":
version: 0.0.0-use.local
resolution: "@grafana/sql@workspace:packages/grafana-sql"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "npm:11.5.0-pre"
"@grafana/runtime": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@grafana/ui": "npm:11.5.0-pre"
"@grafana/ui": "npm:11.6.0-pre"
"@react-awesome-query-builder/ui": "npm:6.6.4"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/jest-dom": "npm:^6.1.2"
@@ -3997,7 +3997,7 @@ __metadata:
languageName: node
linkType: hard
"@grafana/ui@npm:11.5.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
"@grafana/ui@npm:11.6.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui":
version: 0.0.0-use.local
resolution: "@grafana/ui@workspace:packages/grafana-ui"
dependencies:
@@ -4007,10 +4007,10 @@ __metadata:
"@emotion/serialize": "npm:1.3.3"
"@faker-js/faker": "npm:^9.0.0"
"@floating-ui/react": "npm:0.27.3"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/data": "npm:11.6.0-pre"
"@grafana/e2e-selectors": "npm:11.6.0-pre"
"@grafana/faro-web-sdk": "npm:^1.3.6"
"@grafana/schema": "npm:11.5.0-pre"
"@grafana/schema": "npm:11.6.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@hello-pangea/dnd": "npm:17.0.0"
"@leeoniya/ufuzzy": "npm:1.0.18"
@@ -8712,7 +8712,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.11.2"
"@grafana/data": "workspace:*"
"@grafana/plugin-configs": "npm:11.5.0-pre"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
"@grafana/ui": "workspace:*"