From 2627df30d6226fe94f5b6f44b49a7fb98a3d7840 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:41:07 -0800 Subject: [PATCH] Prevent class name collisions --- public/app/features/canvas/elements/svg.tsx | 56 +++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/public/app/features/canvas/elements/svg.tsx b/public/app/features/canvas/elements/svg.tsx index 1d2a634e4df..89c1fe69e3d 100644 --- a/public/app/features/canvas/elements/svg.tsx +++ b/public/app/features/canvas/elements/svg.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; import { GrafanaTheme2, textUtil } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -8,6 +9,34 @@ import { DimensionContext } from 'app/features/dimensions/context'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps } from '../element'; +// Simple hash function to generate unique scope IDs +function hashString(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0; + } + return Math.abs(hash).toString(36); +} + +// Scope CSS classes to avoid conflicts between multiple SVG elements +function scopeSvgClasses(content: string, scopeId: string): string { + // Replace class definitions in style blocks (.classname) + let scoped = content.replace(/\.([a-zA-Z_-][\w-]*)/g, (match, className) => { + return `.${className}-${scopeId}`; + }); + + // Replace class attributes (class="name1 name2") + scoped = scoped.replace(/class="([^"]+)"/g, (match, classNames) => { + const scopedNames = classNames + .split(/\s+/) + .map((name: string) => (name ? `${name}-${scopeId}` : '')) + .join(' '); + return `class="${scopedNames}"`; + }); + + return scoped; +} + export interface SvgConfig { content?: TextDimensionConfig; } @@ -20,6 +49,14 @@ export function SvgDisplay(props: CanvasElementProps) { const { data } = props; const styles = useStyles2(getStyles); + // Generate unique scope ID based on content hash + const scopeId = useMemo(() => { + if (!data?.content) { + return ''; + } + return hashString(data.content); + }, [data?.content]); + if (!data?.content) { return (
{t('canvas.svg-element.placeholder', 'Double click to add SVG content')}
@@ -29,17 +66,18 @@ export function SvgDisplay(props: CanvasElementProps) { // Check if content already has an SVG wrapper const hasSvgWrapper = data.content.trim().toLowerCase().startsWith('${data.content}`; - sanitizedContent = textUtil.sanitizeSVGContent(wrappedContent); + // Prepare content (wrap if needed) + let contentToScope = data.content; + if (!hasSvgWrapper) { + contentToScope = `${data.content}`; } + // Scope class names to prevent conflicts + const scopedContent = scopeSvgClasses(contentToScope, scopeId); + + // Sanitize the scoped content + const sanitizedContent = textUtil.sanitizeSVGContent(scopedContent); + return
; }