From d8cdee80f00d7025062f7fa1c2ddb62f45189a6f Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Mon, 5 Jan 2026 09:48:16 -0500 Subject: [PATCH] Canvas: Fix image loading when icon element SVG defined by field mappings (#115748) * chore(gdev-dashboard): minimal repro of escalation #19939 bug report * fix(canvas): add branching logic to handle field mapping to icons case * test(canvas): validate integration of canvas icon mappings * refactor(resource-dimension): defensive against JS `undefined` in paths --- .../panel-canvas/canvas_kitchen_sink.v42.json | 598 ++++++++++++++++++ .../panel-canvas/canvas_kitchen_sink.json | 582 +++++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 1 + .../panels-suite/canvas-icon-mappings.spec.ts | 99 +++ .../app/features/dimensions/resource.test.ts | 99 +++ public/app/features/dimensions/resource.ts | 12 +- 6 files changed, 1388 insertions(+), 3 deletions(-) create mode 100644 apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-canvas/canvas_kitchen_sink.v42.json create mode 100644 devenv/dev-dashboards/panel-canvas/canvas_kitchen_sink.json create mode 100644 e2e-playwright/panels-suite/canvas-icon-mappings.spec.ts diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-canvas/canvas_kitchen_sink.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-canvas/canvas_kitchen_sink.v42.json new file mode 100644 index 00000000000..cf4db2fb1b6 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-canvas/canvas_kitchen_sink.v42.json @@ -0,0 +1,598 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + }, + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + }, + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "success" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warning" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "unmapped" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + }, + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + }, + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "inlineEditing": true, + "root": { + "background": { + "color": { + "fixed": "transparent" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "elements": [ + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 16, + "text": { + "fixed": "Field-based Icons (from value mappings):", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Header", + "placement": { + "height": 40, + "left": 20, + "top": 10, + "width": 400 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "success", + "fixed": "green" + }, + "path": { + "field": "success", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Success Icon", + "placement": { + "height": 50, + "left": 50, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "success", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "success", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Success Text", + "placement": { + "height": 25, + "left": 30, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "warning", + "fixed": "orange" + }, + "path": { + "field": "warning", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Warning Icon", + "placement": { + "height": 50, + "left": 180, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "warning", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "warning", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Warning Text", + "placement": { + "height": 25, + "left": 160, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "error", + "fixed": "red" + }, + "path": { + "field": "error", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Error Icon", + "placement": { + "height": 50, + "left": 310, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "error", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "error", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Error Text", + "placement": { + "height": 25, + "left": 290, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "unmapped", + "fixed": "#808080" + }, + "path": { + "field": "unmapped", + "fixed": "img/icons/unicons/question-circle.svg", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Unmapped Icon", + "placement": { + "height": 50, + "left": 440, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "unmapped", + "fixed": "text" + }, + "size": 12, + "text": { + "fixed": "No mapping (14)", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Unmapped Text", + "placement": { + "height": 25, + "left": 410, + "top": 115, + "width": 110 + }, + "type": "text" + }, + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 14, + "text": { + "fixed": "Fixed Relative Path:", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Relative Label", + "placement": { + "height": 30, + "left": 50, + "top": 170, + "width": 200 + }, + "type": "text" + }, + { + "config": { + "fill": { + "fixed": "blue" + }, + "path": { + "fixed": "img/icons/unicons/cloud.svg", + "mode": "fixed" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Relative Icon", + "placement": { + "height": 50, + "left": 260, + "top": 165, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 14, + "text": { + "fixed": "Fixed Absolute URL:", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Absolute Label", + "placement": { + "height": 30, + "left": 50, + "top": 240, + "width": 200 + }, + "type": "text" + }, + { + "config": { + "fill": { + "fixed": "purple" + }, + "path": { + "fixed": "https://grafana.com/static/assets/img/grafana_icon.svg", + "mode": "fixed" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Absolute Icon", + "placement": { + "height": 50, + "left": 260, + "top": 235, + "width": 50 + }, + "type": "icon" + } + ], + "name": "Canvas Root", + "placement": { + "height": 100, + "left": 0, + "top": 0, + "width": 100 + }, + "type": "frame" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "csvContent": "success\n1", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + }, + { + "csvContent": "warning\n2", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_content" + }, + { + "csvContent": "error\n3", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_content" + }, + { + "csvContent": "unmapped\n14", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_content" + } + ], + "title": "Various SVG icons", + "type": "canvas" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [ + "canvas", + "icons", + "test", + "v2" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Panel tests - Canvas - Kitchen sink", + "uid": "canvas-icon-fix-test-v2", + "weekStart": "" +} \ No newline at end of file diff --git a/devenv/dev-dashboards/panel-canvas/canvas_kitchen_sink.json b/devenv/dev-dashboards/panel-canvas/canvas_kitchen_sink.json new file mode 100644 index 00000000000..069a43c0888 --- /dev/null +++ b/devenv/dev-dashboards/panel-canvas/canvas_kitchen_sink.json @@ -0,0 +1,582 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + }, + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + }, + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "success" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warning" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "unmapped" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "color": "green", + "icon": "img/icons/unicons/check-circle.svg", + "index": 0, + "text": "Success" + }, + "2": { + "color": "orange", + "icon": "img/icons/unicons/exclamation-triangle.svg", + "index": 1, + "text": "Warning" + }, + "3": { + "color": "red", + "icon": "img/icons/unicons/times-circle.svg", + "index": 2, + "text": "Error" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "inlineEditing": true, + "root": { + "background": { + "color": { + "fixed": "transparent" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "elements": [ + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 16, + "text": { + "fixed": "Field-based Icons (from value mappings):", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Header", + "placement": { + "height": 40, + "left": 20, + "top": 10, + "width": 400 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "success", + "fixed": "green" + }, + "path": { + "field": "success", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Success Icon", + "placement": { + "height": 50, + "left": 50, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "success", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "success", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Success Text", + "placement": { + "height": 25, + "left": 30, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "warning", + "fixed": "orange" + }, + "path": { + "field": "warning", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Warning Icon", + "placement": { + "height": 50, + "left": 180, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "warning", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "warning", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Warning Text", + "placement": { + "height": 25, + "left": 160, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "error", + "fixed": "red" + }, + "path": { + "field": "error", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Error Icon", + "placement": { + "height": 50, + "left": 310, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "error", + "fixed": "text" + }, + "size": 12, + "text": { + "field": "error", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Error Text", + "placement": { + "height": 25, + "left": 290, + "top": 115, + "width": 90 + }, + "type": "text" + }, + { + "config": { + "fill": { + "field": "unmapped", + "fixed": "#808080" + }, + "path": { + "field": "unmapped", + "fixed": "img/icons/unicons/question-circle.svg", + "mode": "field" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Unmapped Icon", + "placement": { + "height": 50, + "left": 440, + "top": 60, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "field": "unmapped", + "fixed": "text" + }, + "size": 12, + "text": { + "fixed": "No mapping (14)", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Unmapped Text", + "placement": { + "height": 25, + "left": 410, + "top": 115, + "width": 110 + }, + "type": "text" + }, + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 14, + "text": { + "fixed": "Fixed Relative Path:", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Relative Label", + "placement": { + "height": 30, + "left": 50, + "top": 170, + "width": 200 + }, + "type": "text" + }, + { + "config": { + "fill": { + "fixed": "blue" + }, + "path": { + "fixed": "img/icons/unicons/cloud.svg", + "mode": "fixed" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Relative Icon", + "placement": { + "height": 50, + "left": 260, + "top": 165, + "width": 50 + }, + "type": "icon" + }, + { + "config": { + "align": "center", + "color": { + "fixed": "text" + }, + "size": 14, + "text": { + "fixed": "Fixed Absolute URL:", + "mode": "fixed" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Absolute Label", + "placement": { + "height": 30, + "left": 50, + "top": 240, + "width": 200 + }, + "type": "text" + }, + { + "config": { + "fill": { + "fixed": "purple" + }, + "path": { + "fixed": "https://grafana.com/static/assets/img/grafana_icon.svg", + "mode": "fixed" + } + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Absolute Icon", + "placement": { + "height": 50, + "left": 260, + "top": 235, + "width": 50 + }, + "type": "icon" + } + ], + "name": "Canvas Root", + "placement": { + "height": 100, + "left": 0, + "top": 0, + "width": 100 + }, + "type": "frame" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content", + "csvContent": "success\n1" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_content", + "csvContent": "warning\n2" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "csv_content", + "csvContent": "error\n3" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "D", + "scenarioId": "csv_content", + "csvContent": "unmapped\n14" + } + ], + "title": "Various SVG icons", + "type": "canvas" + } + ], + "schemaVersion": 39, + "tags": ["canvas", "icons", "test", "v2"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Panel tests - Canvas - Kitchen sink", + "uid": "canvas-icon-fix-test-v2", + "version": 1, + "weekStart": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index cac117cd4e7..98eb14e9217 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -24,6 +24,7 @@ "canvas-connection-examples": (import '../dev-dashboards/panel-canvas/canvas-connection-examples.json'), "canvas-datalinks": (import '../dev-dashboards/panel-canvas/canvas-datalinks.json'), "canvas-examples": (import '../dev-dashboards/panel-canvas/canvas-examples.json'), + "canvas_kitchen_sink": (import '../dev-dashboards/panel-canvas/canvas_kitchen_sink.json'), "color_modes": (import '../dev-dashboards/panel-common/color_modes.json'), "config-from-query": (import '../dev-dashboards/transforms/config-from-query.json'), "dashlist": (import '../dev-dashboards/panel-dashlist/dashlist.json'), diff --git a/e2e-playwright/panels-suite/canvas-icon-mappings.spec.ts b/e2e-playwright/panels-suite/canvas-icon-mappings.spec.ts new file mode 100644 index 00000000000..72de7a6c62c --- /dev/null +++ b/e2e-playwright/panels-suite/canvas-icon-mappings.spec.ts @@ -0,0 +1,99 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'canvas-icon-fix-test-v2'; +const PANEL_TITLE = 'Various SVG icons'; + +test.describe('Canvas Panel - Icon Mappings', () => { + test('should render field-based icons from value mappings correctly', async ({ gotoDashboardPage, page }) => { + await test.step('Navigate to dashboard and wait for panel to load', async () => { + await gotoDashboardPage({ uid: DASHBOARD_UID }); + await page.waitForSelector('svg', { timeout: 10000 }); + }); + + await test.step('Verify value mapping text values are displayed', async () => { + await expect(page.getByText('Success')).toBeVisible(); + await expect(page.getByText('Warning')).toBeVisible(); + await expect(page.getByText('Error')).toBeVisible(); + }); + + await test.step('Verify SVG icons rendered for mapped values', async () => { + const svgCount = await page.locator('svg').count(); + expect(svgCount).toBeGreaterThanOrEqual(3); + }); + }); + + test('should render fixed path icons correctly', async ({ gotoDashboardPage, page }) => { + await test.step('Set up network interception for absolute URL icon', async () => { + await page.route('https://grafana.com/static/assets/img/grafana_icon.svg', async (route) => { + const dummySvg = ` + + TEST + `; + await route.fulfill({ + status: 200, + contentType: 'image/svg+xml', + body: dummySvg, + }); + }); + }); + + await test.step('Navigate to dashboard and wait for SVGs to load', async () => { + await gotoDashboardPage({ uid: DASHBOARD_UID }); + await page.waitForSelector('svg:not([aria-hidden="true"])', { timeout: 10000 }); + await page.waitForLoadState('networkidle', { timeout: 10000 }); + }); + + await test.step('Verify at least 5 visible SVG icons are rendered (3 mapped + 2 fixed)', async () => { + const visibleSvgs = page.locator('svg:not([aria-hidden="true"])'); + const svgCount = await visibleSvgs.count(); + expect(svgCount).toBeGreaterThanOrEqual(5); + }); + + await test.step('Verify visible SVG icons have content', async () => { + const visibleSvgs = page.locator('svg:not([aria-hidden="true"])'); + const count = await visibleSvgs.count(); + + for (let i = 0; i < Math.min(count, 5); i++) { + const svg = visibleSvgs.nth(i); + await expect(svg).toBeAttached(); + const svgContent = await svg.innerHTML(); + expect(svgContent.length).toBeGreaterThan(0); + } + }); + }); + + test('should not make invalid requests for unmapped numeric values', async ({ gotoDashboardPage, page }) => { + const failedRequests: string[] = []; + + await test.step('Set up network request monitoring', async () => { + page.on('requestfailed', (request) => { + const url = request.url(); + if (url.match(/\/build\/\d+$/)) { + failedRequests.push(url); + } + }); + }); + + await test.step('Navigate to dashboard and wait for loading', async () => { + await gotoDashboardPage({ uid: DASHBOARD_UID }); + await page.waitForTimeout(2000); + }); + + await test.step('Verify no invalid numeric path requests were made', async () => { + expect(failedRequests).toHaveLength(0); + }); + }); + + test('should display text values from value mappings correctly', async ({ gotoDashboardPage, page }) => { + await test.step('Navigate to dashboard', async () => { + await gotoDashboardPage({ uid: DASHBOARD_UID }); + }); + + await test.step('Verify mapped text values are displayed', async () => { + await expect(page.getByText('Success')).toBeVisible(); + await expect(page.getByText('Warning')).toBeVisible(); + await expect(page.getByText('Error')).toBeVisible(); + await expect(page.getByText('No mapping (14)')).toBeVisible(); + }); + }); +}); diff --git a/public/app/features/dimensions/resource.test.ts b/public/app/features/dimensions/resource.test.ts index ad644b61b4f..ac4d1c349dc 100644 --- a/public/app/features/dimensions/resource.test.ts +++ b/public/app/features/dimensions/resource.test.ts @@ -103,6 +103,84 @@ describe('getResourceDimension', () => { expect(getResourceDimension(frame, config).value()).toEqual(''); }); + it('should handle numeric field values with icon from value mapping', () => { + const publicPath = 'https://grafana.fake/public/'; + const frame = createDataFrame({ + fields: [ + { + name: 'status_field', + values: [1, 2, 3], + display: (v) => ({ + text: v === 1 ? 'Success' : v === 2 ? 'Warning' : 'Error', + numeric: Number(v), + icon: + v === 1 + ? 'img/icons/unicons/check-circle.svg' + : v === 2 + ? 'img/icons/unicons/exclamation-triangle.svg' + : 'img/icons/unicons/times-circle.svg', + }), + }, + ], + }); + const config = { mode: ResourceDimensionMode.Field, field: 'status_field', fixed: '' }; + + expect(getResourceDimension(frame, config).get(0)).toEqual(`${publicPath}build/img/icons/unicons/check-circle.svg`); + expect(getResourceDimension(frame, config).get(1)).toEqual( + `${publicPath}build/img/icons/unicons/exclamation-triangle.svg` + ); + expect(getResourceDimension(frame, config).get(2)).toEqual(`${publicPath}build/img/icons/unicons/times-circle.svg`); + }); + + it('should return empty string for unmapped numeric values without icon', () => { + const frame = createDataFrame({ + fields: [ + { + name: 'status_field', + values: [14], + display: (v) => ({ + text: String(v), + numeric: Number(v), + icon: undefined, + }), + }, + ], + }); + const config = { mode: ResourceDimensionMode.Field, field: 'status_field', fixed: '' }; + + expect(getResourceDimension(frame, config).get(0)).toEqual(''); + expect(getResourceDimension(frame, config).value()).toEqual(''); + }); + + it('should handle mixed numeric values with partial mappings', () => { + const publicPath = 'https://grafana.fake/public/'; + const frame = createDataFrame({ + fields: [ + { + name: 'status_field', + values: [1, 99, 2], + display: (v) => ({ + text: v === 1 ? 'Success' : v === 2 ? 'Warning' : String(v), + numeric: Number(v), + icon: + v === 1 + ? 'img/icons/unicons/check-circle.svg' + : v === 2 + ? 'img/icons/unicons/exclamation-triangle.svg' + : undefined, + }), + }, + ], + }); + const config = { mode: ResourceDimensionMode.Field, field: 'status_field', fixed: '' }; + + expect(getResourceDimension(frame, config).get(0)).toEqual(`${publicPath}build/img/icons/unicons/check-circle.svg`); + expect(getResourceDimension(frame, config).get(1)).toEqual(''); + expect(getResourceDimension(frame, config).get(2)).toEqual( + `${publicPath}build/img/icons/unicons/exclamation-triangle.svg` + ); + }); + // TODO: write tests for mapping modes }); @@ -125,4 +203,25 @@ describe('getPublicOrAbsoluteUrl', () => { expect(getPublicOrAbsoluteUrl({ path: 'icon.png' })).toEqual(''); expect(getPublicOrAbsoluteUrl(['icon.png'])).toEqual(''); }); + + it('should handle undefined publicPath gracefully', () => { + const originalPath = window.__grafana_public_path__; + + // @ts-ignore - Intentionally testing runtime edge case + window.__grafana_public_path__ = undefined; + + expect(getPublicOrAbsoluteUrl('icon.png')).toEqual('/build/icon.png'); + + window.__grafana_public_path__ = originalPath; + }); + + it('should handle empty string publicPath gracefully', () => { + const originalPath = window.__grafana_public_path__; + + window.__grafana_public_path__ = ''; + + expect(getPublicOrAbsoluteUrl('icon.png')).toEqual('/build/icon.png'); + + window.__grafana_public_path__ = originalPath; + }); }); diff --git a/public/app/features/dimensions/resource.ts b/public/app/features/dimensions/resource.ts index 4eb28242550..822d808b016 100644 --- a/public/app/features/dimensions/resource.ts +++ b/public/app/features/dimensions/resource.ts @@ -15,8 +15,9 @@ export function getPublicOrAbsoluteUrl(path: unknown): string { // NOTE: The value of `path` could be either an URL string or a relative // path to a Grafana CDN asset served from the CDN. const isUrl = path.indexOf(':/') > 0; + const publicPath = window.__grafana_public_path__ || '/'; - return isUrl ? path : `${window.__grafana_public_path__}build/${path}`; + return isUrl ? path : `${publicPath}build/${path}`; } export function getResourceDimension( @@ -56,11 +57,11 @@ export function getResourceDimension( // mode === ResourceDimensionMode.Field case const getImageOrIcon = (value: unknown): string => { - if (typeof value !== 'string') { + if (typeof value !== 'string' && typeof value !== 'number') { return ''; } - let url = value; + let url = typeof value === 'string' ? value : ''; if (field && field.display) { const displayValue = field.display(value); if (displayValue.icon) { @@ -68,6 +69,11 @@ export function getResourceDimension( } } + const noIconFound = !url; + if (noIconFound) { + return ''; + } + return getPublicOrAbsoluteUrl(url); };