Compare commits
38 Commits
dd-test-cl
...
njvrzm/err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a61a2b63 | ||
|
|
dc4c106e91 | ||
|
|
33a1c60433 | ||
|
|
521670981a | ||
|
|
79ca4e5aec | ||
|
|
e3bc61e7d2 | ||
|
|
cc6a75d021 | ||
|
|
6d0f7f3567 | ||
|
|
913c0ba3c5 | ||
|
|
552b6aa717 | ||
|
|
2ddb4049c6 | ||
|
|
318a0ebb36 | ||
|
|
bba5c44dc4 | ||
|
|
44e6ea3d8b | ||
|
|
014d4758c6 | ||
|
|
82b4ce0ece | ||
|
|
52698cf0da | ||
|
|
d291dfb35b | ||
|
|
9c6feb8de5 | ||
|
|
e7625186af | ||
|
|
75b2c905cd | ||
|
|
45fc95cfc9 | ||
|
|
9c3cdd4814 | ||
|
|
2dad8b7b5b | ||
|
|
9a831ab4e1 | ||
|
|
759035a465 | ||
|
|
6e155523a3 | ||
|
|
5c0ee2d746 | ||
|
|
0c6b97bee2 | ||
|
|
4c79775b57 | ||
|
|
e088c9aac9 | ||
|
|
7182511bcf | ||
|
|
3023a72175 | ||
|
|
30ad61e0e9 | ||
|
|
0b58cd3900 | ||
|
|
4ba2fe6cce | ||
|
|
a345f78ae0 | ||
|
|
fa1e6cce5e |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -501,7 +501,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/various-suite/filter-annotations.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @grafana/plugins-platform-frontend
|
||||
/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @grafana/plugins-platform-frontend
|
||||
/e2e-playwright/various-suite/gauge.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/various-suite/inspect-drawer.spec.ts @grafana/dashboards-squad
|
||||
@@ -520,7 +519,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/various-suite/solo-route.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/various-suite/trace-view-scrolling.spec.ts @grafana/observability-traces-and-profiling
|
||||
/e2e-playwright/various-suite/verify-i18n.spec.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/various-suite/visualization-suggestions.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/various-suite/visualization-suggestions*.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/various-suite/perf-test.spec.ts @grafana/grafana-frontend-platform
|
||||
|
||||
# Packages
|
||||
|
||||
@@ -157,7 +157,7 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/google/wire v0.7.0 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
|
||||
@@ -619,8 +619,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
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-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
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/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=
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
|
||||
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
|
||||
|
||||
@@ -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-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
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/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=
|
||||
|
||||
@@ -180,12 +180,15 @@ func countAnnotationsV0V1(spec map[string]interface{}) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
annotationList, ok := annotations["list"].([]interface{})
|
||||
if !ok {
|
||||
return 0
|
||||
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
|
||||
if annotationList, ok := annotations["list"].([]interface{}); ok {
|
||||
return len(annotationList)
|
||||
}
|
||||
if annotationList, ok := annotations["list"].([]map[string]interface{}); ok {
|
||||
return len(annotationList)
|
||||
}
|
||||
|
||||
return len(annotationList)
|
||||
return 0
|
||||
}
|
||||
|
||||
// countLinksV0V1 counts dashboard links in v0alpha1 or v1beta1 dashboard spec
|
||||
@@ -194,12 +197,15 @@ func countLinksV0V1(spec map[string]interface{}) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
links, ok := spec["links"].([]interface{})
|
||||
if !ok {
|
||||
return 0
|
||||
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
|
||||
if links, ok := spec["links"].([]interface{}); ok {
|
||||
return len(links)
|
||||
}
|
||||
if links, ok := spec["links"].([]map[string]interface{}); ok {
|
||||
return len(links)
|
||||
}
|
||||
|
||||
return len(links)
|
||||
return 0
|
||||
}
|
||||
|
||||
// countVariablesV0V1 counts template variables in v0alpha1 or v1beta1 dashboard spec
|
||||
@@ -213,12 +219,15 @@ func countVariablesV0V1(spec map[string]interface{}) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
variableList, ok := templating["list"].([]interface{})
|
||||
if !ok {
|
||||
return 0
|
||||
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
|
||||
if variableList, ok := templating["list"].([]interface{}); ok {
|
||||
return len(variableList)
|
||||
}
|
||||
if variableList, ok := templating["list"].([]map[string]interface{}); ok {
|
||||
return len(variableList)
|
||||
}
|
||||
|
||||
return len(variableList)
|
||||
return 0
|
||||
}
|
||||
|
||||
// collectStatsV0V1 collects statistics from v0alpha1 or v1beta1 dashboard
|
||||
|
||||
142
apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.bom-in-links.json
vendored
Normal file
142
apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.bom-in-links.json
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v1beta1",
|
||||
"metadata": {
|
||||
"name": "bom-in-links-test",
|
||||
"namespace": "org-1",
|
||||
"labels": {
|
||||
"test": "bom-stripping"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "BOM Stripping Test Dashboard",
|
||||
"description": "Testing that BOM characters are stripped from URLs during conversion",
|
||||
"schemaVersion": 42,
|
||||
"tags": ["test", "bom"],
|
||||
"editable": true,
|
||||
"links": [
|
||||
{
|
||||
"title": "Dashboard link with BOM",
|
||||
"type": "link",
|
||||
"url": "http://example.com?var=${datasource}&other=value",
|
||||
"targetBlank": true,
|
||||
"icon": "external link"
|
||||
}
|
||||
],
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "table",
|
||||
"title": "Panel with BOM in field config override links",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "green"},
|
||||
{"color": "red", "value": 80}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "server"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "Override link with BOM",
|
||||
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}&var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"title": "Panel data link with BOM",
|
||||
"url": "http://example.com/${__data.fields.cluster}&var=value",
|
||||
"targetBlank": true
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "timeseries",
|
||||
"title": "Panel with BOM in options dataLinks",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"showLegend": true,
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"dataLinks": [
|
||||
{
|
||||
"title": "Options data link with BOM",
|
||||
"url": "http://example.com?series=${__series.name}&time=${__value.time}",
|
||||
"targetBlank": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"links": [
|
||||
{
|
||||
"title": "Field config default link with BOM",
|
||||
"url": "http://example.com?field=${__field.name}&value=${__value.raw}",
|
||||
"targetBlank": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"value": [
|
||||
{
|
||||
"title": "filter",
|
||||
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"value": [
|
||||
{
|
||||
"title": "filter",
|
||||
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2051,4 +2051,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2691,4 +2691,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2764,4 +2764,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1173,4 +1173,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1618,4 +1618,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1670,4 +1670,4 @@
|
||||
"storedVersion": "v0alpha1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v0alpha1.json
vendored
Normal file
161
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v0alpha1.json
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"name": "bom-in-links-test",
|
||||
"namespace": "org-1",
|
||||
"labels": {
|
||||
"test": "bom-stripping"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Testing that BOM characters are stripped from URLs during conversion",
|
||||
"editable": true,
|
||||
"links": [
|
||||
{
|
||||
"icon": "external link",
|
||||
"targetBlank": true,
|
||||
"title": "Dashboard link with BOM",
|
||||
"type": "link",
|
||||
"url": "http://example.com?var=${datasource}\u0026other=value"
|
||||
}
|
||||
],
|
||||
"panels": [
|
||||
{
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "server"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "Override link with BOM",
|
||||
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Panel data link with BOM",
|
||||
"url": "http://example.com/${__data.fields.cluster}\u0026var=value"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with BOM in field config override links",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Field config default link with BOM",
|
||||
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"dataLinks": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Options data link with BOM",
|
||||
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
|
||||
}
|
||||
],
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with BOM in options dataLinks",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 42,
|
||||
"tags": [
|
||||
"test",
|
||||
"bom"
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m"
|
||||
]
|
||||
},
|
||||
"title": "BOM Stripping Test Dashboard"
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
242
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2alpha1.json
vendored
Normal file
242
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2alpha1.json
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2alpha1",
|
||||
"metadata": {
|
||||
"name": "bom-in-links-test",
|
||||
"namespace": "org-1",
|
||||
"labels": {
|
||||
"test": "bom-stripping"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [],
|
||||
"cursorSync": "Off",
|
||||
"description": "Testing that BOM characters are stripped from URLs during conversion",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 1,
|
||||
"title": "Panel with BOM in field config override links",
|
||||
"description": "",
|
||||
"links": [
|
||||
{
|
||||
"title": "Panel data link with BOM",
|
||||
"url": "http://example.com/${__data.fields.cluster}\u0026var=value",
|
||||
"targetBlank": true
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "prometheus",
|
||||
"spec": {}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "table",
|
||||
"spec": {
|
||||
"pluginVersion": "",
|
||||
"options": {},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": null,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "server"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "Override link with BOM",
|
||||
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 2,
|
||||
"title": "Panel with BOM in options dataLinks",
|
||||
"description": "",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "prometheus",
|
||||
"spec": {}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "test-ds"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "timeseries",
|
||||
"spec": {
|
||||
"pluginVersion": "",
|
||||
"options": {
|
||||
"dataLinks": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Options data link with BOM",
|
||||
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
|
||||
}
|
||||
],
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Field config default link with BOM",
|
||||
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"title": "Dashboard link with BOM",
|
||||
"type": "link",
|
||||
"icon": "external link",
|
||||
"tooltip": "",
|
||||
"url": "http://example.com?var=${datasource}\u0026other=value",
|
||||
"tags": [],
|
||||
"asDropdown": false,
|
||||
"targetBlank": true,
|
||||
"includeVars": false,
|
||||
"keepTime": false
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [
|
||||
"test",
|
||||
"bom"
|
||||
],
|
||||
"timeSettings": {
|
||||
"timezone": "browser",
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "BOM Stripping Test Dashboard",
|
||||
"variables": []
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
246
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2beta1.json
vendored
Normal file
246
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.bom-in-links.v2beta1.json
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2beta1",
|
||||
"metadata": {
|
||||
"name": "bom-in-links-test",
|
||||
"namespace": "org-1",
|
||||
"labels": {
|
||||
"test": "bom-stripping"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [],
|
||||
"cursorSync": "Off",
|
||||
"description": "Testing that BOM characters are stripped from URLs during conversion",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 1,
|
||||
"title": "Panel with BOM in field config override links",
|
||||
"description": "",
|
||||
"links": [
|
||||
{
|
||||
"title": "Panel data link with BOM",
|
||||
"url": "http://example.com/${__data.fields.cluster}\u0026var=value",
|
||||
"targetBlank": true
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "prometheus",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "test-ds"
|
||||
},
|
||||
"spec": {}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "table",
|
||||
"version": "",
|
||||
"spec": {
|
||||
"options": {},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": null,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "server"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "links",
|
||||
"value": [
|
||||
{
|
||||
"title": "Override link with BOM",
|
||||
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 2,
|
||||
"title": "Panel with BOM in options dataLinks",
|
||||
"description": "",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "prometheus",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "test-ds"
|
||||
},
|
||||
"spec": {}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "timeseries",
|
||||
"version": "",
|
||||
"spec": {
|
||||
"options": {
|
||||
"dataLinks": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Options data link with BOM",
|
||||
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
|
||||
}
|
||||
],
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Field config default link with BOM",
|
||||
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"title": "Dashboard link with BOM",
|
||||
"type": "link",
|
||||
"icon": "external link",
|
||||
"tooltip": "",
|
||||
"url": "http://example.com?var=${datasource}\u0026other=value",
|
||||
"tags": [],
|
||||
"asDropdown": false,
|
||||
"targetBlank": true,
|
||||
"includeVars": false,
|
||||
"keepTime": false
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [
|
||||
"test",
|
||||
"bom"
|
||||
],
|
||||
"timeSettings": {
|
||||
"timezone": "browser",
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "BOM Stripping Test Dashboard",
|
||||
"variables": []
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,36 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// stripBOM removes Byte Order Mark (BOM) characters from a string.
|
||||
// BOMs (U+FEFF) can be introduced through copy/paste from certain editors
|
||||
// and cause CUE validation errors ("illegal byte order mark").
|
||||
func stripBOM(s string) string {
|
||||
return strings.ReplaceAll(s, "\ufeff", "")
|
||||
}
|
||||
|
||||
// stripBOMFromInterface recursively strips BOM characters from all strings
|
||||
// in an interface{} value (map, slice, or string).
|
||||
func stripBOMFromInterface(v interface{}) interface{} {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return stripBOM(val)
|
||||
case map[string]interface{}:
|
||||
result := make(map[string]interface{}, len(val))
|
||||
for k, v := range val {
|
||||
result[k] = stripBOMFromInterface(v)
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
result := make([]interface{}, len(val))
|
||||
for i, item := range val {
|
||||
result[i] = stripBOMFromInterface(item)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
|
||||
if val, ok := m[key]; ok {
|
||||
if str, ok := val.(string); ok && str != "" {
|
||||
@@ -393,7 +423,8 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa
|
||||
// Optional field - only set if present
|
||||
if url, exists := linkMap["url"]; exists {
|
||||
if urlStr, ok := url.(string); ok {
|
||||
dashLink.Url = &urlStr
|
||||
cleanUrl := stripBOM(urlStr)
|
||||
dashLink.Url = &cleanUrl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2239,7 +2270,7 @@ func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.Dashboar
|
||||
if linkMap, ok := link.(map[string]interface{}); ok {
|
||||
dataLink := dashv2alpha1.DashboardDataLink{
|
||||
Title: schemaversion.GetStringValue(linkMap, "title"),
|
||||
Url: schemaversion.GetStringValue(linkMap, "url"),
|
||||
Url: stripBOM(schemaversion.GetStringValue(linkMap, "url")),
|
||||
}
|
||||
if _, exists := linkMap["targetBlank"]; exists {
|
||||
targetBlank := getBoolField(linkMap, "targetBlank", false)
|
||||
@@ -2331,6 +2362,12 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo
|
||||
}
|
||||
}
|
||||
|
||||
// Strip BOMs from options (may contain dataLinks with URLs that have BOMs)
|
||||
cleanedOptions := stripBOMFromInterface(options)
|
||||
if cleanedMap, ok := cleanedOptions.(map[string]interface{}); ok {
|
||||
options = cleanedMap
|
||||
}
|
||||
|
||||
// Build field config by mapping each field individually
|
||||
fieldConfigSource := extractFieldConfigSource(fieldConfig)
|
||||
|
||||
@@ -2474,9 +2511,14 @@ func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.Da
|
||||
hasDefaults = true
|
||||
}
|
||||
|
||||
// Extract array field
|
||||
// Extract array field - strip BOMs from link URLs
|
||||
if linksArray, ok := extractArrayField(defaults, "links"); ok {
|
||||
fieldConfigDefaults.Links = linksArray
|
||||
cleanedLinks := stripBOMFromInterface(linksArray)
|
||||
if cleanedArray, ok := cleanedLinks.([]interface{}); ok {
|
||||
fieldConfigDefaults.Links = cleanedArray
|
||||
} else {
|
||||
fieldConfigDefaults.Links = linksArray
|
||||
}
|
||||
hasDefaults = true
|
||||
}
|
||||
|
||||
@@ -2762,9 +2804,11 @@ func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alp
|
||||
fieldOverride.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, 0, len(propertiesArray))
|
||||
for _, property := range propertiesArray {
|
||||
if propertyMap, ok := property.(map[string]interface{}); ok {
|
||||
// Strip BOMs from property values (may contain links with URLs)
|
||||
cleanedValue := stripBOMFromInterface(propertyMap["value"])
|
||||
fieldOverride.Properties = append(fieldOverride.Properties, dashv2alpha1.DashboardDynamicConfigValue{
|
||||
Id: schemaversion.GetStringValue(propertyMap, "id"),
|
||||
Value: propertyMap["value"],
|
||||
Value: cleanedValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,20 @@
|
||||
}
|
||||
],
|
||||
"title": "Only nulls and no user set min \u0026 max",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "convertFieldType",
|
||||
"options": {
|
||||
"conversions": [
|
||||
{
|
||||
"destinationType": "number",
|
||||
"targetField": "A-series"
|
||||
}
|
||||
],
|
||||
"fields": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
@@ -1179,4 +1193,4 @@
|
||||
"title": "Panel Tests - Gauge",
|
||||
"uid": "_5rDmaQiz",
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1760,6 +1760,22 @@
|
||||
"startValue": 0
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"mode": "unary",
|
||||
"reduce": {
|
||||
"reducer": "sum"
|
||||
},
|
||||
"replaceFields": true,
|
||||
"unary": {
|
||||
"operator": "round",
|
||||
"fieldName": "A-series"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Active gateways",
|
||||
"type": "radialbar"
|
||||
},
|
||||
@@ -1843,6 +1859,22 @@
|
||||
"startValue": 0
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"mode": "unary",
|
||||
"reduce": {
|
||||
"reducer": "sum"
|
||||
},
|
||||
"replaceFields": true,
|
||||
"unary": {
|
||||
"operator": "round",
|
||||
"fieldName": "A-series"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Active pods",
|
||||
"type": "radialbar"
|
||||
},
|
||||
|
||||
@@ -485,6 +485,7 @@
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"displayName": "My gauge",
|
||||
"minVizHeight": 75,
|
||||
"minVizWidth": 75,
|
||||
"orientation": "auto",
|
||||
|
||||
@@ -223,7 +223,7 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // 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
|
||||
|
||||
@@ -827,8 +827,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
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-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
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/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=
|
||||
|
||||
20
apps/plugins/README.md
Normal file
20
apps/plugins/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Plugins App
|
||||
|
||||
API documentation is available at http://localhost:3000/swagger?api=plugins.grafana.app-v0alpha1
|
||||
|
||||
## Codegen
|
||||
|
||||
- Go: `make generate`
|
||||
- Frontend: Follow instructions in this [README](../..//packages/grafana-api-clients/README.md)
|
||||
|
||||
## Plugin sync
|
||||
|
||||
The plugin sync pushes the plugins loaded from disk to the plugins API.
|
||||
|
||||
To enable, add these feature toggles in your `custom.ini`:
|
||||
|
||||
```ini
|
||||
[feature_toggles]
|
||||
pluginInstallAPISync = true
|
||||
pluginStoreServiceLoading = true
|
||||
```
|
||||
@@ -90,7 +90,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-20251223160021-926c74910196 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // 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
|
||||
|
||||
@@ -213,8 +213,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-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
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/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=
|
||||
|
||||
@@ -600,6 +600,20 @@
|
||||
"stringInput": "null,null"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "convertFieldType",
|
||||
"options": {
|
||||
"fields": {},
|
||||
"conversions": [
|
||||
{
|
||||
"targetField": "A-series",
|
||||
"destinationType": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Only nulls and no user set min & max",
|
||||
"type": "gauge"
|
||||
},
|
||||
|
||||
@@ -1718,6 +1718,22 @@
|
||||
"startValue": 0
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"mode": "unary",
|
||||
"reduce": {
|
||||
"reducer": "sum"
|
||||
},
|
||||
"replaceFields": true,
|
||||
"unary": {
|
||||
"operator": "round",
|
||||
"fieldName": "A-series"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Active gateways",
|
||||
"type": "radialbar"
|
||||
},
|
||||
@@ -1799,6 +1815,22 @@
|
||||
"startValue": 0
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "calculateField",
|
||||
"options": {
|
||||
"mode": "unary",
|
||||
"reduce": {
|
||||
"reducer": "sum"
|
||||
},
|
||||
"replaceFields": true,
|
||||
"unary": {
|
||||
"operator": "round",
|
||||
"fieldName": "A-series"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Active pods",
|
||||
"type": "radialbar"
|
||||
},
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"displayName": "My gauge",
|
||||
"minVizHeight": 75,
|
||||
"minVizWidth": 75,
|
||||
"orientation": "auto",
|
||||
|
||||
@@ -134,7 +134,7 @@ To convert data source-managed alert rules to Grafana managed alerts:
|
||||
|
||||
Pausing stops alert rule evaluation behavior for the newly created Grafana-managed alert rules.
|
||||
|
||||
9. (Optional) In the **Target data source** of the **Recording rules** section, you can select the data source that the imported recording rules will query. By default, it is the data source selected in the **Data source** dropdown.
|
||||
9. (Optional) In the **Target data source** of the **Recording rules** section, you can select the data source to which the imported recording rules will write metrics. By default, it is the data source selected in the **Data source** dropdown.
|
||||
|
||||
10. Click **Import**.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ You can share dashboards in the following ways:
|
||||
- [As a report](#schedule-a-report)
|
||||
- [As a snapshot](#share-a-snapshot)
|
||||
- [As a PDF export](#export-a-dashboard-as-pdf)
|
||||
- [As a JSON file export](#export-a-dashboard-as-json)
|
||||
- [As a JSON file export](#export-a-dashboard-as-code)
|
||||
- [As an image export](#export-a-dashboard-as-an-image)
|
||||
|
||||
When you share a dashboard externally as a link or by email, those dashboards are included in a list of your shared dashboards. To view the list and manage these dashboards, navigate to **Dashboards > Shared dashboards**.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json';
|
||||
|
||||
import { test, expect } from './fixtures';
|
||||
import {
|
||||
checkRepeatedPanelTitles,
|
||||
verifyChanges,
|
||||
movePanel,
|
||||
getPanelPosition,
|
||||
saveDashboard,
|
||||
importTestDashboard,
|
||||
goToEmbeddedPanel,
|
||||
} from './utils';
|
||||
|
||||
@@ -32,8 +34,8 @@ test.describe(
|
||||
tag: ['@dashboards'],
|
||||
},
|
||||
() => {
|
||||
test('can enable repeats', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - add repeats');
|
||||
test('can enable repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(page, selectors, 'Custom grid repeats - add repeats');
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
@@ -60,8 +62,13 @@ test.describe(
|
||||
await checkRepeatedPanelTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions);
|
||||
});
|
||||
|
||||
test('can update repeats with variable change', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - update on variable change', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can update repeats with variable change', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - update on variable change',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(
|
||||
@@ -87,8 +94,13 @@ test.describe(
|
||||
)
|
||||
).toBeHidden();
|
||||
});
|
||||
test('can update repeats in edit pane', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - update through edit pane', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can update repeats in edit pane', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - update through edit pane',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
@@ -110,8 +122,13 @@ test.describe(
|
||||
await checkRepeatedPanelTitles(dashboardPage, selectors, newTitleBase, repeatOptions);
|
||||
});
|
||||
|
||||
test('can update repeats in panel editor', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - update through panel editor', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can update repeats in panel editor', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - update through panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
@@ -164,13 +181,10 @@ test.describe(
|
||||
await checkRepeatedPanelTitles(dashboardPage, selectors, newTitleBase, repeatOptions);
|
||||
});
|
||||
|
||||
test('can update repeats in panel editor when loaded directly', async ({
|
||||
dashboardPage,
|
||||
selectors,
|
||||
page,
|
||||
importDashboard,
|
||||
}) => {
|
||||
await importDashboard(
|
||||
test('can update repeats in panel editor when loaded directly', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - update through directly loaded panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
@@ -218,8 +232,13 @@ test.describe(
|
||||
|
||||
await checkRepeatedPanelTitles(dashboardPage, selectors, newTitleBase, repeatOptions);
|
||||
});
|
||||
test('can move repeated panels', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - move repeated panels', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can move repeated panels', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - move repeated panels',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
@@ -257,8 +276,13 @@ test.describe(
|
||||
`${repeatTitleBase}${repeatOptions.at(-1)}`
|
||||
);
|
||||
});
|
||||
test('can view repeated panel', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - move repeated panels', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can view repeated panel', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - move repeated panels',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(-1)}`))
|
||||
@@ -308,8 +332,10 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can view embedded repeated panel', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard(
|
||||
test('can view embedded repeated panel', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - view embedded repeated panel',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
@@ -327,8 +353,13 @@ test.describe(
|
||||
)
|
||||
).toBeVisible();
|
||||
});
|
||||
test('can remove repeats', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Custom grid repeats - remove repeats', JSON.stringify(testV2DashWithRepeats));
|
||||
test('can remove repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - remove repeats',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
// verify 6 panels are present (4 repeats and 2 normal)
|
||||
expect(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import V2DashWithTabRepeats from '../dashboards/V2DashWithTabRepeats.json';
|
||||
|
||||
import { test, expect } from './fixtures';
|
||||
import {
|
||||
verifyChanges,
|
||||
saveDashboard,
|
||||
importTestDashboard,
|
||||
goToEmbeddedPanel,
|
||||
checkRepeatedTabTitles,
|
||||
groupIntoTab,
|
||||
@@ -33,8 +35,8 @@ test.describe(
|
||||
tag: ['@dashboards'],
|
||||
},
|
||||
() => {
|
||||
test('can enable tab repeats', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - add repeats');
|
||||
test('can enable tab repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(page, selectors, 'Tabs layout repeats - add repeats');
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
@@ -67,8 +69,13 @@ test.describe(
|
||||
await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions);
|
||||
});
|
||||
|
||||
test('can update tab repeats with variable change', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - update on variable change', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can update tab repeats with variable change', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - update on variable change',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
|
||||
const c1Var = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('c1'));
|
||||
await c1Var
|
||||
@@ -90,8 +97,13 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(-1)}`))
|
||||
).toBeHidden();
|
||||
});
|
||||
test('can update repeats in edit pane', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - update through edit pane', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can update repeats in edit pane', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - update through edit pane',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
// select first/original repeat tab to activate edit pane
|
||||
@@ -113,8 +125,10 @@ test.describe(
|
||||
await checkRepeatedTabTitles(dashboardPage, selectors, newTitleBase, repeatOptions);
|
||||
});
|
||||
|
||||
test('can update repeats after panel change', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard(
|
||||
test('can update repeats after panel change', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - update repeats after panel change',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
@@ -151,13 +165,10 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can update repeats after panel change in editor', async ({
|
||||
dashboardPage,
|
||||
selectors,
|
||||
page,
|
||||
importDashboard,
|
||||
}) => {
|
||||
await importDashboard(
|
||||
test('can update repeats after panel change in editor', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - update repeats after panel change in editor',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
@@ -214,13 +225,10 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can hide canvas grid add row action in repeats', async ({
|
||||
dashboardPage,
|
||||
selectors,
|
||||
page,
|
||||
importDashboard,
|
||||
}) => {
|
||||
await importDashboard(
|
||||
test('can hide canvas grid add row action in repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - hide canvas add action in repeats',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
@@ -236,8 +244,13 @@ test.describe(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)).toBeHidden();
|
||||
});
|
||||
|
||||
test('can move repeated tabs', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - move repeated tabs', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can move repeated tabs', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - move repeated tabs',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await moveTab(dashboardPage, page, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`, 'New tab');
|
||||
|
||||
@@ -256,8 +269,13 @@ test.describe(
|
||||
expect(normalTab2?.x).toBeLessThan(repeatedTab2?.x || 0);
|
||||
});
|
||||
|
||||
test('can load into repeated tab', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - can load into repeated tab', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can load into repeated tab', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - can load into repeated tab',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`))
|
||||
@@ -274,8 +292,13 @@ test.describe(
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
test('can view panels in repeated tab', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - view panels in repeated tabs', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can view panels in repeated tab', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - view panels in repeated tabs',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
|
||||
// non repeated panel in repeated tab
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().hover();
|
||||
@@ -344,8 +367,10 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can view embedded panels in repeated tab', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard(
|
||||
test('can view embedded panels in repeated tab', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - view embedded panels in repeated tabs',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
@@ -392,8 +417,13 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('can remove repeats', async ({ dashboardPage, selectors, page, importDashboard }) => {
|
||||
await importDashboard('Tabs layout repeats - remove repeats', JSON.stringify(V2DashWithTabRepeats));
|
||||
test('can remove repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Tabs layout repeats - remove repeats',
|
||||
JSON.stringify(V2DashWithTabRepeats)
|
||||
);
|
||||
|
||||
// verify 5 tabs are present (4 repeats and 1 normal)
|
||||
await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { test as base } from '@grafana/plugin-e2e';
|
||||
|
||||
import { importTestDashboard } from './utils';
|
||||
|
||||
type ImportDashboardFn = (title: string, dashJSON?: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Extended test fixtures for dashboard-new-layouts tests.
|
||||
* Provides `importDashboard` - a wrapped version of `importTestDashboard` that
|
||||
* automatically cleans up dashboards after each test.
|
||||
*/
|
||||
export const test = base.extend<{ importDashboard: ImportDashboardFn }>({
|
||||
// imports dashboard and cleans it up after the test
|
||||
importDashboard: async ({ page, selectors, request }, use) => {
|
||||
const importedUIDs: string[] = [];
|
||||
|
||||
const importDashboard: ImportDashboardFn = async (title, dashJSON) => {
|
||||
const uid = await importTestDashboard(page, selectors, title, dashJSON);
|
||||
importedUIDs.push(uid);
|
||||
return uid;
|
||||
};
|
||||
|
||||
await use(importDashboard);
|
||||
|
||||
for (const uid of importedUIDs) {
|
||||
await request.delete(`/api/dashboards/uid/${uid}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@grafana/plugin-e2e';
|
||||
@@ -160,12 +160,7 @@ export async function verifyChanges(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click();
|
||||
}
|
||||
|
||||
export async function importTestDashboard(
|
||||
page: Page,
|
||||
selectors: E2ESelectorGroups,
|
||||
title: string,
|
||||
dashInput?: string
|
||||
): Promise<string> {
|
||||
export async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string, dashInput?: string) {
|
||||
await page.goto(selectors.pages.ImportDashboard.url);
|
||||
await page
|
||||
.getByTestId(selectors.components.DashboardImportPage.textarea)
|
||||
@@ -182,15 +177,6 @@ export async function importTestDashboard(
|
||||
}
|
||||
|
||||
await expect(page.locator('[data-testid="uplot-main-div"]').first()).toBeVisible();
|
||||
|
||||
if (testV2Dashboard.metadata.uid) {
|
||||
return testV2Dashboard.metadata.uid;
|
||||
}
|
||||
// else extract from url
|
||||
const url = new URL(page.url());
|
||||
const pathParts = url.pathname.split('/');
|
||||
const dIndex = pathParts.indexOf('d');
|
||||
return dIndex !== -1 ? pathParts[dIndex + 1] : '';
|
||||
}
|
||||
|
||||
export async function goToEmbeddedPanel(page: Page) {
|
||||
|
||||
@@ -10,7 +10,7 @@ const NUM_NESTED_DASHBOARDS = 60;
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import testDashboard from '../dashboards/TestDashboard.json';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ test.use({
|
||||
scenes: true,
|
||||
sharingDashboardImage: true, // Enable the export image feature
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import testDashboard from '../dashboards/DashboardLiveTest.json';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ test.use({
|
||||
timezoneId: 'Pacific/Easter',
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const TIMEZONE_DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test.use({
|
||||
},
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import testDashboard from '../dashboards/TestDashboard.json';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ async function assertPreviewValues(
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ async function assertPreviewValues(
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Templating - Nested Template Variables';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const DASHBOARD_UID = 'ZqZnVvFZz';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ const DASHBOARD_UID = 'yBCC3aKGk';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const PAGE_UNDER_TEST = 'AejrN1AMz';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardNewLayouts: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@ import { Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from './vizpicker-utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
canvasPanelPanZoom: true,
|
||||
},
|
||||
});
|
||||
test.describe('Canvas Panel - Scene Tests', () => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage, selectors }) => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage }) => {
|
||||
const dashboardPage = await gotoDashboardPage({});
|
||||
const panelEditPage = await dashboardPage.addPanel();
|
||||
await setVisualization(panelEditPage, 'Canvas', selectors);
|
||||
await panelEditPage.setVisualization('Canvas');
|
||||
|
||||
// Wait for canvas panel to load
|
||||
await page.waitForSelector('[data-testid="canvas-scene-pan-zoom"]', { timeout: 10000 });
|
||||
|
||||
101
e2e-playwright/panels-suite/gauge.spec.ts
Normal file
101
e2e-playwright/panels-suite/gauge.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
// this test requires a larger viewport so all gauge panels load properly
|
||||
test.use({
|
||||
featureToggles: { newGauge: true },
|
||||
viewport: { width: 1280, height: 3000 },
|
||||
});
|
||||
|
||||
const OLD_GAUGES_DASHBOARD_UID = '_5rDmaQiz';
|
||||
const NEW_GAUGES_DASHBOARD_UID = 'panel-tests-gauge-new';
|
||||
|
||||
test.describe(
|
||||
'Gauge Panel',
|
||||
{
|
||||
tag: ['@panels', '@gauge'],
|
||||
},
|
||||
() => {
|
||||
test('successfully migrates all gauge panels', async ({ gotoDashboardPage, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: OLD_GAUGES_DASHBOARD_UID });
|
||||
|
||||
// check that gauges are rendered
|
||||
const gaugeElements = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.Panels.Visualization.Gauge.Container
|
||||
);
|
||||
await expect(gaugeElements).toHaveCount(16);
|
||||
|
||||
// check that no panel errors exist
|
||||
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
|
||||
await expect(errorInfo).toBeHidden();
|
||||
});
|
||||
|
||||
test('renders new gauge panels', async ({ gotoDashboardPage, selectors }) => {
|
||||
// open Panel Tests - Gauge
|
||||
const dashboardPage = await gotoDashboardPage({ uid: NEW_GAUGES_DASHBOARD_UID });
|
||||
|
||||
// check that gauges are rendered
|
||||
const gaugeElements = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.Panels.Visualization.Gauge.Container
|
||||
);
|
||||
await expect(gaugeElements).toHaveCount(32);
|
||||
|
||||
// check that no panel errors exist
|
||||
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
|
||||
await expect(errorInfo).toBeHidden();
|
||||
});
|
||||
|
||||
test('renders sparklines in gauge panels', async ({ gotoDashboardPage, page }) => {
|
||||
await gotoDashboardPage({
|
||||
uid: NEW_GAUGES_DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '11' }),
|
||||
});
|
||||
|
||||
await expect(page.locator('.uplot')).toHaveCount(5);
|
||||
});
|
||||
|
||||
test('"no data"', async ({ gotoDashboardPage, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: NEW_GAUGES_DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '36' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
|
||||
'that the gauge does not appear'
|
||||
).toBeHidden();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage),
|
||||
'that the empty text appears'
|
||||
).toHaveText('No data');
|
||||
|
||||
// update the "No value" option and see if the panel updates
|
||||
const noValueOption = dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Standard options No value'))
|
||||
.locator('input');
|
||||
|
||||
await noValueOption.fill('My empty value');
|
||||
await noValueOption.blur();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
|
||||
'that the empty text shows up in an empty gauge'
|
||||
).toHaveText('My empty value');
|
||||
|
||||
// test the "no numeric fields" message on the next panel
|
||||
const dashboardPage2 = await gotoDashboardPage({
|
||||
uid: NEW_GAUGES_DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '37' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
|
||||
'that the gauge does not appear'
|
||||
).toBeHidden();
|
||||
|
||||
await expect(
|
||||
dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage),
|
||||
'that the empty text appears'
|
||||
).toHaveText('Data is missing a number field');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect, E2ESelectorGroups, PanelEditPage } from '@grafana/plugin-e2e';
|
||||
|
||||
// this replaces the panelEditPage.setVisualization method used previously in tests, since it
|
||||
// does not know how to use the updated 12.4 viz picker UI to set the visualization
|
||||
export const setVisualization = async (panelEditPage: PanelEditPage, vizName: string, selectors: E2ESelectorGroups) => {
|
||||
const vizPicker = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker);
|
||||
await expect(vizPicker, '"Change" button should be visible').toBeVisible();
|
||||
await vizPicker.click();
|
||||
|
||||
const allVizTabBtn = panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations'));
|
||||
await expect(allVizTabBtn, '"All visualiations" button should be visible').toBeVisible();
|
||||
await allVizTabBtn.click();
|
||||
|
||||
const vizItem = panelEditPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(vizName));
|
||||
await expect(vizItem, `"${vizName}" item should be visible`).toBeVisible();
|
||||
await vizItem.scrollIntoViewIfNeeded();
|
||||
await vizItem.click();
|
||||
|
||||
await expect(vizPicker, '"Change" button should be visible again').toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
'Panel header should have the new viz type name'
|
||||
).toHaveText(vizName);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
|
||||
@@ -25,10 +24,10 @@ test.describe(
|
||||
).toContainText(['Field', 'Max', 'Mean', 'Last']);
|
||||
});
|
||||
|
||||
test('table panel data assertions', async ({ panelEditPage, selectors }) => {
|
||||
test('table panel data assertions', async ({ panelEditPage }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, 'Table', selectors);
|
||||
await panelEditPage.setVisualization('Table');
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.locator,
|
||||
@@ -44,10 +43,10 @@ test.describe(
|
||||
).toContainText(['val1', 'val2', 'val3', 'val4']);
|
||||
});
|
||||
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage, selectors }) => {
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, 'Time series', selectors);
|
||||
await panelEditPage.setVisualization('Time series');
|
||||
await panelEditPage.refreshPanel();
|
||||
await panelEditPage.toggleTableView();
|
||||
await expect(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
import { scenarios } from '../mocks/resources';
|
||||
@@ -54,10 +53,10 @@ test.describe(
|
||||
).toHaveText(scenarios.map((s) => s.name));
|
||||
});
|
||||
|
||||
test('mocked query data response', async ({ panelEditPage, page, selectors }) => {
|
||||
test('mocked query data response', async ({ panelEditPage, page }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.getErrorIcon(),
|
||||
@@ -76,7 +75,7 @@ test.describe(
|
||||
selectors,
|
||||
page,
|
||||
}) => {
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
formatExpectError('Expected panel visualization to be set to table')
|
||||
@@ -93,8 +92,8 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = await panelEditPage.getCustomOptions('Axis');
|
||||
const timeZonePicker = axisOptions.getSelect('Time zone');
|
||||
|
||||
@@ -102,8 +101,8 @@ test.describe(
|
||||
await expect(timeZonePicker).toHaveSelected('Europe/Stockholm');
|
||||
});
|
||||
|
||||
test('select unit in unit picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('select unit in unit picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const unitPicker = standardOptions.getUnitPicker('Unit');
|
||||
|
||||
@@ -112,8 +111,8 @@ test.describe(
|
||||
await expect(unitPicker).toHaveSelected('Pixels');
|
||||
});
|
||||
|
||||
test('enter value in number input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in number input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const lineWith = axisOptions.getNumberInput('Soft min');
|
||||
|
||||
@@ -122,8 +121,8 @@ test.describe(
|
||||
await expect(lineWith).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('enter value in slider', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in slider', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const graphOptions = panelEditPage.getCustomOptions('Graph styles');
|
||||
const lineWidth = graphOptions.getSliderInput('Line width');
|
||||
|
||||
@@ -132,8 +131,8 @@ test.describe(
|
||||
await expect(lineWidth).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('select value in single value select', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('select value in single value select', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const colorSchemeSelect = standardOptions.getSelect('Color scheme');
|
||||
|
||||
@@ -141,8 +140,8 @@ test.describe(
|
||||
await expect(colorSchemeSelect).toHaveSelected('Classic palette');
|
||||
});
|
||||
|
||||
test('clear input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('clear input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const title = panelOptions.getTextInput('Title');
|
||||
|
||||
@@ -151,8 +150,8 @@ test.describe(
|
||||
await expect(title).toHaveValue('');
|
||||
});
|
||||
|
||||
test('enter value in input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const description = panelOptions.getTextInput('Description');
|
||||
|
||||
@@ -161,8 +160,8 @@ test.describe(
|
||||
await expect(description).toHaveValue('This is a panel');
|
||||
});
|
||||
|
||||
test('unchecking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('unchecking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -174,8 +173,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked({ checked: false });
|
||||
});
|
||||
|
||||
test('checking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('checking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -184,8 +183,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked();
|
||||
});
|
||||
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
@@ -196,8 +195,8 @@ test.describe(
|
||||
await expect(placement).toHaveChecked('Auto');
|
||||
});
|
||||
|
||||
test('selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BootData } from '@grafana/data';
|
||||
import { BootData, PanelPluginMeta } from '@grafana/data';
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.describe(
|
||||
@@ -22,7 +22,7 @@ test.describe(
|
||||
await dashboardPage.addPanel();
|
||||
|
||||
// Get panel types from window object
|
||||
const panelTypes = await page.evaluate(() => {
|
||||
const panelTypes: PanelPluginMeta[] = await page.evaluate(() => {
|
||||
// @grafana/plugin-e2e doesn't export the full bootdata config
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const win = window as typeof window & { grafanaBootData: BootData };
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
// this test requires a larger viewport so all gauge panels load properly
|
||||
test.use({
|
||||
viewport: { width: 1280, height: 1080 },
|
||||
});
|
||||
|
||||
test.describe(
|
||||
'Gauge Panel',
|
||||
{
|
||||
tag: ['@various'],
|
||||
},
|
||||
() => {
|
||||
test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
// open Panel Tests - Gauge
|
||||
const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' });
|
||||
|
||||
// check that gauges are rendered
|
||||
const gaugeElements = page.locator('.flot-base');
|
||||
await expect(gaugeElements).toHaveCount(16);
|
||||
|
||||
// check that no panel errors exist
|
||||
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
|
||||
await expect(errorInfo).toBeHidden();
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,178 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
newVizSuggestions: true,
|
||||
externalVizSuggestions: false,
|
||||
},
|
||||
viewport: {
|
||||
width: 800,
|
||||
height: 1500,
|
||||
},
|
||||
});
|
||||
|
||||
test.describe(
|
||||
'Visualization suggestions v2',
|
||||
{
|
||||
tag: ['@various', '@suggestions'],
|
||||
},
|
||||
() => {
|
||||
test('Should be shown and clickable', async ({ selectors, gotoPanelEditPage }) => {
|
||||
// Open dashboard with edit panel
|
||||
const panelEditPage = await gotoPanelEditPage({
|
||||
dashboard: {
|
||||
uid: 'aBXrJ0R7z',
|
||||
},
|
||||
id: '9',
|
||||
});
|
||||
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'),
|
||||
'time series to be rendered inside panel'
|
||||
).toBeVisible();
|
||||
|
||||
// Try visualization suggestions
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click();
|
||||
|
||||
// Verify we see suggestions
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')),
|
||||
'line chart suggestion to be rendered'
|
||||
).toBeVisible();
|
||||
|
||||
// TODO: in this part of the test, we will change the query and the transforms and observe suggestions being updated.
|
||||
|
||||
// Select a visualization and verify table header is visible from preview
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click();
|
||||
await expect(
|
||||
panelEditPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.content)
|
||||
.getByRole('grid')
|
||||
.getByRole('row')
|
||||
.first(),
|
||||
'table to be rendered inside panel'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton),
|
||||
'discard changes button disabled since panel has not yet changed'
|
||||
).toBeDisabled();
|
||||
|
||||
// apply the suggestion and verify panel options are visible
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.confirm('Table')).click();
|
||||
await expect(
|
||||
panelEditPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.content)
|
||||
.getByRole('grid')
|
||||
.getByRole('row')
|
||||
.first(),
|
||||
'table to be rendered inside panel'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
'options pane to be rendered'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton),
|
||||
'discard changes button enabled now that panel is dirty'
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should not apply suggestion if you navigate toggle the viz picker back off', async ({
|
||||
selectors,
|
||||
gotoPanelEditPage,
|
||||
}) => {
|
||||
// Open dashboard with edit panel
|
||||
const panelEditPage = await gotoPanelEditPage({
|
||||
dashboard: {
|
||||
uid: 'aBXrJ0R7z',
|
||||
},
|
||||
id: '9',
|
||||
});
|
||||
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'),
|
||||
'time series to be rendered inside panel;'
|
||||
).toBeVisible();
|
||||
|
||||
// Try visualization suggestions
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click();
|
||||
|
||||
// Verify we see suggestions
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')),
|
||||
'line chart suggestion to be rendered'
|
||||
).toBeVisible();
|
||||
|
||||
// Select a visualization
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click();
|
||||
await expect(
|
||||
panelEditPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.content)
|
||||
.getByRole('grid')
|
||||
.getByRole('row')
|
||||
.first(),
|
||||
'table to be rendered inside panel'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton)
|
||||
).toBeDisabled();
|
||||
|
||||
// Verify that toggling the viz picker back cancels the suggestion, restores the line chart, shows panel options
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).locator('.uplot'),
|
||||
'time series to be rendered inside panel'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
'options pane to be rendered'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton),
|
||||
'discard changes button is still disabled since no changes were applied'
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not apply suggestion if you navigate back to the dashboard', async ({
|
||||
page,
|
||||
selectors,
|
||||
gotoPanelEditPage,
|
||||
}) => {
|
||||
// Open dashboard with edit panel
|
||||
const panelEditPage = await gotoPanelEditPage({
|
||||
dashboard: {
|
||||
uid: 'aBXrJ0R7z',
|
||||
},
|
||||
id: '9',
|
||||
});
|
||||
|
||||
// Try visualization suggestions
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click();
|
||||
|
||||
// Verify we see suggestions
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Line chart')),
|
||||
'line chart suggestion to be rendered'
|
||||
).toBeVisible();
|
||||
|
||||
// Select a visualization
|
||||
await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click();
|
||||
await expect(page.getByRole('grid').getByRole('row').first(), 'table row to be rendered').toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.discardChangesButton)
|
||||
).toBeDisabled();
|
||||
|
||||
// Verify that navigating back to the dashboard cancels the suggestion and restores the line chart.
|
||||
await panelEditPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await expect(
|
||||
page.locator('[data-viz-panel-key="panel-9"]').locator('.uplot'),
|
||||
'time series to be rendered inside the panel'
|
||||
).toBeVisible();
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.describe(
|
||||
'Visualization suggestions',
|
||||
{
|
||||
tag: ['@various'],
|
||||
tag: ['@various', '@suggestions'],
|
||||
},
|
||||
() => {
|
||||
test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => {
|
||||
|
||||
4
go.mod
4
go.mod
@@ -87,7 +87,7 @@ require (
|
||||
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-20251223160021-926c74910196 // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @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
|
||||
@@ -181,6 +181,7 @@ require (
|
||||
github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // @grafana/grafana-operator-experience-squad
|
||||
github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group
|
||||
go.etcd.io/bbolt v1.4.2 // @grafana/grafana-search-and-storage
|
||||
go.opentelemetry.io/collector/pdata v1.44.0 // @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // @grafana/plugins-platform-backend
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // @grafana/grafana-operator-experience-squad
|
||||
@@ -603,7 +604,6 @@ require (
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.2 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.6 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1622,8 +1622,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-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
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/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=
|
||||
|
||||
@@ -285,6 +285,10 @@ const injectedRtkApi = api
|
||||
query: (queryArg) => ({ url: `/snapshots/delete/${queryArg.deleteKey}`, method: 'DELETE' }),
|
||||
invalidatesTags: ['Snapshot'],
|
||||
}),
|
||||
getSnapshotSettings: build.query<GetSnapshotSettingsApiResponse, GetSnapshotSettingsApiArg>({
|
||||
query: () => ({ url: `/snapshots/settings` }),
|
||||
providesTags: ['Snapshot'],
|
||||
}),
|
||||
getSnapshot: build.query<GetSnapshotApiResponse, GetSnapshotApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/snapshots/${queryArg.name}`,
|
||||
@@ -742,6 +746,8 @@ export type DeleteWithKeyApiArg = {
|
||||
/** unique key returned in create */
|
||||
deleteKey: string;
|
||||
};
|
||||
export type GetSnapshotSettingsApiResponse = /** status 200 undefined */ any;
|
||||
export type GetSnapshotSettingsApiArg = void;
|
||||
export type GetSnapshotApiResponse = /** status 200 OK */ Snapshot;
|
||||
export type GetSnapshotApiArg = {
|
||||
/** name of the Snapshot */
|
||||
@@ -1273,6 +1279,8 @@ export const {
|
||||
useLazyListSnapshotQuery,
|
||||
useCreateSnapshotMutation,
|
||||
useDeleteWithKeyMutation,
|
||||
useGetSnapshotSettingsQuery,
|
||||
useLazyGetSnapshotSettingsQuery,
|
||||
useGetSnapshotQuery,
|
||||
useLazyGetSnapshotQuery,
|
||||
useDeleteSnapshotMutation,
|
||||
|
||||
@@ -356,10 +356,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
dashboardNewLayouts?: boolean;
|
||||
/**
|
||||
* Use the v2 kubernetes API in the frontend for dashboards
|
||||
*/
|
||||
kubernetesDashboardsV2?: boolean;
|
||||
/**
|
||||
* Enables undo/redo in dynamic dashboards
|
||||
*/
|
||||
dashboardUndoRedo?: boolean;
|
||||
@@ -421,6 +417,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
jitterAlertRulesWithinGroups?: boolean;
|
||||
/**
|
||||
* Enable audit logging with Kubernetes under app platform
|
||||
*/
|
||||
auditLoggingAppPlatform?: boolean;
|
||||
/**
|
||||
* Enable the secrets management API and services under app platform
|
||||
*/
|
||||
secretsManagementAppPlatform?: boolean;
|
||||
|
||||
@@ -535,6 +535,11 @@ export const versionedComponents = {
|
||||
'12.3.0': 'data-testid viz-tooltip-wrapper',
|
||||
},
|
||||
},
|
||||
Gauge: {
|
||||
Container: {
|
||||
'12.4.0': 'data-testid gauge container',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
VizLegend: {
|
||||
@@ -1288,6 +1293,9 @@ export const versionedComponents = {
|
||||
card: {
|
||||
[MIN_GRAFANA_VERSION]: (name: string) => `data-testid suggestion-${name}`,
|
||||
},
|
||||
confirm: {
|
||||
'12.4.0': (name: string) => `data-testid suggestion-${name} confirm button`,
|
||||
},
|
||||
},
|
||||
ColorSwatch: {
|
||||
name: {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('MetricsModal', () => {
|
||||
operations: [],
|
||||
};
|
||||
|
||||
setup(query, ['with-labels'], true);
|
||||
setup(query, ['with-labels']);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('with-labels')).toBeInTheDocument();
|
||||
});
|
||||
@@ -220,6 +220,10 @@ function createDatasource(withLabels?: boolean) {
|
||||
// display different results if their labels are selected in the PromVisualQuery
|
||||
if (withLabels) {
|
||||
languageProvider.queryMetricsMetadata = jest.fn().mockResolvedValue({
|
||||
ALERTS: {
|
||||
type: 'gauge',
|
||||
help: 'alerts help text',
|
||||
},
|
||||
'with-labels': {
|
||||
type: 'with-labels-type',
|
||||
help: 'with-labels-help',
|
||||
@@ -297,7 +301,7 @@ function createProps(query: PromVisualQuery, datasource: PrometheusDatasource, m
|
||||
};
|
||||
}
|
||||
|
||||
function setup(query: PromVisualQuery, metrics: string[], withlabels?: boolean) {
|
||||
function setup(query: PromVisualQuery, metrics: string[]) {
|
||||
const withLabels: boolean = query.labels.length > 0;
|
||||
const datasource = createDatasource(withLabels);
|
||||
const props = createProps(query, datasource, metrics);
|
||||
|
||||
@@ -138,7 +138,7 @@ const MetricsModalContent = (props: MetricsModalProps) => {
|
||||
|
||||
export const MetricsModal = (props: MetricsModalProps) => {
|
||||
return (
|
||||
<MetricsModalContextProvider languageProvider={props.datasource.languageProvider}>
|
||||
<MetricsModalContextProvider languageProvider={props.datasource.languageProvider} timeRange={props.timeRange}>
|
||||
<MetricsModalContent {...props} />
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ReactNode } from 'react';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { PrometheusLanguageProviderInterface } from '../../../language_provider';
|
||||
import { getMockTimeRange } from '../../../test/mocks/datasource';
|
||||
|
||||
import { DEFAULT_RESULTS_PER_PAGE, MetricsModalContextProvider, useMetricsModal } from './MetricsModalContext';
|
||||
import { generateMetricData } from './helpers';
|
||||
@@ -25,7 +26,9 @@ const mockLanguageProvider: PrometheusLanguageProviderInterface = {
|
||||
// Helper to create wrapper component
|
||||
const createWrapper = (languageProvider = mockLanguageProvider) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<MetricsModalContextProvider languageProvider={languageProvider}>{children}</MetricsModalContextProvider>
|
||||
<MetricsModalContextProvider languageProvider={languageProvider} timeRange={getMockTimeRange()}>
|
||||
{children}
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -167,6 +170,7 @@ describe('MetricsModalContext', () => {
|
||||
|
||||
it('should handle empty metadata response', async () => {
|
||||
(mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({});
|
||||
(mockLanguageProvider.queryLabelValues as jest.Mock).mockResolvedValue(['metric1', 'metric2']);
|
||||
|
||||
const { result } = renderHook(() => useMetricsModal(), {
|
||||
wrapper: createWrapper(),
|
||||
@@ -176,7 +180,18 @@ describe('MetricsModalContext', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.filteredMetricsData).toEqual([]);
|
||||
expect(result.current.filteredMetricsData).toEqual([
|
||||
{
|
||||
value: 'metric1',
|
||||
type: 'counter',
|
||||
description: 'Test metric',
|
||||
},
|
||||
{
|
||||
value: 'metric2',
|
||||
type: 'counter',
|
||||
description: 'Test metric',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle metadata fetch error', async () => {
|
||||
@@ -239,6 +254,7 @@ describe('MetricsModalContext', () => {
|
||||
}));
|
||||
|
||||
(mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({
|
||||
ALERTS: { type: 'gauge', help: 'Test alerts help' },
|
||||
test_metric: { type: 'counter', help: 'Test metric' },
|
||||
});
|
||||
|
||||
@@ -250,7 +266,7 @@ describe('MetricsModalContext', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.filteredMetricsData).toHaveLength(1);
|
||||
expect(result.current.filteredMetricsData).toHaveLength(2);
|
||||
expect(result.current.selectedTypes).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -318,7 +334,7 @@ describe('MetricsModalContext', () => {
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<MetricsModalContextProvider languageProvider={mockLanguageProvider}>
|
||||
<MetricsModalContextProvider languageProvider={mockLanguageProvider} timeRange={getMockTimeRange()}>
|
||||
<TestComponent />
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
|
||||
@@ -52,11 +52,13 @@ const MetricsModalContext = createContext<MetricsModalContextValue | undefined>(
|
||||
|
||||
type MetricsModalContextProviderProps = {
|
||||
languageProvider: PrometheusLanguageProviderInterface;
|
||||
timeRange: TimeRange;
|
||||
};
|
||||
|
||||
export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalContextProviderProps>> = ({
|
||||
children,
|
||||
languageProvider,
|
||||
timeRange,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [metricsData, setMetricsData] = useState<MetricsData>([]);
|
||||
@@ -111,8 +113,16 @@ export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalConte
|
||||
setIsLoading(true);
|
||||
const metadata = await languageProvider.queryMetricsMetadata(PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
|
||||
if (Object.keys(metadata).length === 0) {
|
||||
setMetricsData([]);
|
||||
// We receive ALERTS metadata in any case
|
||||
if (Object.keys(metadata).length <= 1) {
|
||||
const fetchedMetrics = await languageProvider.queryLabelValues(
|
||||
timeRange,
|
||||
METRIC_LABEL,
|
||||
undefined,
|
||||
PROMETHEUS_QUERY_BUILDER_MAX_RESULTS
|
||||
);
|
||||
const processedData = fetchedMetrics.map((m) => generateMetricData(m, languageProvider));
|
||||
setMetricsData(processedData);
|
||||
} else {
|
||||
const processedData = Object.keys(metadata).map((m) => generateMetricData(m, languageProvider));
|
||||
setMetricsData(processedData);
|
||||
@@ -122,7 +132,7 @@ export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalConte
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [languageProvider]);
|
||||
}, [languageProvider, timeRange]);
|
||||
|
||||
const debouncedBackendSearch = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface Options extends common.SingleStatBaseOptions {
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
sparkline?: boolean;
|
||||
textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none');
|
||||
}
|
||||
|
||||
export const defaultOptions: Partial<Options> = {
|
||||
@@ -48,4 +49,5 @@ export const defaultOptions: Partial<Options> = {
|
||||
showThresholdLabels: false,
|
||||
showThresholdMarkers: true,
|
||||
sparkline: true,
|
||||
textMode: 'auto',
|
||||
};
|
||||
|
||||
@@ -248,15 +248,17 @@ export function PanelChrome({
|
||||
|
||||
const onContentPointerDown = React.useCallback(
|
||||
(evt: React.PointerEvent) => {
|
||||
// Ignore clicks inside buttons, links, canvas and svg elments
|
||||
// When selected, ignore clicks inside buttons, links, canvas and svg elments
|
||||
// This does prevent a clicks inside a graphs from selecting panel as there is normal div above the canvas element that intercepts the click
|
||||
if (evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) {
|
||||
if (isSelected && evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) {
|
||||
// Stop propagation otherwise row config editor will get selected
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect?.(evt);
|
||||
},
|
||||
[onSelect]
|
||||
[isSelected, onSelect]
|
||||
);
|
||||
|
||||
const headerContent = (
|
||||
|
||||
@@ -32,24 +32,6 @@ const meta: Meta<StoryProps> = {
|
||||
controls: {
|
||||
exclude: ['theme', 'values', 'vizCount'],
|
||||
},
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
id: 'scrollable-region-focusable',
|
||||
selector: 'body',
|
||||
enabled: false,
|
||||
},
|
||||
// NOTE: this is necessary due to a false positive with the filered svg glow in one of the examples.
|
||||
// The color-contrast in this component should be accessible!
|
||||
{
|
||||
id: 'color-contrast',
|
||||
selector: 'text',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
barWidthFactor: 0.2,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||
@@ -275,7 +276,11 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.vizWrapper} style={{ width, height }}>
|
||||
<div
|
||||
data-testid={selectors.components.Panels.Visualization.Gauge.Container}
|
||||
className={styles.vizWrapper}
|
||||
style={{ width, height }}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { RadialGaugeDimensions } from './types';
|
||||
|
||||
@@ -25,13 +25,14 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const CENTER_GLOW_OPACITY = 0.15;
|
||||
const CENTER_GLOW_OPACITY = 0.25;
|
||||
|
||||
export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) {
|
||||
const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY);
|
||||
return (
|
||||
<radialGradient id={`circle-glow-${gaugeId}`} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
|
||||
<stop offset="90%" stopColor={color} stopOpacity={0} />
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor={'#ffffff00'} />
|
||||
</radialGradient>
|
||||
);
|
||||
}
|
||||
@@ -44,13 +45,14 @@ export interface CenterGlowProps {
|
||||
|
||||
export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps) {
|
||||
const gradientId = `circle-glow-${gaugeId}`;
|
||||
const transparentColor = color ? colorManipulator.alpha(color, CENTER_GLOW_OPACITY) : color;
|
||||
|
||||
return (
|
||||
<>
|
||||
<defs>
|
||||
<radialGradient id={gradientId} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
|
||||
<stop offset="90%" stopColor={color} stopOpacity={0} />
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor="#ffffff00" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
@@ -86,9 +88,9 @@ export function SpotlightGradient({
|
||||
|
||||
return (
|
||||
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor={'white'} stopOpacity={0.0} />
|
||||
<stop offset="95%" stopColor={'white'} stopOpacity={0.5} />
|
||||
{roundedBars && <stop offset="100%" stopColor={'white'} stopOpacity={roundedBars ? 0.7 : 1} />}
|
||||
<stop offset="0%" stopColor="#ffffff00" />
|
||||
<stop offset="95%" stopColor="#ffffff88" />
|
||||
{roundedBars && <stop offset="100%" stopColor={roundedBars ? '#ffffffbb' : 'white'} />}
|
||||
</linearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ export interface SparklineProps extends Themeable2 {
|
||||
showHighlights?: boolean;
|
||||
}
|
||||
|
||||
export const SparklineFn: React.FC<SparklineProps> = memo((props) => {
|
||||
export const Sparkline: React.FC<SparklineProps> = memo((props) => {
|
||||
const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props;
|
||||
|
||||
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights);
|
||||
if (warning) {
|
||||
return null;
|
||||
@@ -30,14 +31,4 @@ export const SparklineFn: React.FC<SparklineProps> = memo((props) => {
|
||||
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
|
||||
});
|
||||
|
||||
SparklineFn.displayName = 'Sparkline';
|
||||
|
||||
// we converted to function component above, but some apps extend Sparkline, so we need
|
||||
// to keep exporting a class component until those apps are all rolled out.
|
||||
// see https://github.com/grafana/app-observability-plugin/pull/2079
|
||||
// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component
|
||||
export class Sparkline extends React.PureComponent<SparklineProps> {
|
||||
render() {
|
||||
return <SparklineFn {...this.props} />;
|
||||
}
|
||||
}
|
||||
Sparkline.displayName = 'Sparkline';
|
||||
|
||||
@@ -167,7 +167,8 @@ export class VizRepeater<V, D = {}> extends PureComponent<PropsWithDefaults<V, D
|
||||
|
||||
const repeaterStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
overflow: `${minVizWidth ? 'auto' : 'hidden'} ${minVizHeight ? 'auto' : 'hidden'}`,
|
||||
overflowX: `${minVizWidth ? 'auto' : 'hidden'}`,
|
||||
overflowY: `${minVizHeight ? 'auto' : 'hidden'}`,
|
||||
};
|
||||
|
||||
let vizHeight = height;
|
||||
|
||||
88
pkg/apiserver/auditing/event.go
Normal file
88
pkg/apiserver/auditing/event.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package auditing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
// The namespace the action was performed in.
|
||||
Namespace string `json:"namespace"`
|
||||
|
||||
// When it happened.
|
||||
ObservedAt time.Time `json:"-"` // see MarshalJSON for why this is omitted
|
||||
|
||||
// Who/what performed the action.
|
||||
SubjectName string `json:"subjectName"`
|
||||
SubjectUID string `json:"subjectUID"`
|
||||
|
||||
// What was performed.
|
||||
Verb string `json:"verb"`
|
||||
|
||||
// The object the action was performed on. For verbs like "list" this will be empty.
|
||||
Object string `json:"object,omitempty"`
|
||||
|
||||
// API information.
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
|
||||
// Outcome of the action.
|
||||
Outcome EventOutcome `json:"outcome"`
|
||||
|
||||
// Extra fields to add more context to the event.
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
func (e Event) Time() time.Time {
|
||||
return e.ObservedAt
|
||||
}
|
||||
|
||||
func (e Event) MarshalJSON() ([]byte, error) {
|
||||
type Alias Event
|
||||
return json.Marshal(&struct {
|
||||
FormattedTimestamp string `json:"observedAt"`
|
||||
Alias
|
||||
}{
|
||||
FormattedTimestamp: e.ObservedAt.UTC().Format(time.RFC3339Nano),
|
||||
Alias: (Alias)(e),
|
||||
})
|
||||
}
|
||||
|
||||
func (e Event) KVPairs() []any {
|
||||
args := []any{
|
||||
"audit", true,
|
||||
"namespace", e.Namespace,
|
||||
"observedAt", e.ObservedAt.UTC().Format(time.RFC3339Nano),
|
||||
"subjectName", e.SubjectName,
|
||||
"subjectUID", e.SubjectUID,
|
||||
"verb", e.Verb,
|
||||
"object", e.Object,
|
||||
"apiGroup", e.APIGroup,
|
||||
"apiVersion", e.APIVersion,
|
||||
"kind", e.Kind,
|
||||
"outcome", e.Outcome,
|
||||
}
|
||||
|
||||
if len(e.Extra) > 0 {
|
||||
extraArgs := make([]any, 0, len(e.Extra)*2)
|
||||
|
||||
for k, v := range e.Extra {
|
||||
extraArgs = append(extraArgs, "extra_"+k, v)
|
||||
}
|
||||
|
||||
args = append(args, extraArgs...)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
type EventOutcome string
|
||||
|
||||
const (
|
||||
EventOutcomeUnknown EventOutcome = "unknown"
|
||||
EventOutcomeSuccess EventOutcome = "success"
|
||||
EventOutcomeFailureUnauthorized EventOutcome = "failure_unauthorized"
|
||||
EventOutcomeFailureNotFound EventOutcome = "failure_not_found"
|
||||
EventOutcomeFailureGeneric EventOutcome = "failure_generic"
|
||||
)
|
||||
64
pkg/apiserver/auditing/event_test.go
Normal file
64
pkg/apiserver/auditing/event_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package auditing_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apiserver/auditing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEvent_MarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("marshals the event", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
event := auditing.Event{
|
||||
ObservedAt: now,
|
||||
Extra: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]any
|
||||
require.NoError(t, json.Unmarshal(data, &result))
|
||||
|
||||
require.Equal(t, event.Time().UTC().Format(time.RFC3339Nano), result["observedAt"])
|
||||
require.NotNil(t, result["extra"])
|
||||
require.Len(t, result["extra"], 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEvent_KVPairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("records extra fields", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
extraFields := 2
|
||||
extra := make(map[string]string, 0)
|
||||
for i := 0; i < extraFields; i++ {
|
||||
extra[strconv.Itoa(i)] = "value"
|
||||
}
|
||||
|
||||
event := auditing.Event{Extra: extra}
|
||||
|
||||
kvPairs := event.KVPairs()
|
||||
|
||||
extraCount := 0
|
||||
for i := 0; i < len(kvPairs); i += 2 {
|
||||
if strings.HasPrefix(kvPairs[i].(string), "extra_") {
|
||||
extraCount++
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, extraCount, extraFields)
|
||||
})
|
||||
}
|
||||
55
pkg/apiserver/auditing/logger.go
Normal file
55
pkg/apiserver/auditing/logger.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package auditing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Sinkable is a log entry abstraction that can be sent to an audit log sink through the different implementing methods.
|
||||
type Sinkable interface {
|
||||
json.Marshaler
|
||||
KVPairs() []any
|
||||
Time() time.Time
|
||||
}
|
||||
|
||||
// Logger specifies the contract for a specific audit logger.
|
||||
type Logger interface {
|
||||
Log(entry Sinkable) error
|
||||
Close() error
|
||||
Type() string
|
||||
}
|
||||
|
||||
// Implementation inspired by https://github.com/grafana/grafana-app-sdk/blob/main/logging/logger.go
|
||||
type loggerContextKey struct{}
|
||||
|
||||
var (
|
||||
// DefaultLogger is the default Logger if one hasn't been provided in the context.
|
||||
// You may use this to add arbitrary audit logging outside of an API request lifecycle.
|
||||
DefaultLogger Logger = &NoopLogger{}
|
||||
|
||||
contextKey = loggerContextKey{}
|
||||
)
|
||||
|
||||
// FromContext returns the Logger set in the context with Context(), or the DefaultLogger if no Logger is set in the context.
|
||||
// If DefaultLogger is nil, it returns a *NoopLogger so that the return is always valid to call methods on without nil-checking.
|
||||
// You may use this to add arbitrary audit logging outside of an API request lifecycle.
|
||||
func FromContext(ctx context.Context) Logger {
|
||||
if l := ctx.Value(contextKey); l != nil {
|
||||
if logger, ok := l.(Logger); ok {
|
||||
return logger
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultLogger != nil {
|
||||
return DefaultLogger
|
||||
}
|
||||
|
||||
return &NoopLogger{}
|
||||
}
|
||||
|
||||
// Context returns a new context built from the provided context with the provided logger in it.
|
||||
// The Logger added with Context() can be retrieved with FromContext()
|
||||
func Context(ctx context.Context, logger Logger) context.Context {
|
||||
return context.WithValue(ctx, contextKey, logger)
|
||||
}
|
||||
@@ -11,9 +11,9 @@ type NoopBackend struct{}
|
||||
|
||||
func ProvideNoopBackend() audit.Backend { return &NoopBackend{} }
|
||||
|
||||
func (b *NoopBackend) ProcessEvents(k8sEvents ...*auditinternal.Event) bool { return false }
|
||||
func (NoopBackend) ProcessEvents(...*auditinternal.Event) bool { return false }
|
||||
|
||||
func (NoopBackend) Run(stopCh <-chan struct{}) error { return nil }
|
||||
func (NoopBackend) Run(<-chan struct{}) error { return nil }
|
||||
|
||||
func (NoopBackend) Shutdown() {}
|
||||
|
||||
@@ -34,3 +34,14 @@ type NoopPolicyRuleEvaluator struct{}
|
||||
func (NoopPolicyRuleEvaluator) EvaluatePolicyRule(authorizer.Attributes) audit.RequestAuditConfig {
|
||||
return audit.RequestAuditConfig{Level: auditinternal.LevelNone}
|
||||
}
|
||||
|
||||
// NoopLogger is a no-op implementation of Logger
|
||||
type NoopLogger struct{}
|
||||
|
||||
func ProvideNoopLogger() Logger { return &NoopLogger{} }
|
||||
|
||||
func (NoopLogger) Type() string { return "noop" }
|
||||
|
||||
func (NoopLogger) Log(Sinkable) error { return nil }
|
||||
|
||||
func (NoopLogger) Close() error { return nil }
|
||||
|
||||
@@ -46,14 +46,23 @@ func (defaultGrafanaPolicyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Att
|
||||
}
|
||||
}
|
||||
|
||||
// Logging the response object allows us to get the resource name for create requests.
|
||||
level := auditinternal.LevelMetadata
|
||||
if attrs.GetVerb() == utils.VerbCreate {
|
||||
level = auditinternal.LevelRequestResponse
|
||||
}
|
||||
|
||||
return audit.RequestAuditConfig{
|
||||
Level: auditinternal.LevelMetadata,
|
||||
Level: level,
|
||||
|
||||
// Only log on StageResponseComplete, to avoid noisy logs.
|
||||
OmitStages: []auditinternal.Stage{
|
||||
// Only log on StageResponseComplete
|
||||
auditinternal.StageRequestReceived,
|
||||
auditinternal.StageResponseStarted,
|
||||
auditinternal.StagePanic,
|
||||
},
|
||||
OmitManagedFields: false, // Setting it to true causes extra copying/unmarshalling.
|
||||
|
||||
// Setting it to true causes extra copying/unmarshalling.
|
||||
OmitManagedFields: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestDefaultGrafanaPolicyRuleEvaluator(t *testing.T) {
|
||||
require.Equal(t, auditinternal.LevelNone, config.Level)
|
||||
})
|
||||
|
||||
t.Run("return audit level metadata for other resource requests", func(t *testing.T) {
|
||||
t.Run("return audit level request+response for create requests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
attrs := authorizer.AttributesRecord{
|
||||
@@ -67,6 +67,22 @@ func TestDefaultGrafanaPolicyRuleEvaluator(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config := evaluator.EvaluatePolicyRule(attrs)
|
||||
require.Equal(t, auditinternal.LevelRequestResponse, config.Level)
|
||||
})
|
||||
|
||||
t.Run("return audit level metadata for other resource requests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
attrs := authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
Verb: utils.VerbGet,
|
||||
User: &user.DefaultInfo{
|
||||
Name: "test-user",
|
||||
Groups: []string{"test-group"},
|
||||
},
|
||||
}
|
||||
|
||||
config := evaluator.EvaluatePolicyRule(attrs)
|
||||
require.Equal(t, auditinternal.LevelMetadata, config.Level)
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/configprovider"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -62,7 +63,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -128,7 +128,6 @@ type DashboardsAPIBuilder struct {
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
@@ -154,7 +153,14 @@ func RegisterAPIService(
|
||||
publicDashboardService publicdashboards.Service,
|
||||
snapshotService dashboardsnapshots.Service,
|
||||
dashboardActivityChannel live.DashboardActivityChannel,
|
||||
configProvider configprovider.ConfigProvider,
|
||||
) *DashboardsAPIBuilder {
|
||||
cfg, err := configProvider.Get(context.Background())
|
||||
if err != nil {
|
||||
logging.DefaultLogger.Error("failed to load settings configuration instance", "stackId", cfg.StackID, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
@@ -237,7 +243,7 @@ func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles,
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
|
||||
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts, featuremgmt.FlagKubernetesDashboardsV2) {
|
||||
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) {
|
||||
// If dashboards v2 is enabled, we want to use v2beta1 as the default API version.
|
||||
return []schema.GroupVersion{
|
||||
dashv2beta1.DashboardResourceInfo.GroupVersion(),
|
||||
@@ -747,7 +753,6 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
ResourceInfo: *snapshots,
|
||||
Service: b.snapshotService,
|
||||
Namespacer: b.namespacer,
|
||||
Options: b.snapshotOptions,
|
||||
}
|
||||
storage[snapshots.StoragePath()] = snapshotLegacyStore
|
||||
storage[snapshots.StoragePath("dashboard")], err = snapshot.NewDashboardREST(dashboards, b.snapshotService)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user