Compare commits

..

24 Commits

Author SHA1 Message Date
Gábor Farkas 9a89e155e7 datasources: querier: log the statuscode not the pointer 2026-01-13 13:40:47 +01:00
alerting-team[bot] 5dbbe8164b Alerting: Update alerting module to 98a49ed9557fd9b5f33ecb77cbaa0748f13dc568 (#116197)
* [create-pull-request] automated change

* update prometheus-alertmanager

---------

Co-authored-by: titolins <8942194+titolins@users.noreply.github.com>
Co-authored-by: Tito Lins <tito.linsesilva@grafana.com>
2026-01-13 12:27:35 +00:00
Tobias Skarhed d1064da4cd Scopes: Add RTK Query API client for caching (#115494)
* Scopes API client

* Initial RTK query commit

* Copy API client from generated enterprise folder

* Mock ScopesApiClient for integration tests

* Update e2e tests

* Handle group expansion for dashboard navigation

* Extract integration test mocks

* Move mock to only be for integration tests

* Update path for enterprise sync script

* Re-export mockData

* Disregard caching for search

* Leave name parameters empty

* Disable subscriptions for client requests

* Add functionality to reset cache between mocked requests

* Use grafana-test-utils for scopes integration tests

* Rollback mock setup

* Remove store form window object

* Remove cache helper

* Restore scopenode search functionality

* Improve request erro handling

* Clean up subscription in case subscription: false lies

* Fix logging security risk

* Rewrite tests to cover RTK query usage and improve error catching

* Update USE_LIVE_DATA to be consistent

* Remove unused timout parameter

* Fix error handling

* Make dashboard-navigation test pass
2026-01-13 13:09:08 +01:00
Tito Lins b57b8d4359 fix: handle go mod issues (#116187) 2026-01-13 12:48:16 +01:00
Mustafa Sencer Özcan 5219ccddb6 fix: improve resilience for unified storage and search service grpc clients (#116122)
* fix: reliability

* fix: resilience

* fix: add connection backoff

* fix: reduce backoff
2026-01-13 11:42:21 +00:00
Ashley Harrison c95e3da2d5 Theme: Convert themes to json and define schemas using zod (#116006)
* convert all theme files to json

* automatically discover extra themes in go backend

* use zod

* error tidy up

* error tidy up p2

* generate theme json schema from zod

* generate theme list at build time, don't do it at runtime

* make name and id required in the theme schema
2026-01-13 11:13:11 +00:00
Gareth 43d9fbc056 Tempo: Fix search streaming queries (#116136)
* Tempo: Fix search queries

* apply variables for metrics streaming queries
2026-01-13 19:47:44 +09:00
Konrad Lalik 7b80c44ac7 Alerting: Fix label value search not filtering results (#116133)
Fixes the issue where typing in the label value dropdown would display
all values instead of filtering them based on the search input.

The bug was in `createAsyncValuesLoader` which was ignoring the
`valueQuery` parameter and returning all combined values instead of
the filtered subset.

Changes:
- Rename `_inputValue` parameter to `valueQuery` to indicate it should be used
- Filter combined values based on case-insensitive search query
- Return only filtered values instead of all values

Tests:
- Add test to verify correct values are shown for each label key
- Add test to verify search filtering works correctly
- Improve test infrastructure with proper portal container and element mocking
  for virtualized dropdown rendering
2026-01-13 11:43:07 +01:00
Rafael Bortolon Paulovic 98f271f345 chore(unified): remove unifiedStorageSearchSprinkles feature toggle (#116139)
chore: remove unifiedStorageSearchSprinkles feature flag

The feature flag is no longer needed because:
- OSS: usageinsights code doesn't exist in OSS builds
- Enterprise On-Prem: uses local SQL storage when enable_search=true
- Cloud: explicitly configures sprinkles_api_server URL

The sprinkles functionality now works automatically based on:
- enable_search config (enforced true for unified storage mode 5)
- sprinkles_api_server config (empty = local storage, set = remote API)
2026-01-13 11:24:13 +01:00
Vardan Torosyan 60c4fab063 [Docs] Add Synthentic Monitoring app to the list of RBAC supported apps (#116167)
* [Docs] Add Synthentic Monitoring app to the list of RBAC supported apps

* Run prettier
2026-01-13 11:23:33 +01:00
Ihor Yeromin ce8663ac24 SQL Expressions: Filter Dashboard datasource queries from schema fetching (#116129)
* fix(sql expression): sql schema frontend datasources filtering

* add one more test
2026-01-13 10:26:33 +01:00
Yulia Shanyrova 5dd9a14903 Plugins: Fix the flaky configuration tab on the plugin details page for cloud instances (#114922)
Fix flaky configuration tab for plugin details page at cloud instances
2026-01-13 09:55:52 +01:00
Roberto Jiménez Sánchez 68bf19d840 Provisioning: handle resource version conflicts in connection CRUDL test (#116184)
fix: handle resource version conflicts in connection CRUDL test

After updating a connection resource, the controller may update the
resource status, changing the resource version. This causes the delete
operation to fail with a resource version conflict.

Add retry logic to handle conflicts gracefully by retrying the delete
operation when encountering resource version conflicts.
2026-01-13 08:53:54 +00:00
Costa Alexoglou 220c29de89 fix: 401 in grafana live spam (#116140) 2026-01-13 09:46:06 +01:00
Oscar Kilhed 91ab753368 Dynamic Dashboards: Fix navigation to repeated panels and update outline when lazy items repeat (#116030)
Dashboard Outline: Fix navigation to repeated panels and lazy-loaded repeats

- Remove cursor: not-allowed styling from repeated panels in outline
- Add RepeatsUpdatedEvent to notify when panel repeats are populated
- Subscribe to RepeatsUpdatedEvent in DashboardEditPane to refresh outline
- Remove memoization from visibleChildren to ensure outline updates on re-render
2026-01-13 08:43:50 +01:00
Alex Khomenko 250ca7985f Provisioning: Add Connections page (#116060)
* Provisioning: Add connections page

* Provisioning: Add connections form

* Provisioning: Add connections form

* Update fields

* Fix generated name

* Update connection name

* Add edit page

* error handling

* Form validation

* Add Connections button

* Cleanup

* Extract ConnectionFormData type

* Add list test and separate empty states

* Add form test

* Update tests

* i18n

* Cleanup

* Use SecretTextArea from grafana-ui

* Fix breadcrumbs

* tweaks

* Add missing URL

* Switch to ShowConfirmModalEvent

* i18n

* redirect to list on success

* add timeout

* Fix tags invalidation
2026-01-13 08:25:40 +02:00
Hugo Häggmark b57ed32484 chore: remove app/core/config barrel files (#116068) 2026-01-13 06:23:21 +01:00
Galen Kistler d0217588a3 LogsDrilldown: Remove exploreLogsLimitedTimeRange flag (#116177)
chore: remove flag
2026-01-12 22:43:01 +00:00
Denis Vodopianov ce9ab6a89a Add non-boolean feature flags support to the StaticProvider (#115085)
* initial commit

* add support of integerts

* finialise the static provider

* minor refactoring

* the rest

* revert:  the rest

* add new thiongs

* more tests added

* add ff parsing tests to check if types are handled correctly

* update tests according to recent changes

* address golint issues

* Update pkg/setting/setting_feature_toggles.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* fix rebase issues

* addressing review comments

* add test cases for enterprise

* handle enterprise cases

* minor refactoring to make api a bit easier to debug

* make test names a bit more precise

* fix linter

* add openfeature sdk to goleak ignore in testutil

* Remove only boolean check in ff gen tests

* add non-boolean types top the doc in default.ini and doc string in FeatureFlag type

* apply remarks, add docs to sample.ini

* reflect changes in feature flags in the public grafana configuration doc

* fix doc formatting

* apply suggestions to the doc file

---------

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
2026-01-12 22:53:23 +01:00
Will Assis 8c8efd2494 unified-storage: skip sqlkv/sqlbackend compatibility tests in sqlite (#116164) 2026-01-12 16:31:29 -05:00
Will Assis 69ccfd6bfc unified-storage: fix sharedwithme search not returning folders (#116089)
* unified-storage: fix dashboard sharedwithme search not returning folders shared with the user
2026-01-12 15:33:34 -05:00
Nick Richmond 53aa5e8f7f MetricsDrilldown: Remove exploreMetricsRelatedLogs feature toggle (#116090)
chore: remove unused exploreMetricsRelatedLogs feature toggle
2026-01-12 12:52:40 -05:00
Ida Štambuk 69bf3068b3 Dashboards: Never show scopes variables (#116132) 2026-01-12 18:52:23 +01:00
Will Assis 1263a3d364 unified-storage: HappyPath and notifier tests + couple of bugfixes (#116087)
* unified-storage: couple of bugfixes and enable HappyPath and notifier sqlkv tests
2026-01-12 12:17:41 -05:00
232 changed files with 8156 additions and 4098 deletions
+6 -1
View File
@@ -135,7 +135,7 @@ i18n-extract-enterprise:
@echo "Skipping i18n extract for Enterprise: not enabled"
else
i18n-extract-enterprise:
@echo "Extracting i18n strings for Enterprise"
@echo "Extracting i18n strings for Enterprise"
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
endif
@@ -227,6 +227,10 @@ fix-cue:
gen-jsonnet:
go generate ./devenv/jsonnet
.PHONY: gen-themes
gen-themes:
go generate ./pkg/services/preference
.PHONY: update-workspace
update-workspace: gen-go
@echo "updating workspace"
@@ -244,6 +248,7 @@ build-go-fast: ## Build all Go binaries without updating workspace.
.PHONY: build-backend
build-backend: ## Build Grafana backend.
@echo "build backend"
$(MAKE) gen-themes
$(GO) run build.go $(GO_BUILD_FLAGS) build-backend
.PHONY: build-air
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.25.5
require (
github.com/go-kit/log v0.2.1
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7
+2 -2
View File
@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
+1 -1
View File
@@ -97,7 +97,7 @@ require (
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
+2 -2
View File
@@ -215,8 +215,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
+7 -1
View File
@@ -336,7 +336,7 @@ rudderstack_data_plane_url =
rudderstack_sdk_url =
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
rudderstack_v3_sdk_url =
rudderstack_v3_sdk_url =
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
rudderstack_config_url =
@@ -2079,8 +2079,14 @@ enable =
# To enable features by default, set `Expression: "true"` in:
# https://github.com/grafana/grafana/blob/main/pkg/services/featuremgmt/registry.go
# The feature_toggles section supports feature flags of a number of types,
# including boolean, string, integer, float, and structured values, following the OpenFeature specification.
#
# feature1 = true
# feature2 = false
# feature3 = "foobar"
# feature4 = 1.5
# feature5 = { "foo": "bar" }
[feature_toggles.openfeature]
# This is EXPERIMENTAL. Please, do not use this section
+8 -3
View File
@@ -323,7 +323,7 @@
;rudderstack_sdk_url =
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
;rudderstack_v3_sdk_url =
;rudderstack_v3_sdk_url =
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
;rudderstack_config_url =
@@ -1913,7 +1913,7 @@ default_datasource_uid =
# client_queue_max_size is the maximum size in bytes of the client queue
# for Live connections. Defaults to 4MB.
;client_queue_max_size =
;client_queue_max_size =
#################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer]
@@ -1996,9 +1996,14 @@ default_datasource_uid =
;enable = feature1,feature2
# The feature_toggles section supports feature flags of a number of types,
# including boolean, string, integer, float, and structured values, following the OpenFeature specification.
;feature1 = true
;feature2 = false
;feature3 = "foobar"
;feature4 = 1.5
;feature5 = { "foo": "bar" }
[date_formats]
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
@@ -66,17 +66,18 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
The following list contains app plugins that have fine-grained RBAC support.
| App plugin | App plugin ID | App plugin permission documentation |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
| App plugin | App plugin ID | App plugin permission documentation |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
| [Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/) | `grafana-synthetic-monitoring-app` | [Configure RBAC for Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/user-and-team-management/) |
### Revoke fine-grained access from app plugins
@@ -322,7 +322,7 @@ If you continue to experience issues after following this troubleshooting guide:
1. Review the [Grafana GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
1. Enable debug logging in Grafana to capture detailed error information.
1. Check SQL Server logs for additional error details.
1. Contact [Grafana Support](https://grafana.com/contact/) if you're an Enterprise or Cloud customer.
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
When reporting issues, include:
+20 -113
View File
@@ -17,145 +17,52 @@ menuTitle: MySQL
title: MySQL data source
weight: 1000
refs:
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
configure-mysql-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configuration/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configuration/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
troubleshoot-mysql:
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
transformations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/transform-data/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/
visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
query-caching:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
postgres:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
mssql:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/
---
# MySQL data source
Grafana ships with built-in support for MySQL.
You can query and visualize data from MySQL-compatible databases like [MariaDB](https://mariadb.org/) or [Percona Server](https://www.percona.com/).
Grafana ships with a built-in MySQL data source plugin that allows you to query and visualize data from a MySQL-compatible database like [MariaDB](https://mariadb.org/) or [Percona Server](https://www.percona.com/). You don't need to install a plugin in order to add the MySQL data source to your Grafana instance.
Use this data source to create dashboards, explore SQL data, and monitor MySQL-based workloads in real time.
Grafana offers several configuration options for this data source as well as a visual and code-based query editor.
{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}}
## Get started with the MySQL data source
## Supported databases
This data source supports the following MySQL-compatible databases:
- MySQL 5.7 and newer
- MySQL 8.0 and newer
- MariaDB 10.2 and newer
- Percona Server 5.7 and newer
- Amazon Aurora MySQL
- Azure Database for MySQL
- Google Cloud SQL for MySQL
Grafana recommends using the latest available version for your database for optimal compatibility.
## Key capabilities
The MySQL data source supports:
- **Time series queries:** Visualize metrics over time using built-in time grouping macros.
- **Table queries:** Display query results in table format for any valid SQL query.
- **Template variables:** Create dynamic dashboards with variable-driven queries.
- **Annotations:** Overlay events from MySQL on your dashboard graphs.
- **Alerting:** Create alerts based on MySQL query results.
- **Macros:** Simplify queries with built-in macros for time filtering and grouping.
## Get started
The following documentation helps you get started with the MySQL data source:
The following documents will help you get started with the MySQL data source in Grafana:
- [Configure the MySQL data source](ref:configure-mysql-data-source)
- [MySQL query editor](ref:mysql-query-editor)
- [MySQL template variables](ref:mysql-template-variables)
- [MySQL annotations](ref:mysql-annotations)
- [MySQL alerting](ref:mysql-alerting)
- [Troubleshoot MySQL data source issues](ref:troubleshoot-mysql)
## Additional resources
Once you have configured the data source you can:
After configuring the MySQL data source, you can also:
- Add [annotations](ref:annotate-visualizations)
- Set up [alerting](ref:alerting)
- Add [transformations](ref:transformations)
- Create a wide variety of [visualizations](ref:visualizations).
- Configure and use [templates and variables](ref:variables).
- Add [transformations](ref:transformations).
- Optimize performance with [query caching](ref:query-caching).
View a MySQL overview on Grafana Play:
## Pre-configured dashboards
If you want to monitor your MySQL server's performance metrics (connections, queries, replication, and more), Grafana provides pre-configured dashboards through the MySQL integration:
- **MySQL Overview** - Key performance metrics for your MySQL server.
- **MySQL Logs** - Log analysis for troubleshooting.
The MySQL integration uses the Prometheus MySQL Exporter to collect server metrics and includes 15 pre-configured alert rules.
To use these dashboards:
1. In Grafana Cloud, navigate to **Connections** > **Add new connection**.
1. Search for **MySQL** and select the MySQL integration.
1. Follow the setup instructions to install the MySQL Exporter.
1. Import the pre-configured dashboards from the integration page.
For more MySQL dashboards, browse the [Grafana dashboard catalog](https://grafana.com/grafana/dashboards/?search=mysql).
{{< admonition type="note" >}}
The MySQL integration monitors your MySQL _server_ using Prometheus metrics. The MySQL _data source_ documented here queries data stored _in_ MySQL tables. These are complementary features for different use cases.
{{< /admonition >}}
## Related data sources
- [PostgreSQL](ref:postgres) - For PostgreSQL databases.
- [Microsoft SQL Server](ref:mssql) - For Microsoft SQL Server and Azure SQL databases.
{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}}
@@ -1,188 +0,0 @@
---
description: Using Grafana Alerting with the MySQL data source
keywords:
- grafana
- mysql
- alerting
- alerts
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Alerting
title: MySQL alerting
weight: 350
refs:
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
create-alert-rule:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
---
# MySQL alerting
You can use Grafana Alerting with MySQL to create alerts based on your MySQL data. This allows you to monitor metrics, detect anomalies, and receive notifications when specific conditions are met.
For general information about Grafana Alerting, refer to [Grafana Alerting](ref:alerting).
## Before you begin
Before creating alerts with MySQL, ensure you have:
- A MySQL data source configured in Grafana.
- Appropriate permissions to create alert rules.
- Understanding of the metrics you want to monitor.
## Supported query types
MySQL alerting works with **time series queries** that return numeric data over time. Table formatted queries are not supported in alert rule conditions.
To create a valid alert query:
- Include a `time` column that returns a SQL datetime or UNIX epoch timestamp
- Return numeric values for the metrics you want to alert on
- Sort results by the time column
For more information on writing time series queries, refer to [MySQL query editor](ref:mysql-query-editor).
### Query format requirements
| Query format | Alerting support | Notes |
| ------------ | ---------------- | ---------------------------------------- |
| Time series | Yes | Required for alerting |
| Table | No | Convert to time series format for alerts |
## Create an alert rule
To create an alert rule using MySQL:
1. Navigate to **Alerting** > **Alert rules**.
1. Click **New alert rule**.
1. Enter a name for the alert rule.
1. Select your **MySQL** data source.
1. Build your query using the query editor:
- Set the **Format** to **Time series**
- Include a time column using the `$__time()` or `$__timeGroup()` macro
- Add numeric columns for the values to monitor
- Use `$__timeFilter()` to filter data by the dashboard time range
1. Configure the alert condition (for example, when the average is above a threshold).
1. Set the evaluation interval and pending period.
1. Configure notifications and labels.
1. Click **Save rule**.
For detailed instructions, refer to [Create a Grafana-managed alert rule](ref:create-alert-rule).
## Example alert queries
The following examples show common alerting scenarios with MySQL.
### Alert on high error count
Monitor the number of errors over time:
```sql
SELECT
$__timeGroup(created_at, '1m') AS time,
COUNT(*) AS error_count
FROM error_logs
WHERE $__timeFilter(created_at)
AND level = 'error'
GROUP BY time
ORDER BY time
```
**Condition:** When error_count is above 100.
### Alert on average response time
Monitor API response times:
```sql
SELECT
$__timeGroup(request_time, '5m') AS time,
AVG(response_time_ms) AS avg_response_time
FROM api_requests
WHERE $__timeFilter(request_time)
GROUP BY time
ORDER BY time
```
**Condition:** When avg_response_time is above 500 (milliseconds).
### Alert on low order volume
Detect drops in order activity:
```sql
SELECT
$__timeGroup(order_date, '1h') AS time,
COUNT(*) AS order_count
FROM orders
WHERE $__timeFilter(order_date)
GROUP BY time
ORDER BY time
```
**Condition:** When order_count is below 10.
### Alert on disk usage percentage
Monitor database storage metrics:
```sql
SELECT
$__timeGroup(recorded_at, '5m') AS time,
AVG(disk_used_percent) AS disk_usage
FROM system_metrics
WHERE $__timeFilter(recorded_at)
AND metric_type = 'disk'
GROUP BY time
ORDER BY time
```
**Condition:** When disk_usage is above 85.
## Limitations
When using MySQL with Grafana Alerting, be aware of the following limitations:
### Template variables not supported
Alert queries cannot contain template variables. Grafana evaluates alert rules on the backend without dashboard context, so variables like `$hostname` or `$environment` won't be resolved.
If your dashboard query uses template variables, create a separate query for alerting with hard coded values.
### Table format not supported
Queries using the **Table** format cannot be used for alerting. Set the query format to **Time series** and ensure your query returns a time column.
### Query timeout
Complex queries with large datasets may timeout during alert evaluation. Optimize queries for alerting by:
- Adding appropriate `WHERE` clauses to limit data
- Using indexes on time and filter columns
- Reducing the time range evaluated
## Best practices
Follow these best practices when creating MySQL alerts:
- **Use time series format:** Always set the query format to Time series for alert queries.
- **Include time filters:** Use the `$__timeFilter()` macro to limit data to the evaluation window.
- **Optimize queries:** Add indexes on columns used in `WHERE` clauses and `GROUP BY`.
- **Test queries first:** Verify your query returns expected results in Explore before creating an alert.
- **Set realistic thresholds:** Base alert thresholds on historical data patterns.
- **Use meaningful names:** Give alert rules descriptive names that indicate what they monitor.
@@ -1,163 +0,0 @@
---
description: Using annotations with MySQL in Grafana
keywords:
- grafana
- mysql
- annotations
- events
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Annotations
title: MySQL annotations
weight: 340
refs:
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
---
# MySQL annotations
Annotations overlay event data on your dashboard graphs, helping you correlate events with metrics.
You can use MySQL as a data source for annotations to display events such as deployments, alerts, or other significant occurrences on your visualizations.
For general information about annotations, refer to [Annotate visualizations](ref:annotate-visualizations).
## Before you begin
Before creating MySQL annotations, ensure you have:
- A MySQL data source configured in Grafana.
- Tables containing event data with timestamp fields.
- Read access to the tables containing your events.
## Create an annotation query
To add a MySQL annotation to your dashboard:
1. Navigate to your dashboard and click **Dashboard settings** (gear icon).
1. Select **Annotations** in the left menu.
1. Click **Add annotation query**.
1. Enter a **Name** for the annotation.
1. Select your **MySQL** data source from the **Data source** drop-down.
1. Write a SQL query that returns the required columns.
1. Click **Save dashboard**.
## Query columns
Your annotation query must return a `time` column and can optionally include `timeend`, `text`, and `tags` columns.
| Column | Required | Description |
| --------- | -------- | --------------------------------------------------------------------------------------------- |
| `time` | Yes | The timestamp for the annotation. Can be a SQL datetime or UNIX epoch value. |
| `timeend` | No | The end timestamp for range annotations. Creates a shaded region instead of a vertical line. |
| `text` | No | The annotation description displayed when you hover over the annotation. |
| `tags` | No | Tags for the annotation as a comma-separated string. Helps categorize and filter annotations. |
## Example queries
The following examples show common annotation query patterns.
### Basic annotation with epoch time
Display events using UNIX epoch timestamps:
```sql
SELECT
epoch_time as time,
description as text,
CONCAT(tag1, ',', tag2) as tags
FROM events
WHERE $__unixEpochFilter(epoch_time)
```
### Annotation with a single tag
Display events with a single tag value:
```sql
SELECT
epoch_time as time,
message as text,
category as tags
FROM event_log
WHERE $__unixEpochFilter(epoch_time)
```
### Range annotation with start and end time
Display events with duration as shaded regions:
```sql
SELECT
start_time as time,
end_time as timeend,
description as text,
CONCAT(type, ',', severity) as tags
FROM incidents
WHERE $__unixEpochFilter(start_time)
```
### Annotation with native SQL datetime
Display events using native MySQL datetime columns:
```sql
SELECT
event_date as time,
message as text,
CONCAT(category, ',', priority) as tags
FROM system_events
WHERE $__timeFilter(event_date)
```
### Deployment annotations
Display deployment events:
```sql
SELECT
deployed_at as time,
CONCAT('Deployed ', version, ' to ', environment) as text,
environment as tags
FROM deployments
WHERE $__timeFilter(deployed_at)
```
### Maintenance window annotations
Display maintenance windows as range annotations:
```sql
SELECT
start_time as time,
end_time as timeend,
CONCAT('Maintenance: ', description) as text,
'maintenance' as tags
FROM maintenance_windows
WHERE $__timeFilter(start_time)
```
## Macros
Use these macros in your annotation queries to filter by the dashboard time range:
| Macro | Description |
| ---------------------------- | ---------------------------------------------------------------- |
| `$__timeFilter(column)` | Filters by time range using a native SQL datetime column. |
| `$__unixEpochFilter(column)` | Filters by time range using a column with UNIX epoch timestamps. |
## Best practices
Follow these best practices when creating MySQL annotations:
- **Use time filters:** Always include `$__timeFilter()` or `$__unixEpochFilter()` to limit results to the dashboard time range.
- **Keep queries efficient:** Add indexes on time columns and filter columns to improve query performance.
- **Use meaningful text:** Include descriptive information in the `text` column to make annotations useful.
- **Organize with tags:** Use consistent tag values to categorize annotations and enable filtering.
- **Test queries first:** Verify your query returns expected results in Explore before adding it as an annotation.
@@ -1,6 +1,4 @@
---
aliases:
- ../configuration/
description: This document provides instructions for configuring the MySQL data source and explains available configuration options.
keywords:
- grafana
@@ -12,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Configure
menuTitle: Configure the MySQL data source
title: Configure the MySQL data source
weight: 10
refs:
@@ -36,41 +34,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
mysql-troubleshoot:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
---
# Configure the MySQL data source
@@ -79,35 +42,24 @@ This document provides instructions for configuring the MySQL data source and ex
## Before you begin
Before configuring the MySQL data source, ensure you have the following:
- **Grafana permissions:** You must have the `Organization administrator` role to configure data sources. Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system.
- **A running MySQL instance:** MySQL 5.7 or newer, MariaDB 10.2 or newer, or a compatible MySQL-based database such as Percona Server.
- **Network access:** Grafana must be able to reach your MySQL server. The default port is `3306`.
- **Authentication credentials:** A MySQL user with at least `SELECT` permissions on the databases and tables you want to query.
- **Security certificates:** If using encrypted connections, gather any necessary TLS/SSL certificates.
You must have the `Organization administrator` role in order to configure the MySQL data source.
Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system.
{{< admonition type="note" >}}
Grafana ships with a built-in MySQL data source plugin. No additional installation is required.
Grafana ships with the MySQL data source by default, so no additional installation is required.
{{< /admonition >}}
{{< admonition type="tip" >}}
**Grafana Cloud users:** If your MySQL server is in a private network, you can configure [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) to establish connectivity.
{{< admonition type="caution" >}}
When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana does not validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed.
To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions.
{{< /admonition >}}
### Database user permissions
When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana doesn't validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed.
To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions:
Example:
```sql
CREATE USER 'grafanaReader' IDENTIFIED BY 'password';
GRANT SELECT ON mydatabase.mytable TO 'grafanaReader';
CREATE USER 'grafanaReader' IDENTIFIED BY 'password';
GRANT SELECT ON mydatabase.mytable TO 'grafanaReader';
```
Use wildcards (`*`) in place of a database or table if you want to grant access to more databases and tables.
@@ -261,46 +213,3 @@ datasources:
tlsClientCert: ${GRAFANA_TLS_CLIENT_CERT}
tlsCACert: ${GRAFANA_TLS_CA_CERT}
```
## Configure with Terraform
You can configure the MySQL data source using [Terraform](https://www.terraform.io/) with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
For more information about provisioning resources with Terraform, refer to the [Grafana as code using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/) documentation.
### Terraform example
The following example creates a basic MySQL data source:
```hcl
resource "grafana_data_source" "mysql" {
name = "MySQL"
type = "mysql"
url = "localhost:3306"
user = "grafana"
json_data_encoded = jsonencode({
database = "grafana"
maxOpenConns = 100
maxIdleConns = 100
maxIdleConnsAuto = true
connMaxLifetime = 14400
})
secure_json_data_encoded = jsonencode({
password = "password"
})
}
```
For all available configuration options, refer to the [Grafana provider data source resource documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source).
## Next steps
After configuring your MySQL data source, you can:
- [Write queries](ref:mysql-query-editor) using the query editor to explore and visualize your data.
- [Use template variables](ref:mysql-template-variables) to create dynamic, reusable dashboards.
- [Add annotations](ref:mysql-annotations) to overlay MySQL events on your graphs.
- [Set up alerting](ref:mysql-alerting) to create alert rules based on your MySQL data.
- [Troubleshoot issues](ref:mysql-troubleshoot) if you encounter problems with your data source.
@@ -9,7 +9,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Query editor
menuTitle: MySQL query editor
title: MySQL query editor
weight: 30
refs:
@@ -61,21 +61,6 @@ refs:
configure-standard-options:
- pattern: /docs/grafana/
- destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
---
# MySQL query editor
@@ -320,20 +305,171 @@ Table panel result:
The query returns multiple columns representing minimum and maximum values within the defined range.
## Template variables
## Templating
Instead of hard-coding values like server, application, or sensor names in your metric queries, you can use variables. Variables appear as drop-down select boxes at the top of the dashboard, making it easy to change the data displayed in your dashboard.
Instead of hardcoding values like server, application, or sensor names in your metric queries, you can use variables. Variables appear as drop-down select boxes at the top of the dashboard. These drop-downs make it easy to change the data being displayed in your dashboard.
For detailed information on using template variables with MySQL, refer to [MySQL template variables](ref:mysql-template-variables).
Refer to [Templates](ref:variables) for an introduction to creating template variables as well as the different types.
### Query variable
If you add a `Query` template variable you can write a MySQL query to retrieve items such as measurement names, key names, or key values, which will be displayed in the drop-down menu.
For example, you can use a variable to retrieve all the values from the `hostname` column in a table by creating the following query in the templating variable _Query_ setting.
```sql
SELECT hostname FROM my_host
```
A query can return multiple columns, and Grafana will automatically generate a list based on the query results. For example, the following query returns a list with values from `hostname` and `hostname2`.
```sql
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
To use time range dependent macros like `$__timeFilter(column)` in your query,you must set the template variable's refresh mode to _On Time Range Change_.
```sql
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
```
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column must contain unique values (if not, only the first value is used). This allows the drop-down options to display a text-friendly name as the text while using an ID as the value. For example, a query could use `hostname` as the text and `id` as the value:
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
You can also create nested variables. For example, if you have a variable named `region`, you can configure the `hosts` variable to display only the hosts within the currently selected region as shown in the following example. If `region` is a multi-value variable, use the `IN` operator instead of `=` to match multiple values.
```sql
SELECT hostname FROM my_host WHERE region IN($region)
```
#### Use `__searchFilter` to filter results in a query variable
Using `__searchFilter` in the query field allows the query results to be filtered based on the users input in the drop-down selection box. If you do not enter anything, the default value for `__searchFilter` is %
Note that you must enclose the `__searchFilter` expression in quotes as Grafana does not add them automatically.
The following example demonstrates how to use `__searchFilter` in the query field to enable real-time searching for `hostname` as the user type in the drop-down selection box.
```sql
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
### Using variables in queries
Template variable values are only quoted when the template variable is a `multi-value`.
If the variable is a multi-value variable, use the `IN` comparison operator instead of `=` to match against multiple values.
You can use two different syntaxes:
`$<varname>` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in($hostname)
ORDER BY atimestamp ASC
```
`[[varname]]` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
#### Disabling quoting for multi-value variables
Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. To disable quoting, use the csv formatting option for variables:
Grafana automatically formats multi-value variables as a quoted, comma-separated string. For example, if `server01` and `server02` are selected, they are formatted as `'server01'`, `'server02'`. To remove the quotes, enable the CSV formatting option for the variables.
`${servers:csv}`
Read more about variable formatting options in the [Variables](ref:variable-syntax-advanced-variable-format-options) documentation.
## Annotations
Annotations allow you to overlay event information on your graphs, helping you correlate events with metrics. You can write SQL queries that return event data to display as annotations on your dashboards.
[Annotations](ref:annotate-visualizations) allow you to overlay rich event information on top of graphs. You add annotation queries via the **Dashboard settings > Annotations view**.
For detailed information on creating annotations with MySQL, refer to [MySQL annotations](ref:mysql-annotations).
**Example query using a`time` column with epoch values:**
```sql
SELECT
epoch_time as time,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__unixEpochFilter(epoch_time)
```
You may use one or more tags to show them as annotations in a common-separate string.
**Example query using a `time` column with epoch values for a single tag:**
```sql
SELECT
epoch_time as time,
metric1 as text,
tag1 as tag
FROM
my_data
WHERE
$__unixEpochFilter(epoch_time)
```
**Example region query using `time` and `timeend` columns with epoch values:**
```sql
SELECT
epoch_time as time,
epoch_timeend as timeend,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__unixEpochFilter(epoch_time)
```
**Example query using a `time` column with a native SQL date/time data type:**
```sql
SELECT
native_date_time as time,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__timeFilter(native_date_time)
```
| Name | Description |
| --------- | --------------------------------------------------------------------------------------------------------------------- |
| `time` | The name of the date/time field, which can be a column with a native SQL date/time data type or epoch value. |
| `timeend` | Optional name of the end date/time field, which can be a column with a native SQL date/time data type or epoch value. |
| `text` | Event description field. |
| `tags` | Optional field name to use for event tags as a comma separated string. |
## Alerting
You can use time series queries to create Grafana-managed alert rules. Table formatted queries are not supported in alert rule conditions.
Use time series queries to create alerts. Table formatted queries aren't yet supported in alert rule conditions.
For detailed information on creating alerts with MySQL, refer to [MySQL alerting](ref:mysql-alerting).
For more information regarding alerting refer to the following:
- [Alert rules](ref:alert-rules)
- [Template annotations and labels](ref:template-annotations-and-labels)
@@ -1,146 +0,0 @@
---
description: Using template variables with MySQL in Grafana
keywords:
- grafana
- mysql
- templates
- variables
- queries
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Template variables
title: MySQL template variables
weight: 300
refs:
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
variable-syntax-advanced-variable-format-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
add-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
---
# MySQL template variables
Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables.
Grafana displays these variables in drop-down select boxes at the top of the dashboard to help you change the data displayed in your dashboard.
Grafana refers to such variables as **template variables**.
For an introduction to templating and template variables, refer to [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables).
## Query variable
A query variable in Grafana dynamically retrieves values from your data source using a query. With a query variable, you can write a SQL query that returns values such as measurement names, key names, or key values that are shown in a drop-down select box.
For example, the following query returns all values from the `hostname` column:
```sql
SELECT hostname FROM my_host
```
A query can return multiple columns, and Grafana automatically generates a list using the values from those columns. For example, the following query returns values from both the `hostname` and `hostname2` columns, which are included in the variable's drop-down list.
```sql
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
To use time range dependent macros like `$__timeFilter(column)` in your query, you must set the template variable's refresh mode to **On Time Range Change**.
```sql
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
```
### Key/value variables
You can create a key/value variable using a query that returns two columns named `__text` and `__value`.
- The `__text` column defines the label shown in the drop-down.
- The `__value` column defines the value passed to panel queries.
This is useful when you want to display a user-friendly label (like a hostname) but use a different underlying value (like an ID).
Note that the values in the `__text` column should be unique. If there are duplicates, Grafana uses only the first matching entry.
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
### Nested variables
You can create nested variables, where one variable depends on the value of another. For example, if you have a variable named `region`, you can configure a `hosts` variable to only show hosts from the selected region. If `region` is a multi-value variable, use the `IN` operator instead of `=` to match against multiple selected values.
```sql
SELECT hostname FROM my_host WHERE region IN($region)
```
### Filter results with `__searchFilter`
Using `__searchFilter` in the query field allows the query results to be filtered based on the user's input in the drop-down selection box. If you don't enter anything, the default value for `__searchFilter` is `%`.
Note that you must enclose the `__searchFilter` expression in quotes as Grafana doesn't add them automatically.
The following example demonstrates how to use `__searchFilter` in the query field to enable real-time searching for `hostname` as the user types in the drop-down selection box.
```sql
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
## Use variables in queries
Grafana automatically quotes template variable values only when the template variable is a `multi-value`.
When using a multi-value variable, use the `IN` comparison operator instead of `=` to match against multiple values.
Grafana supports two syntaxes for using variables in queries:
- **`$<varname>` syntax**
Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in($hostname)
ORDER BY atimestamp ASC
```
- **`[[varname]]` syntax**
Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
### Disable quoting for multi-value variables
By default, Grafana formats multi-value variables as a quoted, comma-separated string. For example, if `server01` and `server02` are selected, the result will be `'server01'`, `'server02'`. To disable quoting, use the `csv` formatting option for variables:
```text
${servers:csv}
```
This outputs the values as an unquoted comma-separated list.
Refer to [Advanced variable format options](ref:variable-syntax-advanced-variable-format-options) for additional information.
@@ -0,0 +1,80 @@
---
description: Learn how to troubleshoot common problems with the Grafana MySQL data source plugin
keywords:
- grafana
- mysql
- query
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Troubleshoot
title: Troubleshoot common problems with the Grafana MySQL data source plugin
weight: 40
refs:
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
variable-syntax-advanced-variable-format-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#advanced-variable-format-options
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
explore:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
query-transform-data:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/
panel-inspector:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-inspector/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-inspector/
query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/#query-editors
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#query-editors
alert-rules:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/
template-annotations-and-labels:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/templates/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/
configure-standard-options:
- pattern: /docs/grafana/
- destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/
---
# Troubleshoot common problems with the Grafana MySQL data source plugin
This page lists common issues you might experience when setting up the Grafana MySQL data source plugin.
### My data source connection fails when using the Grafana MySQL data source plugin
- Check if the MySQL server is up and running.
- Make sure that your firewall is open for MySQL server (default port is `3306`).
- Ensure that you have the correct permissions to access the MySQL server and also have permission to access the database.
- If the error persists, create a new user for the Grafana MySQL data source plugin with correct permissions and try to connect with it.
### What should I do if I see "An unexpected error happened" or "Could not connect to MySQL" after trying all of the above?
- Check the Grafana logs for more details about the error.
- For Grafana Cloud customers, contact support.
@@ -1,370 +0,0 @@
---
aliases:
- ../troubleshoot/
description: Troubleshoot common problems with the MySQL data source in Grafana
keywords:
- grafana
- mysql
- troubleshooting
- errors
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Troubleshooting
title: Troubleshoot MySQL data source issues
weight: 400
refs:
configure-mysql-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
private-data-source-connect:
- pattern: /docs/grafana/
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
---
# Troubleshoot MySQL data source issues
This document provides solutions to common issues you may encounter when configuring or using the MySQL data source in Grafana.
## Connection errors
These errors occur when Grafana cannot establish or maintain a connection to the MySQL server.
### Unable to connect to the server
**Error message:** "dial tcp: connection refused" or "Could not connect to MySQL"
**Cause:** Grafana cannot establish a network connection to the MySQL server.
**Solution:**
1. Verify that the MySQL server is running and accessible.
1. Check that the host and port are correct in the data source configuration. The default MySQL port is `3306`.
1. Ensure there are no firewall rules blocking the connection between Grafana and MySQL.
1. Verify that MySQL is configured to allow remote connections by checking the `bind-address` setting in your MySQL configuration.
1. For Grafana Cloud, ensure you have configured [Private data source connect](ref:private-data-source-connect) if your MySQL instance isn't publicly accessible.
### Connection timeout
**Error message:** "Connection timed out" or "I/O timeout"
**Cause:** The connection to MySQL timed out before receiving a response.
**Solution:**
1. Check the network latency between Grafana and MySQL.
1. Verify that MySQL isn't overloaded or experiencing performance issues.
1. Check if any network devices (load balancers, proxies) are timing out the connection.
1. Increase the `wait_timeout` setting in MySQL if connections are timing out during idle periods.
### TLS/SSL connection failures
**Error message:** "TLS handshake failed" or "x509: certificate verify failed"
**Cause:** There is a mismatch between the TLS settings in Grafana and what the MySQL server supports or requires.
**Solution:**
1. Verify that the MySQL server has a valid SSL certificate if encryption is enabled.
1. Check that the certificate is trusted by the Grafana server.
1. If using a self-signed certificate, enable **With CA Cert** and provide the root certificate under **TLS/SSL Root Certificate**.
1. To bypass certificate validation (not recommended for production), enable **Skip TLS Verification** in the data source configuration.
1. Ensure the SSL certificate hasn't expired.
### Connection reset by peer
**Error message:** "Connection reset by peer" or "EOF"
**Cause:** The MySQL server closed the connection unexpectedly.
**Solution:**
1. Check the `max_connections` setting on the MySQL server to ensure it isn't being exceeded.
1. Verify the `wait_timeout` and `interactive_timeout` settings in MySQL aren't set too low.
1. Increase the **Max lifetime** setting in Grafana's data source configuration to be lower than MySQL's `wait_timeout`.
1. Check MySQL server logs for any errors or connection-related messages.
## Authentication errors
These errors occur when there are issues with authentication credentials or permissions.
### Access denied for user
**Error message:** "Access denied for user 'username'@'host'" or "Authentication failed"
**Cause:** The authentication credentials are invalid or the user doesn't have permission to connect from the Grafana server's host.
**Solution:**
1. Verify that the username and password are correct.
1. Check that the user exists in MySQL and is enabled.
1. Ensure the user has permission to connect from the Grafana server's IP address. MySQL restricts access based on the connecting host:
```sql
SELECT user, host FROM mysql.user WHERE user = 'your_user';
```
1. If necessary, create a user that can connect from the Grafana server:
```sql
CREATE USER 'grafana'@'grafana_server_ip' IDENTIFIED BY 'password';
```
1. If using the `mysql_native_password` authentication plugin, ensure it's enabled on the server.
### Cannot access database
**Error message:** "Access denied for user 'username'@'host' to database 'dbname'"
**Cause:** The authenticated user doesn't have permission to access the specified database.
**Solution:**
1. Verify that the database name is correct in the data source configuration.
1. Ensure the user has the required permissions on the database:
```sql
GRANT SELECT ON your_database.* TO 'grafana'@'grafana_server_ip';
FLUSH PRIVILEGES;
```
1. For production environments, grant permissions only on specific tables:
```sql
GRANT SELECT ON your_database.your_table TO 'grafana'@'grafana_server_ip';
```
### PAM authentication issues
**Error message:** "Authentication plugin 'auth_pam' cannot be loaded" or cleartext password errors
**Cause:** PAM (Pluggable Authentication Modules) authentication requires cleartext password transmission.
**Solution:**
1. Enable **Allow Cleartext Passwords** in the data source configuration if using PAM authentication.
1. Ensure TLS is enabled to protect password transmission when using cleartext passwords.
1. Verify that the PAM plugin is correctly installed and configured on the MySQL server.
## Query errors
These errors occur when there are issues with query syntax or configuration.
### Time column not found or invalid
**Error message:** "Could not find time column" or time series visualization shows no data
**Cause:** The query doesn't return a properly formatted `time` column for time series visualization.
**Solution:**
1. Ensure your query includes a column named `time` when using the **Time series** format.
1. Use the `$__time()` macro to convert your date column: `$__time(your_date_column)`.
1. Verify the time column is of a valid MySQL date/time type (`DATETIME`, `TIMESTAMP`, `DATE`) or contains Unix epoch values.
1. Ensure the result set is sorted by the time column using `ORDER BY`.
### Macro expansion errors
**Error message:** "Error parsing query" or macros appear unexpanded in the query
**Cause:** Grafana macros are being used incorrectly.
**Solution:**
1. Verify macro syntax: use `$__timeFilter(column)` not `$_timeFilter(column)`.
1. Check that the column name passed to macros exists in your table.
1. View the expanded query by clicking **Generated SQL** after running the query to debug macro expansion.
1. Ensure backticks are used for reserved words or special characters in column names: `$__timeFilter(\`time-column\`)`.
### Timezone and time shift issues
**Cause:** Time series data appears shifted or doesn't align with expected times.
**Solution:**
1. Store timestamps in UTC in your database to avoid timezone issues.
1. Time macros (`$__time`, `$__timeFilter`, etc.) always expand to UTC values.
1. Set the **Session Timezone** in the data source configuration to match your data's timezone, or use `+00:00` for UTC.
1. If your timestamps are stored in local time, convert them to UTC in your query:
```sql
SELECT
CONVERT_TZ(your_datetime_column, 'Your/Timezone', 'UTC') AS time,
value
FROM your_table
```
### Query returns too many rows
**Error message:** "Result set too large" or browser becomes unresponsive
**Cause:** The query returns more data than can be efficiently processed.
**Solution:**
1. Add time filters using `$__timeFilter(column)` to limit data to the dashboard time range.
1. Use aggregations (`AVG`, `SUM`, `COUNT`) with `GROUP BY` instead of returning raw rows.
1. Add a `LIMIT` clause to restrict results: `SELECT ... LIMIT 1000`.
1. Use the `$__timeGroup()` macro to aggregate data into time intervals.
### Syntax error in SQL statement
**Error message:** "You have an error in your SQL syntax" followed by specific error details
**Cause:** The SQL query contains invalid syntax.
**Solution:**
1. Check for missing or extra commas, parentheses, or quotes.
1. Ensure reserved words used as identifiers are enclosed in backticks: `` `table` ``, `` `select` ``.
1. Verify that template variable syntax is correct: `$variable` or `${variable}`.
1. Test the query directly in a MySQL client to isolate Grafana-specific issues.
### Unknown column in field list
**Error message:** "Unknown column 'column_name' in 'field list'"
**Cause:** The specified column doesn't exist in the table or is misspelled.
**Solution:**
1. Verify the column name is spelled correctly.
1. Check that the column exists in the specified table.
1. If the column name contains special characters or spaces, enclose it in backticks: `` `column-name` ``.
1. Ensure the correct database is selected if you're referencing columns without the full table path.
## Performance issues
These issues relate to slow queries or high resource usage.
### Slow query execution
**Cause:** Queries take a long time to execute.
**Solution:**
1. Reduce the dashboard time range to limit data volume.
1. Add indexes to columns used in `WHERE` clauses and time filters:
```sql
CREATE INDEX idx_time ON your_table(time_column);
```
1. Use aggregations instead of returning individual rows.
1. Increase the **Min time interval** setting to reduce the number of data points.
1. Review the query execution plan using `EXPLAIN` to identify bottlenecks:
```sql
EXPLAIN SELECT * FROM your_table WHERE time_column > NOW() - INTERVAL 1 HOUR;
```
### Connection pool exhaustion
**Error message:** "Too many connections" or "Connection pool exhausted"
**Cause:** Too many concurrent connections to the database.
**Solution:**
1. Increase the **Max open** connection limit in the data source configuration.
1. Enable **Auto (max idle)** to automatically manage idle connections.
1. Reduce the number of panels querying the same data source simultaneously.
1. Check for long-running queries that might be holding connections.
1. Increase the `max_connections` setting in MySQL if necessary:
```sql
SHOW VARIABLES LIKE 'max_connections';
SET GLOBAL max_connections = 200;
```
### Query timeout
**Error message:** "Query execution was interrupted" or "Lock wait timeout exceeded"
**Cause:** The query takes too long and exceeds the configured timeout.
**Solution:**
1. Optimize the query by adding appropriate indexes.
1. Reduce the amount of data being queried by narrowing the time range.
1. Use aggregations to reduce the result set size.
1. Check for table locks that might be blocking the query.
## Other common issues
The following issues don't produce specific error messages but are commonly encountered.
### Template variable queries fail
**Cause:** Variable queries return unexpected results or errors.
**Solution:**
1. Verify the variable query syntax is valid SQL that returns a single column.
1. Check that the data source connection is working.
1. Ensure the user has permission to access the tables referenced in the variable query.
1. Test the query in the query editor before using it as a variable query.
### Data appears incorrect or misaligned
**Cause:** Data formatting or type conversion issues.
**Solution:**
1. Use explicit column aliases to ensure consistent naming: `SELECT value AS metric`.
1. Verify numeric columns are actually numeric types, not strings.
1. Check for `NULL` values that might affect aggregations.
1. Use the `FILL` option in `$__timeGroup()` macro to handle missing data points.
### Special characters in database or table names
**Cause:** Queries fail when tables or databases contain reserved words or special characters.
**Solution:**
1. Enclose identifiers with special characters in backticks: `` `my-database`.`my-table` ``.
1. The query editor automatically handles this for selections, but manual queries require backticks.
1. Avoid using reserved words as identifiers when possible.
### An unexpected error happened
**Error message:** "An unexpected error happened"
**Cause:** A general error occurred that doesn't have a specific error message.
**Solution:**
1. Check the Grafana server logs for more details about the error.
1. Verify all data source configuration settings are correct.
1. Test the connection using the **Save & test** button.
1. Ensure the MySQL server is accessible and responding to queries.
1. For Grafana Cloud customers, contact support for assistance.
## Get additional help
If you continue to experience issues after following this troubleshooting guide:
1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues.
1. Review the [Grafana GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
1. Enable debug logging in Grafana to capture detailed error information.
1. Check MySQL error logs for additional details.
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
When reporting issues, include:
- Grafana version
- MySQL version
- Error messages (redact sensitive information)
- Steps to reproduce
- Relevant query examples (redact sensitive data)
@@ -2836,9 +2836,11 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise](../
Keys of features to enable, separated by space.
#### `FEATURE_TOGGLE_NAME = false`
#### `FEATURE_NAME = <value>`
Some feature toggles for stable features are on by default. Use this setting to disable an on-by-default feature toggle with the name FEATURE_TOGGLE_NAME, for example, `exploreMixedDatasource = false`.
Use a key-value pair to set feature flag values explicitly, overriding any default values. A few different types are supported, following the OpenFeature specification. See the defaults.ini file for more details.
For example, to disable an on-by-default feature toggle named `exploreMixedDatasource`, specify `exploreMixedDatasource = false`.
<hr>
@@ -1,6 +1,7 @@
import { test, expect } from '@grafana/plugin-e2e';
import { setScopes } from '../utils/scope-helpers';
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
import { testScopes } from '../utils/scopes';
import {
getAdHocFilterOptionValues,
@@ -13,6 +14,7 @@ import {
} from './cuj-selectors';
import { prepareAPIMocks } from './utils';
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
test.use({
@@ -34,6 +36,11 @@ test.describe(
const adHocFilterPills = getAdHocFilterPills(page);
const scopesSelectorInput = getScopesSelectorInput(page);
// Set up routes before any navigation (only for mocked mode)
if (!USE_LIVE_DATA) {
await setupScopeRoutes(page, testScopes());
}
await test.step('1.Apply filtering to a whole dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
@@ -66,6 +66,17 @@ export function getScopesDashboards(page: Page) {
return page.locator('[data-testid^="scopes-dashboards-"][role="treeitem"]');
}
/**
* Clicks the first available dashboard in the scopes dashboard list.
*/
export async function clickFirstScopesDashboard(page: Page) {
const dashboards = getScopesDashboards(page);
// Wait for at least one dashboard to be visible
await expect(dashboards.first()).toBeVisible({ timeout: 10000 });
// Click - Playwright will automatically wait for the element to be actionable
await dashboards.first().click();
}
export function getScopesDashboardsSearchInput(page: Page) {
return page.getByTestId('scopes-dashboards-search');
}
@@ -1,8 +1,10 @@
import { test, expect } from '@grafana/plugin-e2e';
import { setScopes } from '../utils/scope-helpers';
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
import { testScopes } from '../utils/scopes';
import {
clickFirstScopesDashboard,
getAdHocFilterPills,
getGroupByInput,
getGroupByValues,
@@ -21,6 +23,7 @@ test.use({
},
});
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
const DASHBOARD_UNDER_TEST_2 = 'cuj-dashboard-2';
const NAVIGATE_TO = 'cuj-dashboard-3';
@@ -38,6 +41,11 @@ test.describe(
const adhocFilterPills = getAdHocFilterPills(page);
const groupByValues = getGroupByValues(page);
// Set up routes before any navigation (only for mocked mode)
if (!USE_LIVE_DATA) {
await setupScopeRoutes(page, testScopes());
}
await test.step('1.Search dashboard', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
@@ -74,7 +82,7 @@ test.describe(
await expect(markdownContent).toContainText(`now-12h`);
await scopesDashboards.first().click();
await clickFirstScopesDashboard(page);
await page.waitForURL('**/d/**');
await expect(markdownContent).toBeVisible();
@@ -117,10 +125,10 @@ test.describe(
await groupByVariable.press('Enter');
await groupByVariable.press('Escape');
await expect(scopesDashboards.first()).toBeVisible();
const { getRequests, waitForExpectedRequests } = await trackDashboardReloadRequests(page);
await scopesDashboards.first().click();
await clickFirstScopesDashboard(page);
await page.waitForURL('**/d/**');
await waitForExpectedRequests();
await page.waitForLoadState('networkidle');
@@ -158,8 +166,7 @@ test.describe(
const oldFilters = `GroupByVar: ${selectedValues}\n\nAdHocVar: ${processedPills}`;
await expect(markdownContent).toContainText(oldFilters);
await expect(scopesDashboards.first()).toBeVisible();
await scopesDashboards.first().click();
await clickFirstScopesDashboard(page);
await page.waitForURL('**/d/**');
const newPillCount = await adhocFilterPills.count();
@@ -165,9 +165,8 @@ test.describe(
await refreshBtn.click();
await page.waitForLoadState('networkidle');
expect(await panelContent.textContent()).not.toBe(panelContents);
// Wait for the panel content to change (not just for network to complete)
await expect(panelContent).not.toHaveText(panelContents!, { timeout: 10000 });
});
await test.step('6.Turn off refresh', async () => {
@@ -9,6 +9,7 @@ import {
openScopesSelector,
searchScopes,
selectScope,
setupScopeRoutes,
} from '../utils/scope-helpers';
import { testScopes } from '../utils/scopes';
@@ -36,32 +37,37 @@ test.describe(
const scopesSelector = getScopesSelectorInput(page);
const recentScopesSelector = getRecentScopesSelector(page);
const scopeTreeCheckboxes = getScopeTreeCheckboxes(page);
const scopes = testScopes();
// Set up routes once before any navigation (only for mocked mode)
if (!USE_LIVE_DATA) {
await setupScopeRoutes(page, scopes);
}
await test.step('1.View and select any scope', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
const firstLevelScopes = scopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
const secondLevelScopes = firstLevelScopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
const selectedScopes = [secondLevelScopes[0]];
scopeName = await getScopeLeafName(page, 0);
let scopeTitle = await getScopeLeafTitle(page, 0);
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitle);
});
@@ -70,28 +76,27 @@ test.describe(
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
const firstLevelScopes = scopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
const secondLevelScopes = firstLevelScopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const scopeTitles: string[] = [];
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]]; //used only in mocked scopes version
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]];
for (let i = 0; i < selectedScopes.length; i++) {
scopeName = await getScopeLeafName(page, i);
scopeTitles.push(await getScopeLeafTitle(page, i));
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]); //used only in mocked scopes version
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]);
}
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
await expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitles.join(' + '));
});
@@ -102,8 +107,7 @@ test.describe(
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
await recentScopesSelector.click();
@@ -121,26 +125,25 @@ test.describe(
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
let scopeName = await getScopeTreeName(page, 1);
const firstLevelScopes = scopes[2].children!; //used only in mocked scopes version
const firstLevelScopes = scopes[2].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
const secondLevelScopes = firstLevelScopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
const selectedScopes = [secondLevelScopes[0]];
scopeName = await getScopeLeafName(page, 0);
let scopeTitle = await getScopeLeafTitle(page, 0);
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
await applyScopes(page, USE_LIVE_DATA ? undefined : []); //used only in mocked scopes version
await applyScopes(page, USE_LIVE_DATA ? undefined : []);
expect.soft(scopesSelector).toHaveAttribute('data-value', new RegExp(`^${scopeTitle}`));
});
@@ -148,17 +151,16 @@ test.describe(
await test.step('5.View pre-completed production entity values as I type', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
const firstLevelScopes = scopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
const secondLevelScopes = firstLevelScopes[0].children!;
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const scopeSearchOne = await getScopeLeafTitle(page, 0);
@@ -1,6 +1,6 @@
import { test, expect } from '@grafana/plugin-e2e';
import { applyScopes, openScopesSelector, selectScope } from '../utils/scope-helpers';
import { applyScopes, openScopesSelector, selectScope, setupScopeRoutes } from '../utils/scope-helpers';
import { testScopesWithRedirect } from '../utils/scopes';
test.use({
@@ -16,8 +16,13 @@ test.describe('Scope Redirect Functionality', () => {
test('should redirect to custom URL when scope has redirectUrl', async ({ page, gotoDashboardPage }) => {
const scopes = testScopesWithRedirect();
await test.step('Navigate to dashboard and open scopes selector', async () => {
await test.step('Set up routes and navigate to dashboard', async () => {
// Set up routes BEFORE navigation to ensure all requests are mocked
await setupScopeRoutes(page, scopes);
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
});
await test.step('Open scopes selector', async () => {
await openScopesSelector(page, scopes);
});
@@ -40,8 +45,12 @@ test.describe('Scope Redirect Functionality', () => {
test('should prioritize redirectUrl over scope navigation fallback', async ({ page, gotoDashboardPage }) => {
const scopes = testScopesWithRedirect();
await test.step('Navigate to dashboard and open scopes selector', async () => {
await test.step('Set up routes and navigate to dashboard', async () => {
await setupScopeRoutes(page, scopes);
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
});
await test.step('Open scopes selector', async () => {
await openScopesSelector(page, scopes);
});
@@ -68,8 +77,12 @@ test.describe('Scope Redirect Functionality', () => {
}) => {
const scopes = testScopesWithRedirect();
await test.step('Navigate to dashboard and select scope', async () => {
await test.step('Set up routes and navigate to dashboard', async () => {
await setupScopeRoutes(page, scopes);
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
});
await test.step('Select and apply scope', async () => {
await openScopesSelector(page, scopes);
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
await applyScopes(page, [scopes[1]]);
@@ -112,8 +125,12 @@ test.describe('Scope Redirect Functionality', () => {
}) => {
const scopes = testScopesWithRedirect();
await test.step('Navigate to dashboard and select scope', async () => {
await test.step('Set up routes and navigate to dashboard', async () => {
await setupScopeRoutes(page, scopes);
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
});
await test.step('Select and apply scope', async () => {
await openScopesSelector(page, scopes);
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
await applyScopes(page, [scopes[1]]);
@@ -151,9 +168,13 @@ test.describe('Scope Redirect Functionality', () => {
test('should not redirect to redirectPath when on active scope navigation', async ({ page, gotoDashboardPage }) => {
const scopes = testScopesWithRedirect();
await test.step('Set up routes and navigate to dashboard', async () => {
await setupScopeRoutes(page, scopes);
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
});
await test.step('Set up scope navigation to dashboard-1', async () => {
// First, apply a scope that creates scope navigation to dashboard-1 (without redirectPath)
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
await openScopesSelector(page, scopes);
await selectScope(page, 'sn-redirect-setup', scopes[2]);
await applyScopes(page, [scopes[2]]);
+183 -15
View File
@@ -6,7 +6,150 @@ import { Resource } from '../../public/app/features/apiserver/types';
import { testScopes } from './scopes';
const USE_LIVE_DATA = Boolean(process.env.API_CALLS_CONFIG_PATH);
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
/**
* Sets up all scope-related API routes before navigation.
* This ensures that ALL scope API requests (including those made during initial page load)
* are intercepted by the mocks, preventing RTK Query from caching real API responses.
*
* Call this BEFORE navigating to a page (e.g., before gotoDashboardPage).
*/
export async function setupScopeRoutes(page: Page, scopes: TestScope[]): Promise<void> {
// Route for scope node children (tree structure)
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_node_children*`, async (route) => {
const url = new URL(route.request().url());
const parentParam = url.searchParams.get('parent');
const queryParam = url.searchParams.get('query');
// Find the appropriate scopes based on parent
let scopesToReturn = scopes;
if (parentParam) {
// Find nested scopes based on parent name
const findChildren = (items: TestScope[]): TestScope[] => {
for (const item of items) {
if (item.name === parentParam && item.children) {
return item.children;
}
if (item.children) {
const found = findChildren(item.children);
if (found.length > 0) {
return found;
}
}
}
return [];
};
scopesToReturn = findChildren(scopes);
if (scopesToReturn.length === 0) {
scopesToReturn = scopes; // Fallback to root scopes
}
}
// Filter by search query if provided
if (queryParam) {
const query = queryParam.toLowerCase();
const filterByQuery = (items: TestScope[]): TestScope[] => {
const results: TestScope[] = [];
for (const item of items) {
// Exact match on name or title containing the query
if (item.name.toLowerCase() === query || item.title.toLowerCase() === query) {
results.push(item);
} else if (item.name.toLowerCase().includes(query) || item.title.toLowerCase().includes(query)) {
results.push(item);
}
// Also search in children
if (item.children) {
results.push(...filterByQuery(item.children));
}
}
return results;
};
scopesToReturn = filterByQuery(scopesToReturn);
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
apiVersion: 'scope.grafana.app/v0alpha1',
kind: 'FindScopeNodeChildrenResults',
metadata: {},
items: scopesToReturn.map((scope) => ({
kind: 'ScopeNode',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: scope.name,
namespace: 'default',
},
spec: {
title: scope.title,
description: scope.title,
disableMultiSelect: scope.disableMultiSelect ?? false,
nodeType: scope.children ? 'container' : 'leaf',
...(parentParam && { parentName: parentParam }),
...((scope.addLinks || scope.children) && {
linkType: 'scope',
linkId: `scope-${scope.name}`,
}),
...(scope.redirectPath && { redirectPath: scope.redirectPath }),
},
})),
}),
});
});
// Route for individual scope fetching
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/*`, async (route) => {
const url = route.request().url();
const scopeName = url.split('/scopes/')[1]?.split('?')[0];
// Find the scope in the test data
const findScope = (items: TestScope[]): TestScope | undefined => {
for (const item of items) {
if (`scope-${item.name}` === scopeName) {
return item;
}
if (item.children) {
const found = findScope(item.children);
if (found) {
return found;
}
}
}
return undefined;
};
const scope = findScope(scopes);
if (scope) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
kind: 'Scope',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: `scope-${scope.name}`,
namespace: 'default',
},
spec: {
title: scope.title,
description: '',
filters: scope.filters,
category: scope.category,
type: scope.type,
},
}),
});
} else {
await route.fulfill({ status: 404 });
}
});
// Note: Dashboard bindings and navigations routes are set up dynamically in applyScopes()
// with scope-specific URL patterns to avoid cache issues. They are not set up here.
}
export type TestScope = {
name: string;
@@ -24,6 +167,9 @@ export type TestScope = {
type ScopeDashboardBinding = Resource<ScopeDashboardBindingSpec, ScopeDashboardBindingStatus, 'ScopeDashboardBinding'>;
/**
* Sets up a route for scope node children requests and waits for the response.
*/
export async function scopeNodeChildrenRequest(
page: Page,
scopes: TestScope[],
@@ -68,10 +214,13 @@ export async function scopeNodeChildrenRequest(
return page.waitForResponse((response) => response.url().includes(`/find/scope_node_children`));
}
/**
* Opens the scopes selector dropdown and waits for the tree to load.
*/
export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
const click = async () => await page.getByTestId('scopes-selector-input').click();
if (!scopes) {
if (!scopes || USE_LIVE_DATA) {
await click();
return;
}
@@ -82,10 +231,13 @@ export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
await responsePromise;
}
/**
* Expands a scope tree node and waits for children to load.
*/
export async function expandScopesSelection(page: Page, parentScope: string, scopes?: TestScope[]) {
const click = async () => await page.getByTestId(`scopes-tree-${parentScope}-expand`).click();
if (!scopes) {
if (!scopes || USE_LIVE_DATA) {
await click();
return;
}
@@ -96,6 +248,9 @@ export async function expandScopesSelection(page: Page, parentScope: string, sco
await responsePromise;
}
/**
* Sets up a route for individual scope requests and waits for the response.
*/
export async function scopeSelectRequest(page: Page, selectedScope: TestScope): Promise<Response> {
await page.route(
`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/scope-${selectedScope.name}`,
@@ -125,6 +280,9 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope):
return page.waitForResponse((response) => response.url().includes(`/scopes/scope-${selectedScope.name}`));
}
/**
* Selects a scope in the tree.
*/
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
const click = async () => {
const element = page.locator(
@@ -134,7 +292,7 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
await element.click({ force: true });
};
if (!selectedScope) {
if (!selectedScope || USE_LIVE_DATA) {
await click();
return;
}
@@ -145,14 +303,22 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
await responsePromise;
}
/**
* Applies the selected scopes and waits for the selector to close and page to settle.
* Sets up routes dynamically with scope-specific URL patterns to avoid cache issues.
*/
export async function applyScopes(page: Page, scopes?: TestScope[]) {
const click = async () => {
await page.getByTestId('scopes-selector-apply').scrollIntoViewIfNeeded();
await page.getByTestId('scopes-selector-apply').click({ force: true });
};
if (!scopes) {
if (!scopes || USE_LIVE_DATA) {
await click();
// Wait for the apply button to disappear (selector closed)
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
// Wait for any resulting API calls (dashboard bindings, etc.) to complete
await page.waitForLoadState('networkidle');
return;
}
@@ -166,7 +332,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', ''];
// Mock scope_dashboard_bindings endpoint
// Mock scope_dashboard_bindings endpoint with scope-specific URL pattern
await page.route(dashboardBindingsUrl, async (route) => {
await route.fulfill({
status: 200,
@@ -220,7 +386,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
});
});
// Mock scope_navigations endpoint
// Mock scope_navigations endpoint with scope-specific URL pattern
await page.route(scopeNavigationsUrl, async (route) => {
await route.fulfill({
status: 200,
@@ -266,21 +432,23 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
(response) =>
response.url().includes(`/find/scope_dashboard_bindings`) || response.url().includes(`/find/scope_navigations`)
);
const scopeRequestPromises: Array<Promise<Response>> = [];
for (const scope of scopes) {
scopeRequestPromises.push(scopeSelectRequest(page, scope));
}
await click();
await responsePromise;
await Promise.all(scopeRequestPromises);
// Wait for the apply button to disappear (selector closed)
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
// Wait for any resulting API calls to complete
await page.waitForLoadState('networkidle');
}
export async function searchScopes(page: Page, value: string, resultScopes: TestScope[]) {
/**
* Searches for scopes in the tree and waits for results.
* Sets up a route dynamically with filtered results to return only matching scopes.
*/
export async function searchScopes(page: Page, value: string, resultScopes?: TestScope[]) {
const click = async () => await page.getByTestId('scopes-tree-search').fill(value);
if (!resultScopes) {
if (!resultScopes || USE_LIVE_DATA) {
await click();
return;
}
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = false
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-1
View File
@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true
-5
View File
@@ -1156,11 +1156,6 @@
"count": 2
}
},
"public/app/core/config.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"public/app/core/navigation/types.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
+15 -8
View File
@@ -32,14 +32,14 @@ require (
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect; @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect; @grafana/grafana-operator-experience-squad
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
@@ -82,14 +82,14 @@ require (
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
github.com/golang/snappy v1.0.0 // @grafana/alerting-backend
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
github.com/google/go-github/v70 v70.0.0 // indirect; @grafana/grafana-git-ui-sync-team
github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-git-ui-sync-team
github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent
github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group
github.com/google/wire v0.7.0 // @grafana/grafana-backend-group
github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
@@ -113,6 +113,7 @@ require (
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f // @grafana/observability-traces-and-profiling
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // @grafana/grafana-backend-group
@@ -260,12 +261,13 @@ require (
github.com/grafana/grafana/pkg/aggregator v0.0.0 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/apiserver v0.0.0 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/plugins v0.0.0 // @grafana/plugins-platform-backend
// This needs to be here for other projects that import grafana/grafana
// For local development grafana/grafana will always use the local files
// Check go.work file for details
github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/semconv v0.0.0 // @grafana/grafana-app-platform-squad
)
// Replace the workspace versions
@@ -294,6 +296,8 @@ replace (
github.com/grafana/grafana/pkg/aggregator => ./pkg/aggregator
github.com/grafana/grafana/pkg/apimachinery => ./pkg/apimachinery
github.com/grafana/grafana/pkg/apiserver => ./pkg/apiserver
github.com/grafana/grafana/pkg/plugins => ./pkg/plugins
github.com/grafana/grafana/pkg/semconv => ./pkg/semconv
)
require (
@@ -652,11 +656,12 @@ require (
sigs.k8s.io/yaml v1.6.0 // indirect
)
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
@@ -676,6 +681,8 @@ require (
github.com/google/gnostic v0.7.1 // indirect
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
github.com/grafana/sqlds/v5 v5.0.3 // indirect
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/go-archive v0.1.0 // indirect
@@ -697,7 +704,7 @@ require (
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
// Use our fork of the upstream Alertmanager.
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
+17 -6
View File
@@ -680,6 +680,7 @@ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7Og
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@@ -737,6 +738,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -759,6 +762,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -1026,6 +1031,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -1620,8 +1627,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
@@ -1664,8 +1671,6 @@ github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDP
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
@@ -1676,8 +1681,8 @@ github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ
github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
@@ -1753,6 +1758,8 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@@ -1877,6 +1884,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+1 -1
View File
@@ -38,6 +38,6 @@ use (
./pkg/semconv
)
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
+5 -1
View File
@@ -280,7 +280,6 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
@@ -906,6 +905,8 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3 h1:KVncUdAc5YwY/OQmw6HgzJmbRKn6IwrhvtcBAd1yDHo=
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3/go.mod h1:Oy4MthJqfErlieO14ryZXdukDrUACy8Lg56P3zP7S1k=
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
@@ -996,6 +997,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
@@ -1911,6 +1914,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
+2 -4
View File
@@ -62,8 +62,7 @@
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
"storybook": "yarn workspace @grafana/ui storybook --ci",
"storybook:build": "yarn workspace @grafana/ui storybook:build",
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
"themes-generate": "yarn workspace @grafana/data themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
@@ -254,7 +253,6 @@
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"typescript": "5.9.2",
"typescript-json-schema": "^0.65.1",
"webpack": "5.101.0",
"webpack-assets-manifest": "^5.1.0",
"webpack-cli": "6.0.1",
@@ -265,7 +263,7 @@
"webpackbar": "^7.0.0",
"yaml": "^2.0.0",
"yargs": "^18.0.0",
"zod": "^4.0.0"
"zod": "^4.3.0"
},
"dependencies": {
"@bsull/augurs": "^0.10.0",
@@ -34,6 +34,8 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
getBackendSrv().fetch({
...requestOptions,
url: baseURL + requestOptions.url,
// Default to GET so backend_srv correctly skips success alerts for queries
method: requestOptions.method ?? 'GET',
showErrorAlert: requestOptions.showErrorAlert ?? false,
data: requestOptions.body,
headers,
+7 -3
View File
@@ -47,11 +47,12 @@
"LICENSE_APACHE2"
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"build": "yarn themes-schema && tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
"postpack": "mv package.json.bak package.json",
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",
@@ -81,10 +82,12 @@
"tinycolor2": "1.6.0",
"tslib": "2.8.1",
"uplot": "1.6.32",
"xss": "^1.0.14"
"xss": "^1.0.14",
"zod": "^4.3.0"
},
"devDependencies": {
"@grafana/scenes": "6.38.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.1",
"@testing-library/react": "16.3.0",
"@types/history": "4.7.11",
@@ -101,6 +104,7 @@
"rollup": "^4.22.4",
"rollup-plugin-esbuild": "6.2.1",
"rollup-plugin-node-externals": "^8.0.0",
"tsx": "^4.21.0",
"typescript": "5.9.2"
},
"peerDependencies": {
+3 -2
View File
@@ -1,3 +1,4 @@
import json from '@rollup/plugin-json';
import { createRequire } from 'node:module';
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
@@ -8,13 +9,13 @@ const pkg = rq('./package.json');
export default [
{
input: entryPoint,
plugins,
plugins: [...plugins, json()],
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},
{
input: 'src/unstable.ts',
plugins,
plugins: [...plugins, json()],
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
treeshake: false,
},
@@ -106,3 +106,4 @@ export { findNumericFieldMinMax } from '../field/fieldOverrides';
export { type PanelOptionsSupplier } from '../panel/PanelPlugin';
export { sanitize, sanitizeUrl } from '../text/sanitize';
export { type NestedValueAccess, type NestedPanelOptions, isNestedPanelOptions } from '../utils/OptionsUIBuilders';
export { NewThemeOptionsSchema } from '../themes/createTheme';
@@ -1,83 +1,103 @@
import { merge } from 'lodash';
import { z } from 'zod';
import { alpha, darken, emphasize, getContrastRatio, lighten } from './colorManipulator';
import { palette } from './palette';
import { DeepPartial, ThemeRichColor } from './types';
import { DeepRequired, ThemeRichColor, ThemeRichColorInputSchema } from './types';
const ThemeColorsModeSchema = z.enum(['light', 'dark']);
/** @internal */
export type ThemeColorsMode = 'light' | 'dark';
export type ThemeColorsMode = z.infer<typeof ThemeColorsModeSchema>;
const createThemeColorsBaseSchema = <TColor>(color: TColor) =>
z
.object({
mode: ThemeColorsModeSchema,
primary: color,
secondary: color,
info: color,
error: color,
success: color,
warning: color,
text: z.object({
primary: z.string().optional(),
secondary: z.string().optional(),
disabled: z.string().optional(),
link: z.string().optional(),
/** Used for auto white or dark text on colored backgrounds */
maxContrast: z.string().optional(),
}),
background: z.object({
/** Dashboard and body background */
canvas: z.string().optional(),
/** Primary content pane background (panels etc) */
primary: z.string().optional(),
/** Cards and elements that need to stand out on the primary background */
secondary: z.string().optional(),
/**
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
* themes it has a brighter shade to help give it contrast against the primary background.
**/
elevated: z.string().optional(),
}),
border: z.object({
weak: z.string().optional(),
medium: z.string().optional(),
strong: z.string().optional(),
}),
gradients: z.object({
brandVertical: z.string().optional(),
brandHorizontal: z.string().optional(),
}),
action: z.object({
/** Used for selected menu item / select option */
selected: z.string().optional(),
/**
* @alpha (Do not use from plugins)
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
**/
selectedBorder: z.string().optional(),
/** Used for hovered menu item / select option */
hover: z.string().optional(),
/** Used for button/colored background hover opacity */
hoverOpacity: z.number().optional(),
/** Used focused menu item / select option */
focus: z.string().optional(),
/** Used for disabled buttons and inputs */
disabledBackground: z.string().optional(),
/** Disabled text */
disabledText: z.string().optional(),
/** Disablerd opacity */
disabledOpacity: z.number().optional(),
}),
hoverFactor: z.number(),
contrastThreshold: z.number(),
tonalOffset: z.number(),
})
.partial();
// Need to override the zod type to include the generic properly
/** @internal */
export interface ThemeColorsBase<TColor> {
mode: ThemeColorsMode;
export type ThemeColorsBase<TColor> = DeepRequired<
Omit<
z.infer<ReturnType<typeof createThemeColorsBaseSchema>>,
'primary' | 'secondary' | 'info' | 'error' | 'success' | 'warning'
>
> & {
primary: TColor;
secondary: TColor;
info: TColor;
error: TColor;
success: TColor;
warning: TColor;
text: {
primary: string;
secondary: string;
disabled: string;
link: string;
/** Used for auto white or dark text on colored backgrounds */
maxContrast: string;
};
background: {
/** Dashboard and body background */
canvas: string;
/** Primary content pane background (panels etc) */
primary: string;
/** Cards and elements that need to stand out on the primary background */
secondary: string;
/**
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
* themes it has a brighter shade to help give it contrast against the primary background.
**/
elevated: string;
};
border: {
weak: string;
medium: string;
strong: string;
};
gradients: {
brandVertical: string;
brandHorizontal: string;
};
action: {
/** Used for selected menu item / select option */
selected: string;
/**
* @alpha (Do not use from plugins)
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
**/
selectedBorder: string;
/** Used for hovered menu item / select option */
hover: string;
/** Used for button/colored background hover opacity */
hoverOpacity: number;
/** Used focused menu item / select option */
focus: string;
/** Used for disabled buttons and inputs */
disabledBackground: string;
/** Disabled text */
disabledText: string;
/** Disablerd opacity */
disabledOpacity: number;
};
hoverFactor: number;
contrastThreshold: number;
tonalOffset: number;
}
};
export interface ThemeHoverStrengh {}
@@ -89,8 +109,10 @@ export interface ThemeColors extends ThemeColorsBase<ThemeRichColor> {
emphasize(color: string, amount?: number): string;
}
export const ThemeColorsInputSchema = createThemeColorsBaseSchema(ThemeRichColorInputSchema);
/** @internal */
export type ThemeColorsInput = DeepPartial<ThemeColorsBase<ThemeRichColor>>;
export type ThemeColorsInput = z.infer<typeof ThemeColorsInputSchema>;
class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
mode: ThemeColorsMode = 'dark';
@@ -1,3 +1,5 @@
import { z } from 'zod';
/** @beta */
export interface ThemeShape {
/**
@@ -34,9 +36,12 @@ export interface Radii {
}
/** @internal */
export interface ThemeShapeInput {
borderRadius?: number;
}
export const ThemeShapeInputSchema = z.object({
borderRadius: z.int().nonnegative().optional(),
});
/** @internal */
export type ThemeShapeInput = z.infer<typeof ThemeShapeInputSchema>;
export function createShape(options: ThemeShapeInput): ThemeShape {
const baseBorderRadius = options.borderRadius ?? 6;
@@ -1,11 +1,15 @@
// Code based on Material UI
// The MIT License (MIT)
// Copyright (c) 2014 Call-Em-All
import { z } from 'zod';
/** @internal */
export type ThemeSpacingOptions = {
gridSize?: number;
};
export const ThemeSpacingOptionsSchema = z.object({
gridSize: z.int().positive().optional(),
});
/** @internal */
export type ThemeSpacingOptions = z.infer<typeof ThemeSpacingOptionsSchema>;
/** @internal */
export type ThemeSpacingArgument = number | string;
+24 -15
View File
@@ -1,28 +1,37 @@
import * as z from 'zod';
import { createBreakpoints } from './breakpoints';
import { createColors, ThemeColorsInput } from './createColors';
import { createColors, ThemeColorsInputSchema } from './createColors';
import { createComponents } from './createComponents';
import { createShadows } from './createShadows';
import { createShape, ThemeShapeInput } from './createShape';
import { createSpacing, ThemeSpacingOptions } from './createSpacing';
import { createShape, ThemeShapeInputSchema } from './createShape';
import { createSpacing, ThemeSpacingOptionsSchema } from './createSpacing';
import { createTransitions } from './createTransitions';
import { createTypography, ThemeTypographyInput } from './createTypography';
import { createTypography, ThemeTypographyInputSchema } from './createTypography';
import { createV1Theme } from './createV1Theme';
import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors';
import { createVisualizationColors, ThemeVisualizationColorsInputSchema } from './createVisualizationColors';
import { GrafanaTheme2 } from './types';
import { zIndex } from './zIndex';
/** @internal */
export interface NewThemeOptions {
name?: string;
colors?: ThemeColorsInput;
spacing?: ThemeSpacingOptions;
shape?: ThemeShapeInput;
typography?: ThemeTypographyInput;
visualization?: ThemeVisualizationColorsInput;
}
export const NewThemeOptionsSchema = z.object({
name: z.string(),
id: z.string(),
colors: ThemeColorsInputSchema.optional(),
spacing: ThemeSpacingOptionsSchema.optional(),
shape: ThemeShapeInputSchema.optional(),
typography: ThemeTypographyInputSchema.optional(),
visualization: ThemeVisualizationColorsInputSchema.optional(),
});
/** @internal */
export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
export type NewThemeOptions = z.infer<typeof NewThemeOptionsSchema>;
/** @internal */
export function createTheme(
options: Omit<NewThemeOptions, 'id' | 'name'> & {
name?: NewThemeOptions['name'];
} = {}
): GrafanaTheme2 {
const {
name,
colors: colorsInput = {},
@@ -1,6 +1,7 @@
// Code based on Material UI
// The MIT License (MIT)
// Copyright (c) 2014 Call-Em-All
import { z } from 'zod';
import { ThemeColors } from './createColors';
@@ -40,18 +41,20 @@ export interface ThemeTypographyVariant {
letterSpacing?: string;
}
export interface ThemeTypographyInput {
fontFamily?: string;
fontFamilyMonospace?: string;
fontSize?: number;
fontWeightLight?: number;
fontWeightRegular?: number;
fontWeightMedium?: number;
fontWeightBold?: number;
// hat's the font-size on the html element.
export const ThemeTypographyInputSchema = z.object({
fontFamily: z.string().optional(),
fontFamilyMonospace: z.string().optional(),
fontSize: z.number().positive().optional(),
fontWeightLight: z.number().positive().optional(),
fontWeightRegular: z.number().positive().optional(),
fontWeightMedium: z.number().positive().optional(),
fontWeightBold: z.number().positive().optional(),
// what's the font-size on the html element.
// 16px is the default font-size used by browsers.
htmlFontSize?: number;
}
htmlFontSize: z.number().positive().optional(),
});
export type ThemeTypographyInput = z.infer<typeof ThemeTypographyInputSchema>;
const defaultFontFamily = "'Inter', 'Helvetica', 'Arial', sans-serif";
const defaultFontFamilyMonospace = "'Roboto Mono', monospace";
@@ -1,3 +1,5 @@
import { z } from 'zod';
import { FALLBACK_COLOR } from '../types/fieldColor';
import { ThemeColors } from './createColors';
@@ -26,29 +28,44 @@ export interface ThemeVizColor<T extends ThemeVizColorName> {
type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';
type ThemeVizColorShadeName<T extends ThemeVizColorName> =
| `super-light-${T}`
| `light-${T}`
| T
| `semi-dark-${T}`
| `dark-${T}`;
const createShadeSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
z.enum([`super-light-${color}`, `light-${color}`, color, `semi-dark-${color}`, `dark-${color}`]);
type ThemeVizHueGeneric<T> = T extends ThemeVizColorName
? {
name: T;
shades: Array<ThemeVizColor<T>>;
}
: never;
type ThemeVizColorShadeName<T extends ThemeVizColorName> = z.infer<ReturnType<typeof createShadeSchema<T>>>;
const createHueSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
z.object({
name: z.literal(color),
shades: z.array(
z.object({
color: z.string(),
name: createShadeSchema(color),
aliases: z.array(z.string()).optional(),
primary: z.boolean().optional(),
})
),
});
const ThemeVizHueSchema = z.union([
createHueSchema('red'),
createHueSchema('orange'),
createHueSchema('yellow'),
createHueSchema('green'),
createHueSchema('blue'),
createHueSchema('purple'),
]);
/**
* @alpha
*/
export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
export type ThemeVizHue = z.infer<typeof ThemeVizHueSchema>;
export type ThemeVisualizationColorsInput = {
hues?: ThemeVizHue[];
palette?: string[];
};
export const ThemeVisualizationColorsInputSchema = z.object({
hues: z.array(ThemeVizHueSchema).optional(),
palette: z.array(z.string()).optional(),
});
export type ThemeVisualizationColorsInput = z.infer<typeof ThemeVisualizationColorsInputSchema>;
/**
* @internal
+14 -11
View File
@@ -1,6 +1,6 @@
import { Registry, RegistryItem } from '../utils/Registry';
import { createTheme } from './createTheme';
import { createTheme, NewThemeOptionsSchema } from './createTheme';
import * as extraThemes from './themeDefinitions';
import { GrafanaTheme2 } from './types';
@@ -42,9 +42,6 @@ export function getBuiltInThemes(allowedExtras: string[]) {
return sortedThemes;
}
/**
* There is also a backend list at pkg/services/preference/themes.go
*/
const themeRegistry = new Registry<ThemeRegistryItem>(() => {
return [
{ id: 'system', name: 'System preference', build: getSystemPreferenceTheme },
@@ -53,13 +50,19 @@ const themeRegistry = new Registry<ThemeRegistryItem>(() => {
];
});
for (const [id, theme] of Object.entries(extraThemes)) {
themeRegistry.register({
id,
name: theme.name ?? '',
build: () => createTheme(theme),
isExtra: true,
});
for (const [name, json] of Object.entries(extraThemes)) {
const result = NewThemeOptionsSchema.safeParse(json);
if (!result.success) {
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
} else {
const theme = result.data;
themeRegistry.register({
id: theme.id,
name: theme.name,
build: () => createTheme(theme),
isExtra: true,
});
}
}
function getSystemPreferenceTheme() {
@@ -0,0 +1,608 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"colors": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["light", "dark"]
},
"primary": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"secondary": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"info": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"error": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"success": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"warning": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"main": {
"type": "string"
},
"shade": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
},
"transparent": {
"type": "string"
},
"borderTransparent": {
"type": "string"
},
"contrastText": {
"type": "string"
}
},
"additionalProperties": false
},
"text": {
"type": "object",
"properties": {
"primary": {
"type": "string"
},
"secondary": {
"type": "string"
},
"disabled": {
"type": "string"
},
"link": {
"type": "string"
},
"maxContrast": {
"type": "string"
}
},
"additionalProperties": false
},
"background": {
"type": "object",
"properties": {
"canvas": {
"type": "string"
},
"primary": {
"type": "string"
},
"secondary": {
"type": "string"
},
"elevated": {
"type": "string"
}
},
"additionalProperties": false
},
"border": {
"type": "object",
"properties": {
"weak": {
"type": "string"
},
"medium": {
"type": "string"
},
"strong": {
"type": "string"
}
},
"additionalProperties": false
},
"gradients": {
"type": "object",
"properties": {
"brandVertical": {
"type": "string"
},
"brandHorizontal": {
"type": "string"
}
},
"additionalProperties": false
},
"action": {
"type": "object",
"properties": {
"selected": {
"type": "string"
},
"selectedBorder": {
"type": "string"
},
"hover": {
"type": "string"
},
"hoverOpacity": {
"type": "number"
},
"focus": {
"type": "string"
},
"disabledBackground": {
"type": "string"
},
"disabledText": {
"type": "string"
},
"disabledOpacity": {
"type": "number"
}
},
"additionalProperties": false
},
"hoverFactor": {
"type": "number"
},
"contrastThreshold": {
"type": "number"
},
"tonalOffset": {
"type": "number"
}
},
"additionalProperties": false
},
"spacing": {
"type": "object",
"properties": {
"gridSize": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"additionalProperties": false
},
"shape": {
"type": "object",
"properties": {
"borderRadius": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"additionalProperties": false
},
"typography": {
"type": "object",
"properties": {
"fontFamily": {
"type": "string"
},
"fontFamilyMonospace": {
"type": "string"
},
"fontSize": {
"type": "number",
"exclusiveMinimum": 0
},
"fontWeightLight": {
"type": "number",
"exclusiveMinimum": 0
},
"fontWeightRegular": {
"type": "number",
"exclusiveMinimum": 0
},
"fontWeightMedium": {
"type": "number",
"exclusiveMinimum": 0
},
"fontWeightBold": {
"type": "number",
"exclusiveMinimum": 0
},
"htmlFontSize": {
"type": "number",
"exclusiveMinimum": 0
}
},
"additionalProperties": false
},
"visualization": {
"type": "object",
"properties": {
"hues": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "red"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-red", "light-red", "red", "semi-dark-red", "dark-red"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "orange"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-orange", "light-orange", "orange", "semi-dark-orange", "dark-orange"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "yellow"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-yellow", "light-yellow", "yellow", "semi-dark-yellow", "dark-yellow"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "green"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-green", "light-green", "green", "semi-dark-green", "dark-green"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "blue"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-blue", "light-blue", "blue", "semi-dark-blue", "dark-blue"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "purple"
},
"shades": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"name": {
"type": "string",
"enum": ["super-light-purple", "light-purple", "purple", "semi-dark-purple", "dark-purple"]
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"primary": {
"type": "boolean"
}
},
"required": ["color", "name"],
"additionalProperties": false
}
}
},
"required": ["name", "shades"],
"additionalProperties": false
}
]
}
},
"palette": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
},
"required": ["name", "id"],
"additionalProperties": false
}
@@ -0,0 +1,19 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { NewThemeOptionsSchema } from '../createTheme';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
fs.writeFileSync(
path.join(__dirname, '../schema.generated.json'),
JSON.stringify(
NewThemeOptionsSchema.toJSONSchema({
target: 'draft-07',
}),
undefined,
2
)
);
@@ -0,0 +1,50 @@
{
"name": "Aubergine",
"id": "aubergine",
"colors": {
"mode": "dark",
"border": {
"weak": "#4F2A3D",
"medium": "#6A3C4B",
"strong": "#8C5A69"
},
"text": {
"primary": "#E5D0D6",
"secondary": "#D1A8C4",
"disabled": "#B7A0A6",
"link": "#A56BB6",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#8C5A69"
},
"secondary": {
"main": "#6A3C4B",
"text": "#D1A8C4",
"border": "#8C5A69"
},
"background": {
"canvas": "#2E1F2D",
"primary": "#3C2136",
"secondary": "#4A2D47",
"elevated": "#4A2D47"
},
"action": {
"hover": "#6A3C4B",
"selected": "#8C5A69",
"selectedBorder": "#FFB300",
"focus": "#A56BB6",
"hoverOpacity": 0.1,
"disabledText": "#B7A0A6",
"disabledBackground": "#4A2D47",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)",
"brandVertical": "linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)"
},
"contrastThreshold": 4,
"hoverFactor": 0.07,
"tonalOffset": 0.15
}
}
@@ -1,53 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const aubergineTheme: NewThemeOptions = {
name: 'Aubergine',
colors: {
mode: 'dark',
border: {
weak: '#4F2A3D',
medium: '#6A3C4B',
strong: '#8C5A69',
},
text: {
primary: '#E5D0D6',
secondary: '#D1A8C4',
disabled: '#B7A0A6',
link: '#A56BB6',
maxContrast: '#FFFFFF',
},
primary: {
main: '#8C5A69',
},
secondary: {
main: '#6A3C4B',
text: '#D1A8C4',
border: '#8C5A69',
},
background: {
canvas: '#2E1F2D',
primary: '#3C2136',
secondary: '#4A2D47',
elevated: '#4A2D47',
},
action: {
hover: '#6A3C4B',
selected: '#8C5A69',
selectedBorder: '#FFB300',
focus: '#A56BB6',
hoverOpacity: 0.1,
disabledText: '#B7A0A6',
disabledBackground: '#4A2D47',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)',
brandVertical: 'linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)',
},
contrastThreshold: 4,
hoverFactor: 0.07,
tonalOffset: 0.15,
},
};
export default aubergineTheme;
@@ -0,0 +1,60 @@
{
"name": "Debug",
"id": "debug",
"colors": {
"mode": "dark",
"background": {
"canvas": "#000033",
"primary": "#000044",
"secondary": "#000055",
"elevated": "#000055"
},
"text": {
"primary": "#bbbb00",
"secondary": "#888800",
"disabled": "#444400",
"link": "#dddd00",
"maxContrast": "#ffff00"
},
"border": {
"weak": "#ff000044",
"medium": "#ff000088",
"strong": "#ff0000ff"
},
"primary": {
"border": "#ff000088",
"text": "#cccc00",
"contrastText": "#ffff00",
"shade": "#9900dd"
},
"secondary": {
"border": "#ff000088",
"text": "#cccc00",
"contrastText": "#ffff00",
"shade": "#9900dd"
},
"info": {
"shade": "#9900dd"
},
"warning": {
"shade": "#9900dd"
},
"success": {
"shade": "#9900dd"
},
"error": {
"shade": "#9900dd"
},
"action": {
"hover": "#9900dd",
"focus": "#6600aa",
"selected": "#440088"
}
},
"shape": {
"borderRadius": 8
},
"spacing": {
"gridSize": 10
}
}
@@ -1,71 +0,0 @@
import { NewThemeOptions } from '../createTheme';
/**
* a very ugly theme that is useful for debugging and checking if the theme is applied correctly
* borders are red,
* backgrounds are blue,
* text is yellow,
* and grafana loves you <3
* (also corners are rounded, action states (hover, focus, selected) are purple)
*/
const debugTheme: NewThemeOptions = {
name: 'Debug',
colors: {
mode: 'dark',
background: {
canvas: '#000033',
primary: '#000044',
secondary: '#000055',
elevated: '#000055',
},
text: {
primary: '#bbbb00',
secondary: '#888800',
disabled: '#444400',
link: '#dddd00',
maxContrast: '#ffff00',
},
border: {
weak: '#ff000044',
medium: '#ff000088',
strong: '#ff0000ff',
},
primary: {
border: '#ff000088',
text: '#cccc00',
contrastText: '#ffff00',
shade: '#9900dd',
},
secondary: {
border: '#ff000088',
text: '#cccc00',
contrastText: '#ffff00',
shade: '#9900dd',
},
info: {
shade: '#9900dd',
},
warning: {
shade: '#9900dd',
},
success: {
shade: '#9900dd',
},
error: {
shade: '#9900dd',
},
action: {
hover: '#9900dd',
focus: '#6600aa',
selected: '#440088',
},
},
shape: {
borderRadius: 8,
},
spacing: {
gridSize: 10,
},
};
export default debugTheme;
@@ -0,0 +1,71 @@
{
"name": "Desert bloom",
"id": "desertbloom",
"colors": {
"mode": "light",
"border": {
"weak": "rgba(0, 0, 0, 0.12)",
"medium": "rgba(0, 0, 0, 0.20)",
"strong": "rgba(0, 0, 0, 0.30)"
},
"text": {
"primary": "#333333",
"secondary": "#555555",
"disabled": "rgba(0, 0, 0, 0.5)",
"link": "#1A82E2",
"maxContrast": "#000000"
},
"primary": {
"main": "#FF6F61",
"text": "#FE6F61",
"border": "#E55B4D",
"name": "primary",
"shade": "#E55B4D",
"transparent": "#FF6F6126",
"contrastText": "#FFFFFF",
"borderTransparent": "#FF6F6140"
},
"secondary": {
"main": "#FFFFFF",
"text": "#695f53",
"border": "#d9cec0",
"name": "secondary",
"shade": "#d9cec0",
"transparent": "#FFFFFF26",
"contrastText": "#4c4339",
"borderTransparent": "#FFFFFF40"
},
"info": {
"main": "#1A82E2"
},
"success": {
"main": "#4CAF50"
},
"warning": {
"main": "#FFC107"
},
"background": {
"canvas": "#FFF8F0",
"primary": "#FFFFFF",
"secondary": "#f9f3e8",
"elevated": "#FFFFFF"
},
"action": {
"hover": "rgba(168, 156, 134, 0.12)",
"selected": "rgba(168, 156, 134, 0.36)",
"selectedBorder": "#FF6F61",
"focus": "rgba(168, 156, 134, 0.50)",
"hoverOpacity": 0.08,
"disabledText": "rgba(168, 156, 134, 0.5)",
"disabledBackground": "rgba(168, 156, 134, 0.06)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)",
"brandVertical": "linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.15
}
}
@@ -1,75 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const desertBloomTheme: NewThemeOptions = {
name: 'Desert bloom',
colors: {
mode: 'light',
border: {
weak: 'rgba(0, 0, 0, 0.12)',
medium: 'rgba(0, 0, 0, 0.20)',
strong: 'rgba(0, 0, 0, 0.30)',
},
text: {
primary: '#333333',
secondary: '#555555',
disabled: 'rgba(0, 0, 0, 0.5)',
link: '#1A82E2',
maxContrast: '#000000',
},
primary: {
main: '#FF6F61',
text: '#FE6F61',
border: '#E55B4D',
name: 'primary',
shade: '#E55B4D',
transparent: '#FF6F6126',
contrastText: '#FFFFFF',
borderTransparent: '#FF6F6140',
},
secondary: {
main: '#FFFFFF',
text: '#695f53',
border: '#d9cec0',
name: 'secondary',
shade: '#d9cec0',
transparent: '#FFFFFF26',
contrastText: '#4c4339',
borderTransparent: '#FFFFFF40',
},
info: {
main: '#1A82E2',
},
success: {
main: '#4CAF50',
},
warning: {
main: '#FFC107',
},
background: {
canvas: '#FFF8F0',
primary: '#FFFFFF',
secondary: '#f9f3e8',
elevated: '#FFFFFF',
},
action: {
hover: 'rgba(168, 156, 134, 0.12)',
selected: 'rgba(168, 156, 134, 0.36)',
selectedBorder: '#FF6F61',
focus: 'rgba(168, 156, 134, 0.50)',
hoverOpacity: 0.08,
disabledText: 'rgba(168, 156, 134, 0.5)',
disabledBackground: 'rgba(168, 156, 134, 0.06)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
brandVertical: 'linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
},
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.15,
},
};
export default desertBloomTheme;
@@ -0,0 +1,62 @@
{
"name": "Gilded grove",
"id": "gildedgrove",
"colors": {
"mode": "dark",
"border": {
"weak": "rgba(200, 200, 180, 0.12)",
"medium": "rgba(200, 200, 180, 0.20)",
"strong": "rgba(200, 200, 180, 0.30)"
},
"text": {
"primary": "rgb(250, 250, 239)",
"secondary": "rgba(200, 200, 180, 0.85)",
"disabled": "rgba(200, 200, 180, 0.6)",
"link": "#FEAC34",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#FEAC34",
"text": "#FFD783",
"border": "#FFD783",
"name": "primary",
"shade": "rgb(255, 173, 80)",
"transparent": "#FEAC3426",
"contrastText": "#111614",
"borderTransparent": "#FFD78340"
},
"secondary": {
"main": "rgba(200, 200, 180, 0.10)",
"shade": "rgba(200, 200, 180, 0.14)",
"transparent": "rgba(200, 200, 180, 0.08)",
"text": "rgb(200, 200, 180)",
"contrastText": "rgb(200, 200, 180)",
"border": "rgba(200, 200, 180, 0.08)",
"name": "secondary",
"borderTransparent": "rgba(200, 200, 180, 0.25)"
},
"background": {
"canvas": "#111614",
"primary": "#1d2220",
"secondary": "#27312E",
"elevated": "#27312E"
},
"action": {
"hover": "rgba(200, 200, 180, 0.16)",
"selected": "rgba(200, 200, 180, 0.12)",
"selectedBorder": "#FEAC34",
"focus": "rgba(200, 200, 180, 0.16)",
"hoverOpacity": 0.08,
"disabledText": "rgba(200, 200, 180, 0.6)",
"disabledBackground": "rgba(200, 200, 180, 0.04)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)",
"brandVertical": "linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.15
}
}
@@ -1,65 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const gildedGroveTheme: NewThemeOptions = {
name: 'Gilded grove',
colors: {
mode: 'dark',
border: {
weak: 'rgba(200, 200, 180, 0.12)',
medium: 'rgba(200, 200, 180, 0.20)',
strong: 'rgba(200, 200, 180, 0.30)',
},
text: {
primary: 'rgb(250, 250, 239)',
secondary: 'rgba(200, 200, 180, 0.85)',
disabled: 'rgba(200, 200, 180, 0.6)',
link: '#FEAC34',
maxContrast: '#FFFFFF',
},
primary: {
main: '#FEAC34',
text: '#FFD783',
border: '#FFD783',
name: 'primary',
shade: 'rgb(255, 173, 80)',
transparent: '#FEAC3426',
contrastText: '#111614',
borderTransparent: '#FFD78340',
},
secondary: {
main: 'rgba(200, 200, 180, 0.10)',
shade: 'rgba(200, 200, 180, 0.14)',
transparent: 'rgba(200, 200, 180, 0.08)',
text: 'rgb(200, 200, 180)',
contrastText: 'rgb(200, 200, 180)',
border: 'rgba(200, 200, 180, 0.08)',
name: 'secondary',
borderTransparent: 'rgba(200, 200, 180, 0.25)',
},
background: {
canvas: '#111614',
primary: '#1d2220',
secondary: '#27312E',
elevated: '#27312E',
},
action: {
hover: 'rgba(200, 200, 180, 0.16)',
selected: 'rgba(200, 200, 180, 0.12)',
selectedBorder: '#FEAC34',
focus: 'rgba(200, 200, 180, 0.16)',
hoverOpacity: 0.08,
disabledText: 'rgba(200, 200, 180, 0.6)',
disabledBackground: 'rgba(200, 200, 180, 0.04)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)',
brandVertical: 'linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)',
},
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.15,
},
};
export default gildedGroveTheme;
@@ -0,0 +1,52 @@
{
"name": "Gloom",
"id": "gloom",
"colors": {
"mode": "dark",
"border": {
"weak": "rgba(210, 210, 220, 0.12)",
"medium": "rgba(210, 210, 220, 0.20)",
"strong": "rgba(210, 210, 220, 0.30)"
},
"text": {
"primary": "rgb(210, 210, 220)",
"secondary": "rgba(210, 210, 220, 0.65)",
"disabled": "rgba(210, 210, 220, 0.48)",
"link": "#f99a5c",
"maxContrast": "#FFF"
},
"primary": {
"main": "#ff934d",
"text": "#f99a5c",
"border": "#ff934d",
"name": "primary"
},
"secondary": {
"main": "rgba(195, 195, 245, 0.10)",
"shade": "rgba(195, 195, 245, 0.14)",
"transparent": "rgba(195, 195, 245, 0.08)",
"text": "rgba(195, 195, 245)",
"contrastText": "rgb(195, 195, 245)",
"border": "rgba(195, 195, 245, 0.08)"
},
"background": {
"canvas": "#000",
"primary": "#121118",
"secondary": "#211e28",
"elevated": "#211e28"
},
"action": {
"hover": "rgba(195, 195, 245, 0.07)",
"selected": "rgba(195, 195, 245, 0.11)",
"selectedBorder": "#ff934d",
"focus": "rgba(195, 195, 245, 0.07)",
"hoverOpacity": 0.05,
"disabledText": "rgba(210, 210, 220, 0.48)",
"disabledBackground": "rgba(210, 210, 220, 0.04)",
"disabledOpacity": 0.38
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.15
}
}
@@ -1,80 +0,0 @@
import { NewThemeOptions } from '../createTheme';
/**
* Torkel's GrafanaCon theme
* very WIP state
*/
const whiteBase = `210, 210, 220`;
const secondaryBase = `195, 195, 245`;
//const brandMain = '#3d71d9';
//const brandText = '#6e9fff';
const brandMain = '#ff934d';
const brandText = '#f99a5c';
const disabledText = `rgba(${whiteBase}, 0.48)`;
const gloomTheme: NewThemeOptions = {
name: 'Gloom',
colors: {
mode: 'dark',
border: {
weak: `rgba(${whiteBase}, 0.12)`,
medium: `rgba(${whiteBase}, 0.20)`,
strong: `rgba(${whiteBase}, 0.30)`,
},
text: {
primary: `rgb(${whiteBase})`,
secondary: `rgba(${whiteBase}, 0.65)`,
disabled: disabledText,
link: brandText,
maxContrast: '#FFF',
},
primary: {
main: brandMain,
text: brandText,
border: brandMain,
name: 'primary',
},
secondary: {
main: `rgba(${secondaryBase}, 0.10)`,
shade: `rgba(${secondaryBase}, 0.14)`,
transparent: `rgba(${secondaryBase}, 0.08)`,
text: `rgba(${secondaryBase})`,
contrastText: `rgb(${secondaryBase})`,
border: `rgba(${secondaryBase}, 0.08)`,
},
background: {
canvas: '#000',
primary: '#121118',
secondary: '#211e28',
elevated: '#211e28',
},
action: {
hover: `rgba(${secondaryBase}, 0.07)`,
selected: `rgba(${secondaryBase}, 0.11)`,
selectedBorder: brandMain,
focus: `rgba(${secondaryBase}, 0.07)`,
hoverOpacity: 0.05,
disabledText: disabledText,
disabledBackground: `rgba(${whiteBase}, 0.04)`,
disabledOpacity: 0.38,
},
// gradients: {
// brandHorizontal: 'linear-gradient(270deg, #ff934d 0%, #FEAC34 100%)',
// brandVertical: 'linear-gradient(0.01deg, #ff934d 0.01%, #FEAC34 99.99%)',
// },
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.15,
},
};
export default gloomTheme;
@@ -1,12 +1,12 @@
export { default as aubergine } from './aubergine';
export { default as debug } from './debug';
export { default as desertbloom } from './desertbloom';
export { default as gildedgrove } from './gildedgrove';
export { default as mars } from './mars';
export { default as matrix } from './matrix';
export { default as sapphiredusk } from './sapphiredusk';
export { default as synthwave } from './synthwave';
export { default as tron } from './tron';
export { default as victorian } from './victorian';
export { default as zen } from './zen';
export { default as gloom } from './gloom';
export { default as aubergine } from './aubergine.json';
export { default as debug } from './debug.json';
export { default as desertbloom } from './desertbloom.json';
export { default as gildedgrove } from './gildedgrove.json';
export { default as mars } from './mars.json';
export { default as matrix } from './matrix.json';
export { default as sapphiredusk } from './sapphiredusk.json';
export { default as synthwave } from './synthwave.json';
export { default as tron } from './tron.json';
export { default as victorian } from './victorian.json';
export { default as zen } from './zen.json';
export { default as gloom } from './gloom.json';
@@ -0,0 +1,50 @@
{
"name": "Mars",
"id": "mars",
"colors": {
"mode": "dark",
"border": {
"weak": "rgba(210, 90, 60, 0.2)",
"medium": "rgba(210, 90, 60, 0.35)",
"strong": "rgba(210, 90, 60, 0.5)"
},
"text": {
"primary": "#DDDDDD",
"secondary": "#BBBBBB",
"disabled": "rgba(221, 221, 221, 0.5)",
"link": "#FF6F61",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#FF6F61"
},
"secondary": {
"main": "#6a2f2f",
"text": "#BBBBBB",
"border": "rgba(210, 90, 60, 0.2)"
},
"background": {
"canvas": "#3C1E1E",
"primary": "#522626",
"secondary": "#6A2F2F",
"elevated": "#6A2F2F"
},
"action": {
"hover": "rgba(210, 90, 60, 0.16)",
"selected": "rgba(210, 90, 60, 0.12)",
"selectedBorder": "#FF6F61",
"focus": "rgba(210, 90, 60, 0.16)",
"hoverOpacity": 0.08,
"disabledText": "rgba(221, 221, 221, 0.5)",
"disabledBackground": "rgba(210, 90, 60, 0.08)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)",
"brandVertical": "linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.05,
"tonalOffset": 0.2
}
}
@@ -1,53 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const marsTheme: NewThemeOptions = {
name: 'Mars',
colors: {
mode: 'dark',
border: {
weak: 'rgba(210, 90, 60, 0.2)',
medium: 'rgba(210, 90, 60, 0.35)',
strong: 'rgba(210, 90, 60, 0.5)',
},
text: {
primary: '#DDDDDD',
secondary: '#BBBBBB',
disabled: 'rgba(221, 221, 221, 0.5)',
link: '#FF6F61',
maxContrast: '#FFFFFF',
},
primary: {
main: '#FF6F61',
},
secondary: {
main: '#6a2f2f',
text: '#BBBBBB',
border: 'rgba(210, 90, 60, 0.2)',
},
background: {
canvas: '#3C1E1E',
primary: '#522626',
secondary: '#6A2F2F',
elevated: '#6A2F2F',
},
action: {
hover: 'rgba(210, 90, 60, 0.16)',
selected: 'rgba(210, 90, 60, 0.12)',
selectedBorder: '#FF6F61',
focus: 'rgba(210, 90, 60, 0.16)',
hoverOpacity: 0.08,
disabledText: 'rgba(221, 221, 221, 0.5)',
disabledBackground: 'rgba(210, 90, 60, 0.08)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)',
brandVertical: 'linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)',
},
contrastThreshold: 3,
hoverFactor: 0.05,
tonalOffset: 0.2,
},
};
export default marsTheme;
@@ -0,0 +1,41 @@
{
"name": "Matrix",
"id": "matrix",
"colors": {
"mode": "dark",
"background": {
"canvas": "#000000",
"primary": "#020202",
"secondary": "#080808",
"elevated": "#080808"
},
"text": {
"primary": "#00c017",
"secondary": "#008910",
"disabled": "#006a0c",
"link": "#00ff41",
"maxContrast": "#00ff41"
},
"border": {
"weak": "#008f1144",
"medium": "#008f1188",
"strong": "#008910"
},
"primary": {
"main": "#008910"
},
"secondary": {
"text": "#008910"
},
"gradients": {
"brandVertical": "linear-gradient(0deg, #008910 0%, #00ff41 100%)",
"brandHorizontal": "linear-gradient(90deg, #008910 0%, #00ff41 100%)"
}
},
"shape": {
"borderRadius": 0
},
"typography": {
"fontFamily": "monospace"
}
}
@@ -1,44 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const matrixTheme: NewThemeOptions = {
name: 'Matrix',
colors: {
mode: 'dark',
background: {
canvas: '#000000',
primary: '#020202',
secondary: '#080808',
elevated: '#080808',
},
text: {
primary: '#00c017',
secondary: '#008910',
disabled: '#006a0c',
link: '#00ff41',
maxContrast: '#00ff41',
},
border: {
weak: '#008f1144',
medium: '#008f1188',
strong: '#008910',
},
primary: {
main: '#008910',
},
secondary: {
text: '#008910',
},
gradients: {
brandVertical: 'linear-gradient(0deg, #008910 0%, #00ff41 100%)',
brandHorizontal: 'linear-gradient(90deg, #008910 0%, #00ff41 100%)',
},
},
shape: {
borderRadius: 0,
},
typography: {
fontFamily: 'monospace',
},
};
export default matrixTheme;
@@ -0,0 +1,76 @@
{
"name": "Sapphire dusk",
"id": "sapphiredusk",
"colors": {
"mode": "dark",
"border": {
"weak": "#232e47",
"medium": "#2c3853",
"strong": "#404d6b"
},
"text": {
"primary": "#FFFFFF",
"secondary": "#bcccdd",
"disabled": "#838da5",
"link": "#93EBF0",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#93EBF0",
"text": "#a8e9ed",
"border": "#93ebf0",
"name": "primary",
"shade": "#c0f5d9",
"transparent": "#93EBF029",
"contrastText": "#111614",
"borderTransparent": "#93ebf040"
},
"secondary": {
"main": "#2c364f",
"shade": "#36415e",
"transparent": "rgba(200, 200, 180, 0.08)",
"text": "#d1dfff",
"contrastText": "#acfeff",
"border": "rgba(200, 200, 180, 0.08)",
"name": "secondary",
"borderTransparent": "rgba(200, 200, 180, 0.25)"
},
"info": {
"main": "#4d4593",
"text": "#a8e9ed",
"border": "#5d54a7"
},
"error": {
"main": "#c63370"
},
"success": {
"main": "#1A7F4B"
},
"warning": {
"main": "#D448EA"
},
"background": {
"canvas": "#1e273d",
"primary": "#12192e",
"secondary": "#212c47",
"elevated": "#212c47"
},
"action": {
"hover": "#364057",
"selected": "#364260",
"selectedBorder": "#D448EA",
"focus": "#364057",
"hoverOpacity": 0.08,
"disabledText": "#838da5",
"disabledBackground": "rgba(54, 64, 87, 0.2)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)",
"brandVertical": "linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.15
}
}
@@ -1,79 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const sapphireDuskTheme: NewThemeOptions = {
name: 'Sapphire dusk',
colors: {
mode: 'dark',
border: {
weak: '#232e47',
medium: '#2c3853',
strong: '#404d6b',
},
text: {
primary: '#FFFFFF',
secondary: '#bcccdd',
disabled: '#838da5',
link: '#93EBF0',
maxContrast: '#FFFFFF',
},
primary: {
main: '#93EBF0',
text: '#a8e9ed',
border: '#93ebf0',
name: 'primary',
shade: '#c0f5d9',
transparent: '#93EBF029',
contrastText: '#111614',
borderTransparent: '#93ebf040',
},
secondary: {
main: '#2c364f',
shade: '#36415e',
transparent: 'rgba(200, 200, 180, 0.08)',
text: '#d1dfff',
contrastText: '#acfeff',
border: 'rgba(200, 200, 180, 0.08)',
name: 'secondary',
borderTransparent: 'rgba(200, 200, 180, 0.25)',
},
info: {
main: '#4d4593',
text: '#a8e9ed',
border: '#5d54a7',
},
error: {
main: '#c63370',
},
success: {
main: '#1A7F4B',
},
warning: {
main: '#D448EA',
},
background: {
canvas: '#1e273d',
primary: '#12192e',
secondary: '#212c47',
elevated: '#212c47',
},
action: {
hover: '#364057',
selected: '#364260',
selectedBorder: '#D448EA',
focus: '#364057',
hoverOpacity: 0.08,
disabledText: '#838da5',
disabledBackground: 'rgba(54, 64, 87, 0.2)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)',
brandVertical: 'linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)',
},
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.15,
},
};
export default sapphireDuskTheme;
@@ -0,0 +1,50 @@
{
"name": "Synthwave",
"id": "synthwave",
"colors": {
"mode": "dark",
"border": {
"weak": "rgba(255, 20, 147, 0.12)",
"medium": "rgba(255, 20, 147, 0.20)",
"strong": "rgba(255, 20, 147, 0.30)"
},
"text": {
"primary": "#E0E0E0",
"secondary": "rgba(224, 224, 224, 0.75)",
"disabled": "rgba(224, 224, 224, 0.5)",
"link": "#FF69B4",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#FF1493"
},
"secondary": {
"main": "#37183a",
"text": "rgba(224, 224, 224, 0.75)",
"border": "rgba(255, 20, 147, 0.10)"
},
"background": {
"canvas": "#1A1A2E",
"primary": "#16213E",
"secondary": "#0F3460",
"elevated": "#0F3460"
},
"action": {
"hover": "rgba(255, 20, 147, 0.16)",
"selected": "rgba(255, 20, 147, 0.12)",
"selectedBorder": "#FF1493",
"focus": "rgba(255, 20, 147, 0.16)",
"hoverOpacity": 0.08,
"disabledText": "rgba(224, 224, 224, 0.5)",
"disabledBackground": "rgba(255, 20, 147, 0.08)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)",
"brandVertical": "linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.15
}
}
@@ -1,53 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const synthwaveTheme: NewThemeOptions = {
name: 'Synthwave',
colors: {
mode: 'dark',
border: {
weak: 'rgba(255, 20, 147, 0.12)',
medium: 'rgba(255, 20, 147, 0.20)',
strong: 'rgba(255, 20, 147, 0.30)',
},
text: {
primary: '#E0E0E0',
secondary: 'rgba(224, 224, 224, 0.75)',
disabled: 'rgba(224, 224, 224, 0.5)',
link: '#FF69B4',
maxContrast: '#FFFFFF',
},
primary: {
main: '#FF1493',
},
secondary: {
main: '#37183a',
text: 'rgba(224, 224, 224, 0.75)',
border: 'rgba(255, 20, 147, 0.10)',
},
background: {
canvas: '#1A1A2E',
primary: '#16213E',
secondary: '#0F3460',
elevated: '#0F3460',
},
action: {
hover: 'rgba(255, 20, 147, 0.16)',
selected: 'rgba(255, 20, 147, 0.12)',
selectedBorder: '#FF1493',
focus: 'rgba(255, 20, 147, 0.16)',
hoverOpacity: 0.08,
disabledText: 'rgba(224, 224, 224, 0.5)',
disabledBackground: 'rgba(255, 20, 147, 0.08)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)',
brandVertical: 'linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)',
},
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.15,
},
};
export default synthwaveTheme;
@@ -0,0 +1,50 @@
{
"name": "Tron",
"id": "tron",
"colors": {
"mode": "dark",
"border": {
"weak": "rgba(0, 255, 255, 0.12)",
"medium": "rgba(0, 255, 255, 0.20)",
"strong": "rgba(0, 255, 255, 0.30)"
},
"text": {
"primary": "#E0E0E0",
"secondary": "rgba(224, 224, 224, 0.75)",
"disabled": "rgba(224, 224, 224, 0.5)",
"link": "#00FFFF",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#00FFFF"
},
"secondary": {
"main": "#0b2e36",
"text": "rgba(224, 224, 224, 0.75)",
"border": "rgba(0, 255, 255, 0.10)"
},
"background": {
"canvas": "#0A0F18",
"primary": "#0F1B2A",
"secondary": "#152234",
"elevated": "#152234"
},
"action": {
"hover": "rgba(0, 255, 255, 0.16)",
"selected": "rgba(0, 255, 255, 0.12)",
"selectedBorder": "#00FFFF",
"focus": "rgba(0, 255, 255, 0.16)",
"hoverOpacity": 0.08,
"disabledText": "rgba(224, 224, 224, 0.5)",
"disabledBackground": "rgba(0, 255, 255, 0.08)",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)",
"brandVertical": "linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.05,
"tonalOffset": 0.2
}
}
@@ -1,53 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const tronTheme: NewThemeOptions = {
name: 'Tron',
colors: {
mode: 'dark',
border: {
weak: 'rgba(0, 255, 255, 0.12)',
medium: 'rgba(0, 255, 255, 0.20)',
strong: 'rgba(0, 255, 255, 0.30)',
},
text: {
primary: '#E0E0E0',
secondary: 'rgba(224, 224, 224, 0.75)',
disabled: 'rgba(224, 224, 224, 0.5)',
link: '#00FFFF',
maxContrast: '#FFFFFF',
},
primary: {
main: '#00FFFF',
},
secondary: {
main: '#0b2e36',
text: 'rgba(224, 224, 224, 0.75)',
border: 'rgba(0, 255, 255, 0.10)',
},
background: {
canvas: '#0A0F18',
primary: '#0F1B2A',
secondary: '#152234',
elevated: '#152234',
},
action: {
hover: 'rgba(0, 255, 255, 0.16)',
selected: 'rgba(0, 255, 255, 0.12)',
selectedBorder: '#00FFFF',
focus: 'rgba(0, 255, 255, 0.16)',
hoverOpacity: 0.08,
disabledText: 'rgba(224, 224, 224, 0.5)',
disabledBackground: 'rgba(0, 255, 255, 0.08)',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)',
brandVertical: 'linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)',
},
contrastThreshold: 3,
hoverFactor: 0.05,
tonalOffset: 0.2,
},
};
export default tronTheme;
@@ -0,0 +1,54 @@
{
"name": "Victorian",
"id": "victorian",
"colors": {
"mode": "dark",
"border": {
"weak": "#3A2C22",
"medium": "#3A2C22",
"strong": "#4B3D32"
},
"text": {
"primary": "#D9D0A2",
"secondary": "#C4B89B",
"disabled": "#A89F91",
"link": "#C28A4D",
"maxContrast": "#FFFFFF"
},
"primary": {
"main": "#C28A4D"
},
"secondary": {
"main": "#3A2C22",
"text": "#C4B89B",
"border": "#4B3D32"
},
"background": {
"canvas": "#1F1510",
"primary": "#2C1A13",
"secondary": "#402A21",
"elevated": "#402A21"
},
"action": {
"hover": "#3A2C22",
"selected": "#4B3D32",
"selectedBorder": "#C28A4D",
"focus": "#C28A4D",
"hoverOpacity": 0.1,
"disabledText": "#A89F91",
"disabledBackground": "#402A21",
"disabledOpacity": 0.38
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)",
"brandVertical": "linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)"
},
"contrastThreshold": 4,
"hoverFactor": 0.07,
"tonalOffset": 0.15
},
"typography": {
"fontFamily": "\"Georgia\", \"Times New Roman\", serif",
"fontFamilyMonospace": "'Courier New', monospace"
}
}
@@ -1,57 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const victorianTheme: NewThemeOptions = {
name: 'Victorian',
colors: {
mode: 'dark',
border: {
weak: '#3A2C22',
medium: '#3A2C22',
strong: '#4B3D32',
},
text: {
primary: '#D9D0A2',
secondary: '#C4B89B',
disabled: '#A89F91',
link: '#C28A4D',
maxContrast: '#FFFFFF',
},
primary: {
main: '#C28A4D',
},
secondary: {
main: '#3A2C22',
text: '#C4B89B',
border: '#4B3D32',
},
background: {
canvas: '#1F1510',
primary: '#2C1A13',
secondary: '#402A21',
elevated: '#402A21',
},
action: {
hover: '#3A2C22',
selected: '#4B3D32',
selectedBorder: '#C28A4D',
focus: '#C28A4D',
hoverOpacity: 0.1,
disabledText: '#A89F91',
disabledBackground: '#402A21',
disabledOpacity: 0.38,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)',
brandVertical: 'linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)',
},
contrastThreshold: 4,
hoverFactor: 0.07,
tonalOffset: 0.15,
},
typography: {
fontFamily: '"Georgia", "Times New Roman", serif',
fontFamilyMonospace: "'Courier New', monospace",
},
};
export default victorianTheme;
@@ -0,0 +1,50 @@
{
"name": "Zen",
"id": "zen",
"colors": {
"mode": "light",
"text": {
"primary": "#333333",
"secondary": "#666666",
"disabled": "#B8B8B8",
"link": "#4F9F6E",
"maxContrast": "#000000"
},
"border": {
"weak": "#B1B7B3",
"medium": "#A2A8A2",
"strong": "#7C7F7A"
},
"primary": {
"main": "#6D8E6D"
},
"secondary": {
"main": "#E0E0E0",
"text": "#666666",
"border": "#A2A8A2"
},
"background": {
"canvas": "#F4F4F4",
"primary": "#E9E9E9",
"secondary": "#D8D8D8",
"elevated": "#E9E9E9"
},
"action": {
"hover": "#D1D1D1",
"selected": "#B8B8B8",
"selectedBorder": "#88B88B",
"hoverOpacity": 0.1,
"focus": "#D1D1D1",
"disabledBackground": "#E0E0E0",
"disabledText": "#B8B8B8",
"disabledOpacity": 0.5
},
"gradients": {
"brandHorizontal": "linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)",
"brandVertical": "linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)"
},
"contrastThreshold": 3,
"hoverFactor": 0.03,
"tonalOffset": 0.2
}
}
@@ -1,53 +0,0 @@
import { NewThemeOptions } from '../createTheme';
const zenTheme: NewThemeOptions = {
name: 'Zen',
colors: {
mode: 'light',
text: {
primary: '#333333',
secondary: '#666666',
disabled: '#B8B8B8',
link: '#4F9F6E',
maxContrast: '#000000',
},
border: {
weak: '#B1B7B3',
medium: '#A2A8A2',
strong: '#7C7F7A',
},
primary: {
main: '#6D8E6D',
},
secondary: {
main: '#E0E0E0',
text: '#666666',
border: '#A2A8A2',
},
background: {
canvas: '#F4F4F4',
primary: '#E9E9E9',
secondary: '#D8D8D8',
elevated: '#E9E9E9',
},
action: {
hover: '#D1D1D1',
selected: '#B8B8B8',
selectedBorder: '#88B88B',
hoverOpacity: 0.1,
focus: '#D1D1D1',
disabledBackground: '#E0E0E0',
disabledText: '#B8B8B8',
disabledOpacity: 0.5,
},
gradients: {
brandHorizontal: 'linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)',
brandVertical: 'linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)',
},
contrastThreshold: 3,
hoverFactor: 0.03,
tonalOffset: 0.2,
},
};
export default zenTheme;
+22 -11
View File
@@ -1,3 +1,5 @@
import { z } from 'zod';
import { GrafanaTheme } from '../types/theme';
import { ThemeBreakpoints } from './breakpoints';
@@ -35,27 +37,36 @@ export interface GrafanaTheme2 {
flags: {};
}
/** @alpha */
export interface ThemeRichColor {
export const ThemeRichColorInputSchema = z.object({
/** color intent (primary, secondary, info, error, etc) */
name: string;
name: z.string().optional(),
/** Main color */
main: string;
main: z.string().optional(),
/** Used for hover */
shade: string;
shade: z.string().optional(),
/** Used for text */
text: string;
text: z.string().optional(),
/** Used for borders */
border: string;
border: z.string().optional(),
/** Used subtly colored backgrounds */
transparent: string;
transparent: z.string().optional(),
/** Used for weak colored borders like larger alert/banner boxes and smaller badges and tags */
borderTransparent: string;
borderTransparent: z.string().optional(),
/** Text color for text ontop of main */
contrastText: string;
}
contrastText: z.string().optional(),
});
export const ThemeRichColorSchema = ThemeRichColorInputSchema.required();
/** @alpha */
export type ThemeRichColor = z.infer<typeof ThemeRichColorSchema>;
/** @internal */
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
/** @internal */
export type DeepRequired<T> = Required<{
[P in keyof T]: T[P] extends Required<T[P]> ? T[P] : DeepRequired<T[P]>;
}>;
-12
View File
@@ -622,10 +622,6 @@ export interface FeatureToggles {
*/
exploreLogsAggregatedMetrics?: boolean;
/**
* Used in Logs Drilldown to limit the time range
*/
exploreLogsLimitedTimeRange?: boolean;
/**
* Enables the gRPC client to authenticate with the App Platform by using ID & access tokens
*/
appPlatformGrpcClientAuth?: boolean;
@@ -653,10 +649,6 @@ export interface FeatureToggles {
*/
rolePickerDrawer?: boolean;
/**
* Enable sprinkles on unified storage search
*/
unifiedStorageSearchSprinkles?: boolean;
/**
* Pick the dual write mode from database configs
*/
managedDualWriter?: boolean;
@@ -695,10 +687,6 @@ export interface FeatureToggles {
*/
passwordlessMagicLinkAuthentication?: boolean;
/**
* Display Related Logs in Grafana Metrics Drilldown
*/
exploreMetricsRelatedLogs?: boolean;
/**
* Adds support for quotes and special characters in label values for Prometheus queries
*/
prometheusSpecialCharsInLabelValues?: boolean;
+1 -2
View File
@@ -9,5 +9,4 @@
* and be subject to the standard policies
*/
// This is a dummy export so typescript doesn't error importing an "empty module"
export const unstable = {};
export { default as themeJsonSchema } from './themes/schema.generated.json';
@@ -0,0 +1,500 @@
/**
* Types for Scopes API - matching @grafana/data types
*/
export interface ScopeFilter {
key: string;
value: string;
operator: 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match';
}
export interface ScopeSpec {
title: string;
filters: ScopeFilter[];
}
export interface Scope {
metadata: {
name: string;
};
spec: ScopeSpec;
}
export interface ScopeNodeSpec {
nodeType: 'container' | 'leaf';
title: string;
description?: string;
disableMultiSelect?: boolean;
linkType?: 'scope';
linkId?: string;
parentName: string;
}
export interface ScopeNode {
metadata: {
name: string;
};
spec: ScopeNodeSpec;
}
export interface ScopeDashboardBindingSpec {
dashboard: string;
scope: string;
}
export interface ScopeDashboardBindingStatus {
dashboardTitle: string;
groups?: string[];
}
export interface ScopeDashboardBinding {
metadata: {
name: string;
};
spec: ScopeDashboardBindingSpec;
status: ScopeDashboardBindingStatus;
}
export interface ScopeNavigation {
metadata: {
name: string;
};
spec: {
url: string;
scope: string;
subScope?: string;
preLoadSubScopeChildren?: boolean;
expandOnLoad?: boolean;
disableSubScopeSelection?: boolean;
};
status: {
title: string;
groups?: string[];
};
}
export const MOCK_SCOPES: Scope[] = [
{
metadata: { name: 'cloud' },
spec: {
title: 'Cloud',
filters: [{ key: 'cloud', value: '.*', operator: 'regex-match' }],
},
},
{
metadata: { name: 'dev' },
spec: {
title: 'Dev',
filters: [{ key: 'cloud', value: 'dev', operator: 'equals' }],
},
},
{
metadata: { name: 'ops' },
spec: {
title: 'Ops',
filters: [{ key: 'cloud', value: 'ops', operator: 'equals' }],
},
},
{
metadata: { name: 'prod' },
spec: {
title: 'Prod',
filters: [{ key: 'cloud', value: 'prod', operator: 'equals' }],
},
},
{
metadata: { name: 'grafana' },
spec: {
title: 'Grafana',
filters: [{ key: 'app', value: 'grafana', operator: 'equals' }],
},
},
{
metadata: { name: 'mimir' },
spec: {
title: 'Mimir',
filters: [{ key: 'app', value: 'mimir', operator: 'equals' }],
},
},
{
metadata: { name: 'loki' },
spec: {
title: 'Loki',
filters: [{ key: 'app', value: 'loki', operator: 'equals' }],
},
},
{
metadata: { name: 'tempo' },
spec: {
title: 'Tempo',
filters: [{ key: 'app', value: 'tempo', operator: 'equals' }],
},
},
{
metadata: { name: 'dev-env' },
spec: {
title: 'Development',
filters: [{ key: 'environment', value: 'dev', operator: 'equals' }],
},
},
{
metadata: { name: 'prod-env' },
spec: {
title: 'Production',
filters: [{ key: 'environment', value: 'prod', operator: 'equals' }],
},
},
];
const dashboardBindingsGenerator = (
scopes: string[],
dashboards: Array<{ dashboardTitle: string; dashboardKey?: string; groups?: string[] }>
) =>
scopes.reduce<ScopeDashboardBinding[]>((scopeAcc, scopeTitle) => {
const scope = scopeTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
return [
...scopeAcc,
...dashboards.reduce<ScopeDashboardBinding[]>((acc, { dashboardTitle, groups, dashboardKey }, idx) => {
dashboardKey = dashboardKey ?? dashboardTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
const group = !groups
? ''
: groups.length === 1
? groups[0] === ''
? ''
: `${groups[0].toLowerCase().replaceAll(' ', '-').replaceAll('/', '-')}-`
: `multiple${idx}-`;
const dashboard = `${group}${dashboardKey}`;
return [
...acc,
{
metadata: { name: `${scope}-${dashboard}` },
spec: {
dashboard,
scope,
},
status: {
dashboardTitle,
groups,
},
},
];
}, []),
];
}, []);
export const MOCK_SCOPE_DASHBOARD_BINDINGS: ScopeDashboardBinding[] = [
...dashboardBindingsGenerator(
['Grafana'],
[
{ dashboardTitle: 'Data Sources', groups: ['General'] },
{ dashboardTitle: 'Usage', groups: ['General'] },
{ dashboardTitle: 'Frontend Errors', groups: ['Observability'] },
{ dashboardTitle: 'Frontend Logs', groups: ['Observability'] },
{ dashboardTitle: 'Backend Errors', groups: ['Observability'] },
{ dashboardTitle: 'Backend Logs', groups: ['Observability'] },
{ dashboardTitle: 'Usage Overview', groups: ['Usage'] },
{ dashboardTitle: 'Data Sources', groups: ['Usage'] },
{ dashboardTitle: 'Stats', groups: ['Usage'] },
{ dashboardTitle: 'Overview', groups: [''] },
{ dashboardTitle: 'Frontend' },
{ dashboardTitle: 'Stats' },
]
),
...dashboardBindingsGenerator(
['Loki', 'Tempo', 'Mimir'],
[
{ dashboardTitle: 'Ingester', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Distributor', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Compacter', groups: ['Components', 'Investigations'] },
{ dashboardTitle: 'Datasource Errors', groups: ['Observability', 'Investigations'] },
{ dashboardTitle: 'Datasource Logs', groups: ['Observability', 'Investigations'] },
{ dashboardTitle: 'Overview' },
{ dashboardTitle: 'Stats', dashboardKey: 'another-stats' },
]
),
...dashboardBindingsGenerator(
['Dev', 'Ops', 'Prod'],
[
{ dashboardTitle: 'Overview', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Metrics', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Labels', groups: ['Cardinality Management'] },
{ dashboardTitle: 'Overview', groups: ['Usage Insights'] },
{ dashboardTitle: 'Data Sources', groups: ['Usage Insights'] },
{ dashboardTitle: 'Query Errors', groups: ['Usage Insights'] },
{ dashboardTitle: 'Alertmanager', groups: ['Usage Insights'] },
{ dashboardTitle: 'Metrics Ingestion', groups: ['Usage Insights'] },
{ dashboardTitle: 'Billing/Usage' },
]
),
];
export const MOCK_NODES: ScopeNode[] = [
{
metadata: { name: 'applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Application Scopes',
parentName: '',
},
},
{
metadata: { name: 'cloud' },
spec: {
nodeType: 'container',
title: 'Cloud',
description: 'Cloud Scopes',
disableMultiSelect: true,
linkType: 'scope',
linkId: 'cloud',
parentName: '',
},
},
{
metadata: { name: 'applications-grafana' },
spec: {
nodeType: 'leaf',
title: 'Grafana',
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
parentName: 'applications',
},
},
{
metadata: { name: 'applications-mimir' },
spec: {
nodeType: 'leaf',
title: 'Mimir',
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
parentName: 'applications',
},
},
{
metadata: { name: 'applications-loki' },
spec: {
nodeType: 'leaf',
title: 'Loki',
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
parentName: 'applications',
},
},
{
metadata: { name: 'applications-tempo' },
spec: {
nodeType: 'leaf',
title: 'Tempo',
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
parentName: 'applications',
},
},
{
metadata: { name: 'applications-cloud' },
spec: {
nodeType: 'container',
title: 'Cloud',
description: 'Application/Cloud Scopes',
linkType: 'scope',
linkId: 'cloud',
parentName: 'applications',
},
},
{
metadata: { name: 'applications-cloud-dev' },
spec: {
nodeType: 'leaf',
title: 'Dev',
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
parentName: 'applications-cloud',
},
},
{
metadata: { name: 'applications-cloud-ops' },
spec: {
nodeType: 'leaf',
title: 'Ops',
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
parentName: 'applications-cloud',
},
},
{
metadata: { name: 'applications-cloud-prod' },
spec: {
nodeType: 'leaf',
title: 'Prod',
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
parentName: 'applications-cloud',
},
},
{
metadata: { name: 'cloud-dev' },
spec: {
nodeType: 'leaf',
title: 'Dev',
description: 'Dev',
linkType: 'scope',
linkId: 'dev',
parentName: 'cloud',
},
},
{
metadata: { name: 'cloud-ops' },
spec: {
nodeType: 'leaf',
title: 'Ops',
description: 'Ops',
linkType: 'scope',
linkId: 'ops',
parentName: 'cloud',
},
},
{
metadata: { name: 'cloud-prod' },
spec: {
nodeType: 'leaf',
title: 'Prod',
description: 'Prod',
linkType: 'scope',
linkId: 'prod',
parentName: 'cloud',
},
},
{
metadata: { name: 'cloud-applications' },
spec: {
nodeType: 'container',
title: 'Applications',
description: 'Cloud/Application Scopes',
parentName: 'cloud',
},
},
{
metadata: { name: 'cloud-applications-grafana' },
spec: {
nodeType: 'leaf',
title: 'Grafana',
description: 'Grafana',
linkType: 'scope',
linkId: 'grafana',
parentName: 'cloud-applications',
},
},
{
metadata: { name: 'cloud-applications-mimir' },
spec: {
nodeType: 'leaf',
title: 'Mimir',
description: 'Mimir',
linkType: 'scope',
linkId: 'mimir',
parentName: 'cloud-applications',
},
},
{
metadata: { name: 'cloud-applications-loki' },
spec: {
nodeType: 'leaf',
title: 'Loki',
description: 'Loki',
linkType: 'scope',
linkId: 'loki',
parentName: 'cloud-applications',
},
},
{
metadata: { name: 'cloud-applications-tempo' },
spec: {
nodeType: 'leaf',
title: 'Tempo',
description: 'Tempo',
linkType: 'scope',
linkId: 'tempo',
parentName: 'cloud-applications',
},
},
{
metadata: { name: 'environments' },
spec: {
nodeType: 'container',
title: 'Environments',
description: 'Environment Scopes',
disableMultiSelect: true,
parentName: '',
},
},
{
metadata: { name: 'environments-dev' },
spec: {
nodeType: 'container',
title: 'Development',
description: 'Development Environment',
linkType: 'scope',
linkId: 'dev-env',
parentName: 'environments',
},
},
{
metadata: { name: 'environments-prod' },
spec: {
nodeType: 'container',
title: 'Production',
description: 'Production Environment',
linkType: 'scope',
linkId: 'prod-env',
parentName: 'environments',
},
},
];
export const MOCK_SUB_SCOPE_MIMIR_ITEMS: ScopeNavigation[] = [
{
metadata: { name: 'mimir-item-1' },
spec: {
scope: 'mimir',
url: '/d/mimir-dashboard-1',
},
status: {
title: 'Mimir Dashboard 1',
groups: ['General'],
},
},
{
metadata: { name: 'mimir-item-2' },
spec: {
scope: 'mimir',
url: '/d/mimir-dashboard-2',
},
status: {
title: 'Mimir Dashboard 2',
groups: ['Observability'],
},
},
];
export const MOCK_SUB_SCOPE_LOKI_ITEMS: ScopeNavigation[] = [
{
metadata: { name: 'loki-item-1' },
spec: {
scope: 'loki',
url: '/d/loki-dashboard-1',
},
status: {
title: 'Loki Dashboard 1',
groups: ['General'],
},
},
];
@@ -12,6 +12,7 @@ import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v
import appPlatformDashboardv1beta1Handlers from './apis/dashboard.grafana.app/v1beta1/handlers';
import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers';
import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers';
import appPlatformScopev0alpha1Handlers from './apis/scope.grafana.app/v0alpha1/handlers';
const allHandlers: HttpHandler[] = [
// Legacy handlers
@@ -29,6 +30,7 @@ const allHandlers: HttpHandler[] = [
...appPlatformFolderv1beta1Handlers,
...appPlatformIamv0alpha1Handlers,
...appPlatformCollectionsv1alpha1Handlers,
...appPlatformScopev0alpha1Handlers,
];
export default allHandlers;
@@ -0,0 +1,131 @@
import { HttpResponse, http } from 'msw';
import {
MOCK_NODES,
MOCK_SCOPES,
MOCK_SCOPE_DASHBOARD_BINDINGS,
MOCK_SUB_SCOPE_LOKI_ITEMS,
MOCK_SUB_SCOPE_MIMIR_ITEMS,
ScopeNavigation,
} from '../../../../fixtures/scopes';
import { getErrorResponse } from '../../../helpers';
const API_BASE = '/apis/scope.grafana.app/v0alpha1/namespaces/:namespace';
/**
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopes/:name
*
* Fetches a single scope by name.
*/
const getScopeHandler = () =>
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopes/:name`, ({ params }) => {
const { name } = params;
const scope = MOCK_SCOPES.find((s) => s.metadata.name === name);
if (!scope) {
return HttpResponse.json(getErrorResponse(`scopes.scope.grafana.app "${name}" not found`, 404), {
status: 404,
});
}
return HttpResponse.json(scope);
});
/**
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopenodes/:name
*
* Fetches a single scope node by name.
*/
const getScopeNodeHandler = () =>
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopenodes/:name`, ({ params }) => {
const { name } = params;
const node = MOCK_NODES.find((n) => n.metadata.name === name);
if (!node) {
return HttpResponse.json(getErrorResponse(`scopenodes.scope.grafana.app "${name}" not found`, 404), {
status: 404,
});
}
return HttpResponse.json(node);
});
/**
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_node_children
*
* Finds scope node children based on parent and query filters.
*/
const findScopeNodeChildrenHandler = () =>
http.get(`${API_BASE}/find/scope_node_children`, ({ request }) => {
const url = new URL(request.url);
const parent = url.searchParams.get('parent') ?? '';
const query = url.searchParams.get('query') ?? '';
const limitParam = url.searchParams.get('limit');
const names = url.searchParams.getAll('names');
let filtered = MOCK_NODES.filter(
(node) => node.spec.parentName === parent && node.spec.title.toLowerCase().includes(query.toLowerCase())
);
if (names.length > 0) {
filtered = MOCK_NODES.filter((node) => names.includes(node.metadata.name));
}
if (limitParam) {
const limit = parseInt(limitParam, 10);
filtered = filtered.slice(0, limit);
}
return HttpResponse.json({
items: filtered,
});
});
/**
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_dashboard_bindings
*
* Finds scope dashboard bindings for the given scope names.
*/
const findScopeDashboardBindingsHandler = () =>
http.get(`${API_BASE}/find/scope_dashboard_bindings`, ({ request }) => {
const url = new URL(request.url);
const scopeNames = url.searchParams.getAll('scope');
const bindings = MOCK_SCOPE_DASHBOARD_BINDINGS.filter((b) => scopeNames.includes(b.spec.scope));
return HttpResponse.json({
items: bindings,
});
});
/**
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_navigations
*
* Finds scope navigations for the given scope names.
*/
const findScopeNavigationsHandler = () =>
http.get(`${API_BASE}/find/scope_navigations`, ({ request }) => {
const url = new URL(request.url);
const scopeNames = url.searchParams.getAll('scope');
let items: ScopeNavigation[] = [];
if (scopeNames.includes('mimir')) {
items = [...items, ...MOCK_SUB_SCOPE_MIMIR_ITEMS];
}
if (scopeNames.includes('loki')) {
items = [...items, ...MOCK_SUB_SCOPE_LOKI_ITEMS];
}
return HttpResponse.json({
items,
});
});
export default [
getScopeHandler(),
getScopeNodeHandler(),
findScopeNodeChildrenHandler(),
findScopeDashboardBindingsHandler(),
findScopeNavigationsHandler(),
];
@@ -2,3 +2,12 @@ import { wellFormedTree } from './fixtures/folders';
export const getFolderFixtures = wellFormedTree;
export { MOCK_TEAMS, MOCK_TEAM_GROUPS } from './fixtures/teams';
export {
MOCK_SCOPES,
MOCK_NODES,
MOCK_SCOPE_DASHBOARD_BINDINGS,
MOCK_SUB_SCOPE_MIMIR_ITEMS,
MOCK_SUB_SCOPE_LOKI_ITEMS,
} from './fixtures/scopes';
export { default as allHandlers } from './handlers/all-handlers';
export { default as scopeHandlers } from './handlers/apis/scope.grafana.app/v0alpha1/handlers';
@@ -14,6 +14,8 @@ export type Props = React.ComponentProps<typeof TextArea> & {
isConfigured: boolean;
/** Called when the user clicks on the "Reset" button in order to clear the secret */
onReset: () => void;
/** If true, the text area will grow to fill available width. */
grow?: boolean;
};
export const CONFIGURED_TEXT = 'configured';
@@ -35,11 +37,11 @@ const getStyles = (theme: GrafanaTheme2) => {
*
* https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-secrettextarea--docs
*/
export const SecretTextArea = ({ isConfigured, onReset, ...props }: Props) => {
export const SecretTextArea = ({ isConfigured, onReset, grow, ...props }: Props) => {
const styles = useStyles2(getStyles);
return (
<Stack>
<Box>
<Box grow={grow ? 1 : undefined}>
{!isConfigured && <TextArea {...props} />}
{isConfigured && (
<TextArea
+1
View File
@@ -11,6 +11,7 @@ import (
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
_ "github.com/Azure/go-autorest/autorest"
_ "github.com/Azure/go-autorest/autorest/adal"
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/beevik/etree"
_ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api"
+17 -8
View File
@@ -552,6 +552,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
// gets dashboards that the user was granted read access to
permissions := user.GetPermissions()
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
folderPermissions := permissions[dashboards.ActionFoldersRead]
dashboardUids := make([]string, 0)
sharedDashboards := make([]string, 0)
@@ -562,6 +563,13 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
}
}
}
for _, folderPermission := range folderPermissions {
if folderUid, found := strings.CutPrefix(folderPermission, dashboards.ScopeFoldersPrefix); found {
if !slices.Contains(dashboardUids, folderUid) && folderUid != foldermodel.SharedWithMeFolderUID && folderUid != foldermodel.GeneralFolderUID {
dashboardUids = append(dashboardUids, folderUid)
}
}
}
if len(dashboardUids) == 0 {
return sharedDashboards, nil
@@ -572,9 +580,15 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
return sharedDashboards, err
}
folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE)
if err != nil {
return sharedDashboards, err
}
dashboardSearchRequest := &resourcepb.ResourceSearchRequest{
Fields: []string{"folder"},
Limit: int64(len(dashboardUids)),
Federated: []*resourcepb.ResourceKey{folderKey},
Fields: []string{"folder"},
Limit: int64(len(dashboardUids)),
Options: &resourcepb.ListOptions{
Key: key,
Fields: []*resourcepb.Requirement{{
@@ -610,12 +624,6 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
}
}
// only folders the user has access to will be returned here
folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE)
if err != nil {
return sharedDashboards, err
}
folderSearchRequest := &resourcepb.ResourceSearchRequest{
Fields: []string{"folder"},
Limit: int64(len(allFolders)),
@@ -628,6 +636,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
}},
},
}
// only folders the user has access to will be returned here
foldersResult, err := s.client.Search(ctx, folderSearchRequest)
if err != nil {
return sharedDashboards, err
+27 -3
View File
@@ -507,6 +507,15 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
[]byte("publicfolder"), // folder uid
},
},
{
Key: &resourcepb.ResourceKey{
Name: "sharedfolder",
Resource: "folder",
},
Cells: [][]byte{
[]byte("privatefolder"), // folder uid
},
},
},
},
}
@@ -550,6 +559,15 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
[]byte("privatefolder"), // folder uid
},
},
{
Key: &resourcepb.ResourceKey{
Name: "sharedfolder",
Resource: "folder",
},
Cells: [][]byte{
[]byte("privatefolder"), // folder uid
},
},
},
},
}
@@ -571,6 +589,7 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
allPermissions := make(map[int64]map[string][]string)
permissions := make(map[string][]string)
permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"}
permissions[dashboards.ActionFoldersRead] = []string{"folders:uid:sharedfolder"}
allPermissions[1] = permissions
// "Permissions" is where we store the uid of dashboards shared with the user
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions}))
@@ -581,14 +600,19 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
// first call gets all dashboards user has permission for
firstCall := mockClient.MockCalls[0]
assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder"})
assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder", "sharedfolder"})
// verify federated field is set to include folders
assert.NotNil(t, firstCall.Federated)
assert.Equal(t, 1, len(firstCall.Federated))
assert.Equal(t, "folder.grafana.app", firstCall.Federated[0].Group)
assert.Equal(t, "folders", firstCall.Federated[0].Resource)
// second call gets folders associated with the previous dashboards
secondCall := mockClient.MockCalls[1]
assert.Equal(t, secondCall.Options.Fields[0].Values, []string{"privatefolder", "publicfolder"})
// lastly, search ONLY for dashboards user has permission to read that are within folders the user does NOT have
// lastly, search ONLY for dashboards and folders user has permission to read that are within folders the user does NOT have
// permission to read
thirdCall := mockClient.MockCalls[2]
assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder"})
assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder", "sharedfolder"})
resp := rr.Result()
defer func() {
+1 -1
View File
@@ -156,7 +156,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O
}
}
}
connectLogger.Debug("responder sending status code", "statusCode", statusCode, "caller", getCaller(ctx))
connectLogger.Debug("responder sending status code", "statusCode", *statusCode, "caller", getCaller(ctx))
},
func(err error) {
+53
View File
@@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"strconv"
"time"
"github.com/grafana/dskit/flagext"
@@ -15,11 +16,15 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health/grpc_health_v1"
)
@@ -111,14 +116,25 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
Help: "Time spent executing requests to resource server.",
Buckets: prometheus.ExponentialBuckets(0.008, 4, 7),
}, []string{"operation", "status_code"})
factoryRequestRetries := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
Name: "resource_server_client_request_retries_total",
Help: "Total number of retries for requests to the resource server.",
}, []string{"operation"})
factory := ringclient.PoolInstFunc(func(inst ring.InstanceDesc) (ringclient.PoolClient, error) {
unaryInterceptors, streamInterceptors := grpcclient.Instrument(factoryRequestDuration)
// Add retry interceptors for transient connection issues
unaryInterceptors = append(unaryInterceptors, ringClientRetryInterceptor())
unaryInterceptors = append(unaryInterceptors, ringClientRetryInstrument(factoryRequestRetries))
opts, err := clientCfg.DialOption(unaryInterceptors, streamInterceptors, nil)
if err != nil {
return nil, err
}
opts = append(opts, connectionBackoffOptions())
conn, err := grpc.NewClient(inst.Addr, opts...)
if err != nil {
return nil, fmt.Errorf("failed to dial resource server %s %s: %s", inst.Id, inst.Addr, err)
@@ -135,3 +151,40 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
return ringclient.NewPool(resource.RingName, poolCfg, nil, factory, clientsCount, log)
}
// ringClientRetryInterceptor creates an interceptor to perform retries for unary methods.
// It retries on ResourceExhausted and Unavailable codes, which are typical for
// transient connection issues and rate limiting.
func ringClientRetryInterceptor() grpc.UnaryClientInterceptor {
return grpc_retry.UnaryClientInterceptor(
grpc_retry.WithMax(3),
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.1)),
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
)
}
// ringClientRetryInstrument creates an interceptor to count retry attempts for metrics.
func ringClientRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// We can tell if a call is a retry by checking the retry attempt metadata.
attempt, err := strconv.Atoi(metautils.ExtractOutgoing(ctx).Get(grpc_retry.AttemptMetadataKey))
if err == nil && attempt > 0 {
metric.WithLabelValues(method).Inc()
}
return invoker(ctx, method, req, resp, cc, opts...)
}
}
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
// transient connection failures (e.g., during pod restarts).
func connectionBackoffOptions() grpc.DialOption {
return grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
BaseDelay: 100 * time.Millisecond,
Multiplier: 1.6,
Jitter: 0.2,
MaxDelay: 10 * time.Second,
},
MinConnectTimeout: 5 * time.Second,
})
}
+49 -8
View File
@@ -11,11 +11,16 @@ import (
"github.com/spf13/pflag"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/rest"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/tracing"
secret "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
@@ -232,19 +237,16 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
if o.StorageType != StorageTypeUnifiedGrpc {
return nil
}
conn, err := grpc.NewClient(o.Address,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
grpcOpts := o.buildGrpcDialOptions()
conn, err := grpc.NewClient(o.Address, grpcOpts...)
if err != nil {
return err
}
var indexConn *grpc.ClientConn
if o.SearchServerAddress != "" {
indexConn, err = grpc.NewClient(o.SearchServerAddress,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
indexConn, err = grpc.NewClient(o.SearchServerAddress, grpcOpts...)
if err != nil {
return err
}
@@ -293,3 +295,42 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
serverConfig.RESTOptionsGetter = getter
return nil
}
// buildGrpcDialOptions creates gRPC dial options with resilience mechanisms:
// - Round-robin load balancing with client-side health checking
// - Retry interceptor for transient connection issues
// - Keepalive for long-lived connections
func (o *StorageOptions) buildGrpcDialOptions() []grpc.DialOption {
// Retry interceptor for transient connection issues (codes.Unavailable includes connection refused)
retryInterceptor := grpc_retry.UnaryClientInterceptor(
grpc_retry.WithMax(3),
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)),
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
)
opts := []grpc.DialOption{
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithChainUnaryInterceptor(retryInterceptor),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
BaseDelay: 100 * time.Millisecond,
Multiplier: 1.6,
Jitter: 0.2,
MaxDelay: 10 * time.Second,
},
MinConnectTimeout: 5 * time.Second,
}),
}
if o.GrpcClientKeepaliveTime > 0 {
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: o.GrpcClientKeepaliveTime,
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}))
}
return opts
}
+5 -1
View File
@@ -133,7 +133,11 @@ type FeatureFlag struct {
Stage FeatureFlagStage `json:"stage,omitempty"`
Owner codeowner `json:"-"` // Owner person or team that owns this feature flag
// CEL-GO expression. Using the value "true" will mean this is on by default
// Expression defined by the feature_toggles configuration.
// Supports multiple types including boolean, string, integer, float,
// and structured values following the OpenFeature specification.
// Using the value "true" means the feature flag is enabled by default,
// Using the value "1.0" means the default value of the feature flag is 1.0
Expression string `json:"expression,omitempty"`
// Special behavior properties
+4 -3
View File
@@ -8,6 +8,7 @@ import (
clientauthmiddleware "github.com/grafana/grafana/pkg/clientauth/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/open-feature/go-sdk/openfeature/memprovider"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/open-feature/go-sdk/openfeature"
@@ -26,7 +27,7 @@ type OpenFeatureConfig struct {
// HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers)
HTTPClient *http.Client
// StaticFlags are the feature flags to use with static provider
StaticFlags map[string]bool
StaticFlags map[string]memprovider.InMemoryFlag
// TargetingKey is used for evaluation context
TargetingKey string
// ContextAttrs are additional attributes for evaluation context
@@ -100,7 +101,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
func createProvider(
providerType string,
u *url.URL,
staticFlags map[string]bool,
staticFlags map[string]memprovider.InMemoryFlag,
httpClient *http.Client,
) (openfeature.FeatureProvider, error) {
if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType {
@@ -117,7 +118,7 @@ func createProvider(
}
}
return newStaticProvider(staticFlags)
return newStaticProvider(staticFlags, standardFeatureFlags)
}
func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
-22
View File
@@ -1031,13 +1031,6 @@ var (
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "exploreLogsLimitedTimeRange",
Description: "Used in Logs Drilldown to limit the time range",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "appPlatformGrpcClientAuth",
Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens",
@@ -1080,13 +1073,6 @@ var (
Stage: FeatureStageExperimental,
Owner: identityAccessTeam,
},
{
Name: "unifiedStorageSearchSprinkles",
Description: "Enable sprinkles on unified storage search",
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
HideFromDocs: true,
},
{
Name: "managedDualWriter",
Description: "Pick the dual write mode from database configs",
@@ -1148,14 +1134,6 @@ var (
Owner: identityAccessTeam,
HideFromDocs: true,
},
{
Name: "exploreMetricsRelatedLogs",
Description: "Display Related Logs in Grafana Metrics Drilldown",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
FrontendOnly: true,
HideFromDocs: false,
},
{
Name: "prometheusSpecialCharsInLabelValues",
Description: "Adds support for quotes and special characters in label values for Prometheus queries",
+2 -1
View File
@@ -47,7 +47,8 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) {
}
mgmt.warnings[key] = "unknown flag in config"
}
mgmt.startup[key] = val
mgmt.startup[key] = val.Variants[val.DefaultVariant] == true
}
// update the values
+1 -1
View File
@@ -29,7 +29,7 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) {
return nil, fmt.Errorf("failed to read feature flags from config: %w", err)
}
staticProvider, err := newStaticProvider(staticFlags)
staticProvider, err := newStaticProvider(staticFlags, standardFeatureFlags)
if err != nil {
return nil, fmt.Errorf("failed to create static provider: %w", err)
}
+18 -29
View File
@@ -1,8 +1,13 @@
package featuremgmt
import (
"fmt"
"maps"
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/grafana/grafana/pkg/setting"
)
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
@@ -28,37 +33,21 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
return keys, nil
}
func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) {
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags))
func newStaticProvider(confFlags map[string]memprovider.InMemoryFlag, standardFlags []FeatureFlag) (openfeature.FeatureProvider, error) {
flags := make(map[string]memprovider.InMemoryFlag, len(standardFlags))
// Parse and add standard flags
for _, flag := range standardFlags {
inMemFlag, err := setting.ParseFlag(flag.Name, flag.Expression)
if err != nil {
return nil, fmt.Errorf("failed to parse flag %s: %w", flag.Name, err)
}
flags[flag.Name] = inMemFlag
}
// Add flags from config.ini file
for name, value := range confFlags {
flags[name] = createInMemoryFlag(name, value)
}
// Add standard flags
for _, flag := range standardFeatureFlags {
if _, exists := flags[flag.Name]; !exists {
enabled := flag.Expression == "true"
flags[flag.Name] = createInMemoryFlag(flag.Name, enabled)
}
}
maps.Copy(flags, confFlags)
return newInMemoryBulkProvider(flags), nil
}
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
variant := "disabled"
if enabled {
variant = "enabled"
}
return memprovider.InMemoryFlag{
Key: name,
DefaultVariant: variant,
Variants: map[string]interface{}{
"enabled": true,
"disabled": false,
},
}
}
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/open-feature/go-sdk/openfeature"
"github.com/stretchr/testify/assert"
@@ -93,3 +94,144 @@ ABCD = true
enabledFeatureManager := mgr.GetEnabled(ctx)
assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager)
}
func Test_StaticProvider_TypedFlags(t *testing.T) {
tests := []struct {
flags FeatureFlag
defaultValue any
expectedValue any
}{
{
flags: FeatureFlag{
Name: "Flag",
Expression: "true",
},
defaultValue: false,
expectedValue: true,
},
{
flags: FeatureFlag{
Name: "Flag",
Expression: "1.0",
},
defaultValue: 0.0,
expectedValue: 1.0,
},
{
flags: FeatureFlag{
Name: "Flag",
Expression: "blue",
},
defaultValue: "red",
expectedValue: "blue",
},
{
flags: FeatureFlag{
Name: "Flag",
Expression: "1",
},
defaultValue: int64(0),
expectedValue: int64(1),
},
{
flags: FeatureFlag{
Name: "Flag",
Expression: `{ "foo": "bar" }`,
},
expectedValue: map[string]any{"foo": "bar"},
},
}
for _, tt := range tests {
provider, err := newStaticProvider(nil, []FeatureFlag{tt.flags})
assert.NoError(t, err)
var result any
switch tt.expectedValue.(type) {
case bool:
result = provider.BooleanEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(bool), openfeature.FlattenedContext{}).Value
case float64:
result = provider.FloatEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(float64), openfeature.FlattenedContext{}).Value
case string:
result = provider.StringEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(string), openfeature.FlattenedContext{}).Value
case int64:
result = provider.IntEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(int64), openfeature.FlattenedContext{}).Value
case map[string]any:
result = provider.ObjectEvaluation(t.Context(), tt.flags.Name, tt.defaultValue, openfeature.FlattenedContext{}).Value
}
assert.Equal(t, tt.expectedValue, result)
}
}
func Test_StaticProvider_ConfigOverride(t *testing.T) {
tests := []struct {
name string
originalValue string
configValue any
}{
{
name: "bool",
originalValue: "false",
configValue: true,
},
{
name: "int",
originalValue: "0",
configValue: int64(1),
},
{
name: "float",
originalValue: "0.0",
configValue: 1.0,
},
{
name: "string",
originalValue: "foo",
configValue: "bar",
},
{
name: "structure",
originalValue: "{}",
configValue: make(map[string]any),
},
}
for _, tt := range tests {
configFlags, standardFlags := makeFlags(tt)
provider, err := newStaticProvider(configFlags, standardFlags)
assert.NoError(t, err)
var result any
switch tt.configValue.(type) {
case bool:
result = provider.BooleanEvaluation(t.Context(), tt.name, false, openfeature.FlattenedContext{}).Value
case float64:
result = provider.FloatEvaluation(t.Context(), tt.name, 0.0, openfeature.FlattenedContext{}).Value
case string:
result = provider.StringEvaluation(t.Context(), tt.name, "foo", openfeature.FlattenedContext{}).Value
case int64:
result = provider.IntEvaluation(t.Context(), tt.name, 1, openfeature.FlattenedContext{}).Value
case map[string]any:
result = provider.ObjectEvaluation(t.Context(), tt.name, make(map[string]any), openfeature.FlattenedContext{}).Value
}
assert.Equal(t, tt.configValue, result)
}
}
func makeFlags(tt struct {
name string
originalValue string
configValue any
}) (map[string]memprovider.InMemoryFlag, []FeatureFlag) {
orig := FeatureFlag{
Name: tt.name,
Expression: tt.originalValue,
}
config := map[string]memprovider.InMemoryFlag{
tt.name: setting.NewInMemoryFlag(tt.name, tt.configValue),
}
return config, []FeatureFlag{orig}
}
@@ -409,7 +409,6 @@ lokiLabelNamesQueryApi,2024-12-13T14:31:41Z,,5ac7443fcec0db412d3333044a82c2c26b5
kubernetesCliDashboards,2024-12-13T22:55:43Z,2025-02-18T23:11:26Z,8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4,Stephanie Hingtgen
useV2DashboardsAPI,2024-12-17T21:17:09Z,2025-03-12T17:43:32Z,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop
investigationsBackend,2024-12-18T08:31:03Z,,f46c07aba7b6faccd2ecafc83051d1410cacc867,Jackson Coelho
unifiedStorageSearchSprinkles,2024-12-18T17:00:54Z,,4837585cab0fd84184a8c6f5d6891f442a2b95f1,owensmallwood
prometheusSpecialCharsInLabelValues,2024-12-18T21:31:08Z,,721c50a304588ebd7cea76e301ec0f68a5a55d68,Nick Richmond
unifiedStorageSearchUI,2024-12-19T18:21:48Z,,a8f347144ddc16f2033fdeb4f3474e49239ba7ab,Scott Lepper
playlistsReconciler,2024-12-20T03:09:31Z,,24bf337c562dc9b9d8684cc9acb7ea171ea83414,Charandas
1 #name created deleted hash author
409 kubernetesCliDashboards 2024-12-13T22:55:43Z 2025-02-18T23:11:26Z 8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4 Stephanie Hingtgen
410 useV2DashboardsAPI 2024-12-17T21:17:09Z 2025-03-12T17:43:32Z 070f0e4457c5967102ef157197073dc2662f6fb8 Dominik Prokop
411 investigationsBackend 2024-12-18T08:31:03Z f46c07aba7b6faccd2ecafc83051d1410cacc867 Jackson Coelho
unifiedStorageSearchSprinkles 2024-12-18T17:00:54Z 4837585cab0fd84184a8c6f5d6891f442a2b95f1 owensmallwood
412 prometheusSpecialCharsInLabelValues 2024-12-18T21:31:08Z 721c50a304588ebd7cea76e301ec0f68a5a55d68 Nick Richmond
413 unifiedStorageSearchUI 2024-12-19T18:21:48Z a8f347144ddc16f2033fdeb4f3474e49239ba7ab Scott Lepper
414 playlistsReconciler 2024-12-20T03:09:31Z 24bf337c562dc9b9d8684cc9acb7ea171ea83414 Charandas
-3
View File
@@ -142,14 +142,12 @@ vizActionsAuth,preview,@grafana/dataviz-squad,false,false,true
alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true
exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true
exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true
exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true
appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false
groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false
alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true
improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false
useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false
managedDualWriter,experimental,@grafana/search-and-storage,false,false,false
pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
@@ -159,7 +157,6 @@ newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
enableSCIM,preview,@grafana/identity-access-team,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
142 alertingPrometheusRulesPrimary experimental @grafana/alerting-squad false false true
143 exploreLogsShardSplitting experimental @grafana/observability-logs false false true
144 exploreLogsAggregatedMetrics experimental @grafana/observability-logs false false true
exploreLogsLimitedTimeRange experimental @grafana/observability-logs false false true
145 appPlatformGrpcClientAuth experimental @grafana/identity-access-team false false false
146 groupAttributeSync privatePreview @grafana/identity-access-team false false false
147 alertingQueryAndExpressionsStepMode GA @grafana/alerting-squad false false true
148 improvedExternalSessionHandling GA @grafana/identity-access-team false false false
149 useSessionStorageForRedirection GA @grafana/identity-access-team false false false
150 rolePickerDrawer experimental @grafana/identity-access-team false false false
unifiedStorageSearchSprinkles experimental @grafana/search-and-storage false false false
151 managedDualWriter experimental @grafana/search-and-storage false false false
152 pluginsSriChecks GA @grafana/plugins-platform-backend false false false
153 unifiedStorageBigObjectsSupport experimental @grafana/search-and-storage false false false
157 azureMonitorDisableLogLimit GA @grafana/partner-datasources false false false
158 playlistsReconciler experimental @grafana/grafana-app-platform-squad false true false
159 passwordlessMagicLinkAuthentication experimental @grafana/identity-access-team false false false
exploreMetricsRelatedLogs experimental @grafana/observability-metrics false false true
160 prometheusSpecialCharsInLabelValues experimental @grafana/oss-big-tent false false true
161 enableExtensionsAdminPage experimental @grafana/plugins-platform-backend false true false
162 enableSCIM preview @grafana/identity-access-team false false false
-4
View File
@@ -455,10 +455,6 @@ const (
// Enables the new role picker drawer design
FlagRolePickerDrawer = "rolePickerDrawer"
// FlagUnifiedStorageSearchSprinkles
// Enable sprinkles on unified storage search
FlagUnifiedStorageSearchSprinkles = "unifiedStorageSearchSprinkles"
// FlagManagedDualWriter
// Pick the dual write mode from database configs
FlagManagedDualWriter = "managedDualWriter"

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