Dashboard: Remove BOMs from links during conversion (#115689)

* Dashboard: Add test case for BOM characters in link URLs

This test demonstrates the issue where BOM (Byte Order Mark) characters
in dashboard link URLs cause CUE validation errors during v1 to v2
conversion ('illegal byte order mark').

The test input contains BOMs in various URL locations:
- Dashboard links
- Panel data links
- Field config override links
- Options dataLinks
- Field config default links

* Dashboard: Strip BOM characters from URLs during v1 to v2 conversion

BOM (Byte Order Mark) characters in dashboard link URLs cause CUE
validation errors ('illegal byte order mark') when opening v2 dashboards.

This fix strips BOMs from all URL fields during conversion:
- Dashboard links
- Panel data links
- Field config override links
- Options dataLinks
- Field config default links

The stripBOM helper recursively processes nested structures to ensure
all string values have BOMs removed.

* Dashboard: Strip BOM characters in frontend v2 conversion

Add stripBOMs parameter to sortedDeepCloneWithoutNulls utility to remove
Byte Order Mark (U+FEFF) characters from all strings when serializing
dashboards to v2 format.

This prevents CUE validation errors ('illegal byte order mark') that occur
when BOMs are present in any string field. BOMs can be introduced through
copy/paste from certain editors or text sources.

Applied at the final serialization step so it catches BOMs from:
- Existing v1 dashboards being converted
- New data entered during dashboard editing
This commit is contained in:
Oscar Kilhed
2025-12-29 09:53:45 +01:00
committed by GitHub
parent 4ba2fe6cce
commit 0b58cd3900
15 changed files with 861 additions and 19 deletions
@@ -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"]
}
}
}