Compare commits

..

20 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
224 changed files with 7800 additions and 3090 deletions

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

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

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=

View File

@@ -46,7 +46,7 @@
"x": 0,
"y": 0
},
"id": 23,
"id": 1,
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
@@ -77,7 +77,7 @@
"x": 0,
"y": 0
},
"id": 24,
"id": 23,
"panels": [],
"targets": [
{

View File

@@ -31,6 +31,53 @@
"cursorSync": "Off",
"editable": false,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"id": 1,
"title": "Application Monitoring",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "prometheus",
"spec": {}
},
"datasource": {
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
},
"fieldConfig": {
"defaults": {},
"overrides": []
}
}
}
}
},
"panel-10": {
"kind": "Panel",
"spec": {
@@ -977,53 +1024,6 @@
}
}
},
"panel-23": {
"kind": "Panel",
"spec": {
"id": 23,
"title": "Application Monitoring",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "prometheus",
"spec": {}
},
"datasource": {
"type": "prometheus",
"uid": "default-ds-uid"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "text",
"spec": {
"pluginVersion": "",
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
},
"fieldConfig": {
"defaults": {},
"overrides": []
}
}
}
}
},
"panel-6": {
"kind": "Panel",
"spec": {
@@ -1259,7 +1259,7 @@
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-23"
"name": "panel-1"
}
}
}

View File

@@ -32,6 +32,55 @@
"cursorSync": "Off",
"editable": false,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"id": 1,
"title": "Application Monitoring",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "default-ds-uid"
},
"spec": {}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "text",
"version": "",
"spec": {
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
},
"fieldConfig": {
"defaults": {},
"overrides": []
}
}
}
}
},
"panel-10": {
"kind": "Panel",
"spec": {
@@ -1018,55 +1067,6 @@
}
}
},
"panel-23": {
"kind": "Panel",
"spec": {
"id": 23,
"title": "Application Monitoring",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "default-ds-uid"
},
"spec": {}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "text",
"version": "",
"spec": {
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
},
"fieldConfig": {
"defaults": {},
"overrides": []
}
}
}
}
},
"panel-6": {
"kind": "Panel",
"spec": {
@@ -1310,7 +1310,7 @@
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-23"
"name": "panel-1"
}
}
}

View File

@@ -432,21 +432,6 @@ func getPanels(dashboard map[string]interface{}) []map[string]interface{} {
}
}
// Also get panels from rows
if rows, ok := dashboard["rows"].([]interface{}); ok {
for _, rowInterface := range rows {
if row, ok := rowInterface.(map[string]interface{}); ok {
if rowPanels, ok := row["panels"].([]interface{}); ok {
for _, panelInterface := range rowPanels {
if panel, ok := panelInterface.(map[string]interface{}); ok {
panels = append(panels, panel)
}
}
}
}
}
}
return panels
}

View File

@@ -46,8 +46,7 @@ func upgradeToGridLayout(dashboard map[string]interface{}) {
widthFactor := gridColumnCount / 12.0
// Find max panel ID (lines 1014-1021 in TS)
// Also check top-level panels which may have been assigned IDs by ensurePanelsHaveUniqueIds
maxPanelID := getMaxPanelID(dashboard, rows)
maxPanelID := getMaxPanelID(rows)
nextRowID := maxPanelID + 1
// Match frontend: dashboard.panels already exists with top-level panels
@@ -270,25 +269,10 @@ func (r *rowArea) getPanelPosition(panelHeight int, panelWidth int) map[string]i
return r.getPanelPosition(panelHeight, panelWidth)
}
func getMaxPanelID(dashboard map[string]interface{}, rows []interface{}) int {
func getMaxPanelID(rows []interface{}) int {
maxID := 0
hasValidID := false
// Check top-level panels first (these may have been assigned IDs by ensurePanelsHaveUniqueIds)
if panels, ok := dashboard["panels"].([]interface{}); ok {
for _, panelInterface := range panels {
if panel, ok := panelInterface.(map[string]interface{}); ok {
if id := GetIntValue(panel, "id", 0); id > 0 {
hasValidID = true
if id > maxID {
maxID = id
}
}
}
}
}
// Also check panels inside rows
for _, rowInterface := range rows {
if row, ok := rowInterface.(map[string]interface{}); ok {
if panels, ok := row["panels"].([]interface{}); ok {

View File

@@ -40,7 +40,7 @@
"x": 0,
"y": 0
},
"id": 23,
"id": 1,
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
@@ -71,7 +71,7 @@
"x": 0,
"y": 0
},
"id": 24,
"id": 23,
"panels": [],
"targets": [
{

View File

@@ -35,7 +35,7 @@
"x": 0,
"y": 0
},
"id": 23,
"id": 1,
"options": {
"content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n",
"mode": "markdown"
@@ -51,7 +51,7 @@
"x": 0,
"y": 0
},
"id": 24,
"id": 23,
"panels": [],
"title": "Application Service",
"type": "row"

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

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=

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

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/

View File

@@ -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

View File

@@ -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>

View File

@@ -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 });

View File

@@ -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');
}

View File

@@ -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();

View File

@@ -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 () => {

View File

@@ -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);

View File

@@ -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]]);

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;
}

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = false
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

View File

@@ -3,7 +3,6 @@
[feature_toggles]
unifiedStorageSearchUI = true
grafanaAPIServerWithExperimentalAPIs = true
unifiedStorageSearchSprinkles = true
[unified_storage]
enable_search = true

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

