mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Avoid conflicting stylesheets when loading SVG icons (#74461)
This commit is contained in:
parent
b779ce5687
commit
7171b35095
58
public/app/core/components/SVG/SanitizedSVG.test.tsx
Normal file
58
public/app/core/components/SVG/SanitizedSVG.test.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { getSvgId, getSvgStyle, svgStyleCleanup } from './utils';
|
||||
|
||||
const ID = 'TEST_ID';
|
||||
|
||||
const svgNoId =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">.st0{fill:purple;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>';
|
||||
|
||||
const svgWithId = `<svg id="${ID}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">.st0{fill:blue;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`;
|
||||
|
||||
const svgWithWrongIdInStyle =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#WRONG .st0{fill:green;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>';
|
||||
|
||||
describe('SanitizedSVG', () => {
|
||||
it('should cleanup the style and generate an ID', () => {
|
||||
const cleanStyle = svgStyleCleanup(svgNoId);
|
||||
const updatedStyle = getSvgStyle(cleanStyle);
|
||||
const svgId = getSvgId(cleanStyle);
|
||||
|
||||
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
|
||||
expect(svgId).toBeDefined();
|
||||
expect(svgId?.startsWith('x')).toBeTruthy();
|
||||
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
|
||||
|
||||
expect(cleanStyle).toEqual(
|
||||
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:purple;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should cleanup the style and use the existing ID', () => {
|
||||
const cleanStyle = svgStyleCleanup(svgWithId);
|
||||
const updatedStyle = getSvgStyle(cleanStyle);
|
||||
const svgId = getSvgId(cleanStyle);
|
||||
|
||||
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
|
||||
expect(svgId).toBeDefined();
|
||||
expect(svgId).toEqual(ID);
|
||||
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
|
||||
|
||||
expect(cleanStyle).toEqual(
|
||||
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:blue;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should cleanup the style and replace the wrong ID', () => {
|
||||
const cleanStyle = svgStyleCleanup(svgWithWrongIdInStyle);
|
||||
const updatedStyle = getSvgStyle(cleanStyle);
|
||||
const svgId = getSvgId(cleanStyle);
|
||||
|
||||
expect(cleanStyle.indexOf('id="')).toBeGreaterThan(-1);
|
||||
expect(svgId).toBeDefined();
|
||||
expect(svgId?.startsWith('x')).toBeTruthy();
|
||||
expect(updatedStyle?.indexOf(`#${svgId}`)).toBeGreaterThan(-1);
|
||||
|
||||
expect(cleanStyle).toEqual(
|
||||
`<svg id="${svgId}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style type="text/css">#${svgId} .st0{fill:green;}</style><circle cx="12" cy="12" r="10" class="st0"/></svg>`
|
||||
);
|
||||
});
|
||||
});
|
@ -3,8 +3,13 @@ import SVG, { Props } from 'react-inlinesvg';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
|
||||
export const SanitizedSVG = (props: Props) => {
|
||||
return <SVG {...props} cacheRequests={true} preProcessor={getCleanSVG} />;
|
||||
import { svgStyleCleanup } from './utils';
|
||||
|
||||
type SanitizedSVGProps = Props & { cleanStyle?: boolean };
|
||||
|
||||
export const SanitizedSVG = (props: SanitizedSVGProps) => {
|
||||
const { cleanStyle, ...inlineSvgProps } = props;
|
||||
return <SVG {...inlineSvgProps} cacheRequests={true} preProcessor={cleanStyle ? getCleanSVGAndStyle : getCleanSVG} />;
|
||||
};
|
||||
|
||||
let cache = new Map<string, string>();
|
||||
@ -15,5 +20,21 @@ function getCleanSVG(code: string): string {
|
||||
clean = textUtil.sanitizeSVGContent(code);
|
||||
cache.set(code, clean);
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
function getCleanSVGAndStyle(code: string): string {
|
||||
let clean = cache.get(code);
|
||||
if (!clean) {
|
||||
clean = textUtil.sanitizeSVGContent(code);
|
||||
|
||||
if (clean.indexOf('<style type="text/css">') > -1) {
|
||||
clean = svgStyleCleanup(clean);
|
||||
}
|
||||
|
||||
cache.set(code, clean);
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
30
public/app/core/components/SVG/utils.ts
Normal file
30
public/app/core/components/SVG/utils.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const MATCH_ID_INDEX = 2;
|
||||
const SVG_ID_INSERT_POS = 5;
|
||||
|
||||
export const getSvgStyle = (svgCode: string) => {
|
||||
const svgStyle = svgCode.match(new RegExp('<style type="text/css">([\\s\\S]*?)<\\/style>'));
|
||||
return svgStyle ? svgStyle[0] : null;
|
||||
};
|
||||
|
||||
export const getSvgId = (svgCode: string) => {
|
||||
return svgCode.match(new RegExp('<svg.*id\\s*=\\s*([\'"])(.*?)\\1'))?.[MATCH_ID_INDEX];
|
||||
};
|
||||
|
||||
export const svgStyleCleanup = (svgCode: string) => {
|
||||
let svgId = getSvgId(svgCode);
|
||||
if (!svgId) {
|
||||
svgId = `x${uuidv4()}`;
|
||||
const pos = svgCode.indexOf('<svg') + SVG_ID_INSERT_POS;
|
||||
svgCode = svgCode.substring(0, pos) + `id="${svgId}" ` + svgCode.substring(pos);
|
||||
}
|
||||
|
||||
let svgStyle = getSvgStyle(svgCode);
|
||||
if (svgStyle) {
|
||||
let replacedId = svgStyle.replace(/(#(.*?))?\./g, `#${svgId} .`);
|
||||
svgCode = svgCode.replace(svgStyle, replacedId);
|
||||
}
|
||||
|
||||
return svgCode;
|
||||
};
|
@ -60,6 +60,7 @@ export function IconDisplay(props: CanvasElementProps) {
|
||||
src={data.path}
|
||||
style={svgStyle}
|
||||
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
|
||||
cleanStyle={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user