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';
|
import { textUtil } from '@grafana/data';
|
||||||
|
|
||||||
export const SanitizedSVG = (props: Props) => {
|
import { svgStyleCleanup } from './utils';
|
||||||
return <SVG {...props} cacheRequests={true} preProcessor={getCleanSVG} />;
|
|
||||||
|
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>();
|
let cache = new Map<string, string>();
|
||||||
@ -15,5 +20,21 @@ function getCleanSVG(code: string): string {
|
|||||||
clean = textUtil.sanitizeSVGContent(code);
|
clean = textUtil.sanitizeSVGContent(code);
|
||||||
cache.set(code, clean);
|
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;
|
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}
|
src={data.path}
|
||||||
style={svgStyle}
|
style={svgStyle}
|
||||||
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
|
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
|
||||||
|
cleanStyle={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user