23
go.mod
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

23
go.sum
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=

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

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=

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",

View File

@@ -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,

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": {

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,
},

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

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 = {},

View File

@@ -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";

View File

@@ -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

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() {

View File

@@ -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
}

View File

@@ -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
)
);

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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';

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;

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]>;
}>;

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;

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';

View File

@@ -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'],
},
},
];

View File

@@ -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;

View File

@@ -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(),
];

View File

@@ -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';

View File

@@ -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

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"

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) {

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,
})
}

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
}

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

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) {

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",

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

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)
}

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,
},
}
}

View File

@@ -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}
}

View File

@@ -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

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
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

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"

View File

@@ -1382,7 +1382,8 @@
"metadata": {
"name": "exploreLogsLimitedTimeRange",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-08-29T13:55:59Z"
"creationTimestamp": "2024-08-29T13:55:59Z",
"deletionTimestamp": "2026-01-12T22:18:14Z"
},
"spec": {
"description": "Used in Logs Drilldown to limit the time range",
@@ -3722,19 +3723,6 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "unifiedStorageSearchSprinkles",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-12-18T17:00:54Z"
},
"spec": {
"description": "Enable sprinkles on unified storage search",
"stage": "experimental",
"codeowner": "@grafana/search-and-storage",
"hideFromDocs": true
}
},
{
"metadata": {
"name": "unifiedStorageSearchUI",

View File

@@ -190,9 +190,6 @@ func verifyFlagsConfiguration(t *testing.T) {
if flag.Stage == FeatureStageGeneralAvailability && flag.Expression == "" {
t.Errorf("GA features must be explicitly enabled or disabled, please add the `Expression` property for %s", flag.Name)
}
if flag.Expression != "" && flag.Expression != "true" && flag.Expression != "false" {
t.Errorf("the `Expression` property for %s is incorrect. valid values are: `true`, `false` or empty string for default", flag.Name)
}
// Check camel case names
if flag.Name != strcase.ToLowerCamel(flag.Name) && !legacyNames[flag.Name] {
invalidNames = append(invalidNames, flag.Name)

View File

@@ -0,0 +1,90 @@
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
type Colors struct {
Mode string `json:"mode"`
}
type ThemeDefinition struct {
Colors Colors `json:"colors"`
Id string `json:"id"`
}
func main() {
themesPath := filepath.Join("..", "..", "..", "packages", "grafana-data", "src", "themes", "themeDefinitions")
// Check if the themes directory exists
if _, err := os.Stat(themesPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Themes directory not found: %s\n", themesPath)
os.Exit(1)
}
output := `// Code generated by go generate; DO NOT EDIT.
package pref
var themes = []ThemeDTO{
{ID: "light", Type: "light"},
{ID: "dark", Type: "dark"},
{ID: "system", Type: "dark"},
`
err := filepath.WalkDir(themesPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
// Only process json files
if d.IsDir() || !strings.HasSuffix(d.Name(), ".json") {
return nil
}
fileBytes, readErr := os.ReadFile(path)
if readErr != nil {
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", path, readErr)
return nil // Continue processing other files
}
var themeDef ThemeDefinition
jsonErr := json.Unmarshal(fileBytes, &themeDef)
if jsonErr != nil {
fmt.Fprintf(os.Stderr, "Error parsing JSON from %s: %v\n", path, jsonErr)
return nil // Continue processing other files
}
themeId := themeDef.Id
themeType := "dark" // default fallback
if themeDef.Colors.Mode != "" {
themeType = themeDef.Colors.Mode
}
output += fmt.Sprintf("\t{ID: %q, Type: %q, IsExtra: true},\n", themeId, themeType)
return nil
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error walking themes directory: %v\n", err)
os.Exit(1)
}
output += "}\n"
// Write the generated file
outputPath := filepath.Join("themes_generated.go")
if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing output file: %v\n", err)
os.Exit(1)
}
fmt.Printf("Successfully generated themes_generated.go\n")
}

View File

@@ -1,3 +1,5 @@
//go:generate go run generate_themes.go
package pref
type ThemeDTO struct {
@@ -6,24 +8,6 @@ type ThemeDTO struct {
IsExtra bool `json:"isExtra"`
}
var themes = []ThemeDTO{
{ID: "light", Type: "light"},
{ID: "dark", Type: "dark"},
{ID: "system", Type: "dark"},
{ID: "debug", Type: "dark", IsExtra: true},
{ID: "aubergine", Type: "dark", IsExtra: true},
{ID: "desertbloom", Type: "light", IsExtra: true},
{ID: "gildedgrove", Type: "dark", IsExtra: true},
{ID: "mars", Type: "dark", IsExtra: true},
{ID: "matrix", Type: "dark", IsExtra: true},
{ID: "sapphiredusk", Type: "dark", IsExtra: true},
{ID: "synthwave", Type: "dark", IsExtra: true},
{ID: "tron", Type: "dark", IsExtra: true},
{ID: "victorian", Type: "dark", IsExtra: true},
{ID: "zen", Type: "light", IsExtra: true},
{ID: "gloom", Type: "dark", IsExtra: true},
}
func GetThemeByID(id string) *ThemeDTO {
for _, theme := range themes {
if theme.ID == id {

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