Icon: Simplify and remove wrapping <div> (#76819)

* remove wrapping div

* update tests, just gotta figure out how to handle fontawesome :(

* add spinner.svg which matches the font-awesome spinner

* add mock for react-inlinesvg

* update mock and fix some tests

* fix FormField test

* fix CorrelationsPage tests

* increase timeout
This commit is contained in:
Ashley Harrison 2023-10-24 12:16:32 +01:00 committed by GitHub
parent cad3c43bb1
commit 07cc7504ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 95 additions and 84 deletions

View File

@ -28,6 +28,7 @@ module.exports = {
moduleNameMapper: {
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
'\\.css': '<rootDir>/public/test/mocks/style.ts',
'react-inlinesvg': '<rootDir>/public/test/mocks/react-inlinesvg.tsx',
'monaco-editor/esm/vs/editor/editor.api': '<rootDir>/public/test/mocks/monaco.ts',
// near-membrane-dom won't work in a nodejs environment.
'@locker/near-membrane-dom': '<rootDir>/public/test/mocks/nearMembraneDom.ts',

View File

@ -93,6 +93,7 @@ export const availableIconsIndex = {
eye: true,
'eye-slash': true,
'ellipsis-h': true,
/* @deprecated, use 'spinner' instead */
'fa fa-spinner': true,
favorite: true,
'file-alt': true,
@ -198,6 +199,7 @@ export const availableIconsIndex = {
sitemap: true,
slack: true,
'sliders-v-alt': true,
spinner: true,
'sort-amount-down': true,
'sort-amount-up': true,
'square-shape': true,

View File

@ -84,7 +84,6 @@ describe('DataLinksListItem', () => {
setupTestContext({ link });
expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument();
expect(screen.getByTitle('')).toBeInTheDocument();
});
});
@ -109,7 +108,6 @@ describe('DataLinksListItem', () => {
setupTestContext({ link });
expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument();
expect(screen.getByTitle('')).toBeInTheDocument();
});
});
});

View File

@ -68,7 +68,7 @@ export const TimeRangeInput = ({
onChange(timeRange);
};
const onRangeClear = (event: MouseEvent<HTMLDivElement>) => {
const onRangeClear = (event: MouseEvent<SVGElement>) => {
event.stopPropagation();
const from = dateTime(null);
const to = dateTime(null);

View File

@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@ -42,8 +42,6 @@ describe('FormField', () => {
screen.getAllByRole('textbox')[0].focus();
await userEvent.tab();
await waitFor(() => {
screen.getByText(tooltip);
});
expect(await screen.findByText(tooltip)).toBeInTheDocument();
});
});

View File

@ -37,7 +37,7 @@ export const FormLabel = ({
{children}
{tooltip && (
<Tooltip placement="top" content={tooltip} theme={'info'} interactive={interactive}>
<Icon tabIndex={0} name="info-circle" size="sm" style={{ marginLeft: '10px' }} />
<Icon name="info-circle" size="sm" style={{ marginLeft: '10px' }} />
</Tooltip>
)}
</label>

View File

@ -9,7 +9,7 @@ import { IconName, IconType, IconSize } from '../../types/icon';
import { getIconRoot, getIconSubDir, getSvgSize } from './utils';
export interface IconProps extends React.HTMLAttributes<HTMLDivElement> {
export interface IconProps extends Omit<React.SVGProps<SVGElement>, 'onLoad' | 'onError' | 'ref'> {
name: IconName;
size?: IconSize;
type?: IconType;
@ -18,16 +18,14 @@ export interface IconProps extends React.HTMLAttributes<HTMLDivElement> {
const getIconStyles = (theme: GrafanaTheme2) => {
return {
// line-height: 0; is needed for correct icon alignment in Safari
container: css({
label: 'Icon',
display: 'inline-block',
lineHeight: 0,
}),
icon: css({
verticalAlign: 'middle',
display: 'inline-block',
fill: 'currentColor',
flexShrink: 0,
label: 'Icon',
// line-height: 0; is needed for correct icon alignment in Safari
lineHeight: 0,
verticalAlign: 'middle',
}),
orange: css({
fill: theme.v1.palette.orange,
@ -35,53 +33,44 @@ const getIconStyles = (theme: GrafanaTheme2) => {
};
};
export const Icon = React.forwardRef<HTMLDivElement, IconProps>(
({ size = 'md', type = 'default', name, className, style, title = '', ...divElementProps }, ref) => {
export const Icon = React.forwardRef<SVGElement, IconProps>(
({ size = 'md', type = 'default', name, className, style, title = '', ...rest }, ref) => {
const styles = useStyles2(getIconStyles);
/* Temporary solution to display also font awesome icons */
if (name?.startsWith('fa fa-')) {
return <i className={getFontAwesomeIconStyles(name, className)} {...divElementProps} style={style} />;
}
if (!isIconName(name)) {
console.warn('Icon component passed an invalid icon name', name);
}
if (!name || name.includes('..')) {
return <div ref={ref}>invalid icon name</div>;
}
// handle the deprecated 'fa fa-spinner'
const iconName: IconName = name === 'fa fa-spinner' ? 'spinner' : name;
const iconRoot = getIconRoot();
const svgSize = getSvgSize(size);
const svgHgt = svgSize;
const svgWid = name.startsWith('gf-bar-align') ? 16 : name.startsWith('gf-interp') ? 30 : svgSize;
const subDir = getIconSubDir(name, type);
const svgPath = `${iconRoot}${subDir}/${name}.svg`;
const subDir = getIconSubDir(iconName, type);
const svgPath = `${iconRoot}${subDir}/${iconName}.svg`;
return (
<div className={styles.container} {...divElementProps} ref={ref}>
<SVG
src={svgPath}
width={svgWid}
height={svgHgt}
title={title}
className={cx(styles.icon, className, type === 'mono' ? { [styles.orange]: name === 'favorite' } : '')}
style={style}
/>
</div>
<SVG
innerRef={ref}
src={svgPath}
width={svgWid}
height={svgHgt}
title={title}
className={cx(
styles.icon,
{
'fa-spin': iconName === 'spinner',
},
className,
type === 'mono' ? { [styles.orange]: name === 'favorite' } : ''
)}
style={style}
{...rest}
/>
);
}
);
Icon.displayName = 'Icon';
function getFontAwesomeIconStyles(iconName: string, className?: string): string {
return cx(
iconName,
{
'fa-spin': iconName === 'fa fa-spinner',
},
className
);
}

View File

@ -112,7 +112,7 @@ export class RefreshPicker extends PureComponent<Props> {
tooltip={tooltip}
onClick={onRefresh}
variant={variant}
icon={isLoading ? 'fa fa-spinner' : 'sync'}
icon={isLoading ? 'spinner' : 'sync'}
style={width ? { width } : undefined}
data-testid={selectors.components.RefreshPicker.runButtonV2}
>

View File

@ -28,7 +28,7 @@ export const Spinner = ({ className, inline = false, iconClassName, style, size
const styles = getStyles(size, inline);
return (
<div data-testid="Spinner" style={style} className={cx(styles, className)}>
<Icon className={cx('fa-spin', iconClassName)} name="fa fa-spinner" aria-label="loading spinner" />
<Icon className={cx('fa-spin', iconClassName)} name="spinner" aria-label="loading spinner" />
</div>
);
};

View File

@ -6,7 +6,7 @@ export interface Props {
label: string;
removeIcon: boolean;
count: number;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onClick?: React.MouseEventHandler<SVGElement>;
}
export class TagBadge extends React.Component<Props> {

View File

@ -91,7 +91,7 @@ export const GlobalConfigForm = ({ config, alertManagerSourceName }: Props) => {
{!readOnly && (
<>
{loading && (
<Button disabled={true} icon="fa fa-spinner" variant="primary">
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}

View File

@ -203,7 +203,7 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
</Field>
<div className={styles.buttons}>
{loading && (
<Button disabled={true} icon="fa fa-spinner" variant="primary">
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}

View File

@ -159,7 +159,7 @@ export function ChannelSubForm<R extends ChannelValues>({
variant="secondary"
type="button"
onClick={() => handleTest()}
icon={testingReceiver ? 'fa fa-spinner' : 'message'}
icon={testingReceiver ? 'spinner' : 'message'}
>
Test
</Button>

View File

@ -186,7 +186,7 @@ export function ReceiverForm<R extends ChannelValues>({
{isEditable && (
<>
{isSubmitting && (
<Button disabled={true} icon="fa fa-spinner" variant="primary">
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}

View File

@ -488,7 +488,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
{config.expressionsEnabled && <TypeSelectorButton onClickType={onClickType} />}
{isPreviewLoading && (
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={cancelQueries}>
<Button icon="spinner" type="button" variant="destructive" onClick={cancelQueries}>
Cancel
</Button>
)}

View File

@ -242,7 +242,7 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
</FieldSet>
<div className={styles.flexRow}>
{loading && (
<Button disabled={true} icon="fa fa-spinner" variant="primary">
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}

View File

@ -292,7 +292,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(await screen.findByRole('button', { name: /add$/i }));
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
});
// the table showing correlations should have appeared
expect(await screen.findByRole('table')).toBeInTheDocument();
@ -439,7 +441,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /add$/i }));
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
});
// the table showing correlations should have appeared
expect(await screen.findByRole('table')).toBeInTheDocument();
@ -474,7 +478,9 @@ describe('CorrelationsPage', () => {
expect(screen.queryByRole('cell', { name: /some label$/i })).not.toBeInTheDocument();
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_deleted');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_deleted');
});
});
it('correctly edits correlations', async () => {
@ -486,7 +492,9 @@ describe('CorrelationsPage', () => {
const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i });
await userEvent.click(rowExpanderButton);
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_details_expanded');
});
await userEvent.clear(screen.getByRole('textbox', { name: /label/i }));
await userEvent.type(screen.getByRole('textbox', { name: /label/i }), 'edited label');
@ -500,9 +508,11 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
expect(await screen.findByRole('cell', { name: /edited label$/i })).toBeInTheDocument();
expect(await screen.findByRole('cell', { name: /edited label$/i }, { timeout: 5000 })).toBeInTheDocument();
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_edited');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited');
});
});
it('correctly edits transformations', async () => {
@ -553,7 +563,9 @@ describe('CorrelationsPage', () => {
expect(screen.getByText('Please define an expression')).toBeInTheDocument();
await userEvent.type(screen.getByLabelText(/expression/i), 'test expression');
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_edited');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited');
});
});
});
@ -684,7 +696,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(rowExpanderButton);
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded');
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_details_expanded');
});
// form elements should be readonly
const labelInput = await screen.findByRole('textbox', { name: /label/i });

View File

@ -11,7 +11,7 @@ export const CorrelationFormNavigation = () => {
const { readOnly, loading, correlation } = useCorrelationsFormContext();
const LastPageNext = !readOnly && (
<Button variant="primary" icon={loading ? 'fa fa-spinner' : 'save'} type="submit" disabled={loading}>
<Button variant="primary" icon={loading ? 'spinner' : 'save'} type="submit" disabled={loading}>
{correlation === undefined ? 'Add' : 'Save'}
</Button>
);

View File

@ -45,7 +45,7 @@ export const SaveDashboardForm = ({ dashboard, onCancel, onSubmit, onSuccess, sa
<Button variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<Button type="submit" disabled={!hasChanges} icon={saving ? 'fa fa-spinner' : undefined}>
<Button type="submit" disabled={!hasChanges} icon={saving ? 'spinner' : undefined}>
Save
</Button>
{!hasChanges && <div>No changes to save</div>}

View File

@ -126,7 +126,7 @@ export const SaveDashboardForm = ({
<Button
type="submit"
disabled={!saveModel.hasChanges || isLoading}
icon={saving ? 'fa fa-spinner' : undefined}
icon={saving ? 'spinner' : undefined}
aria-label={selectors.pages.SaveDashboardModal.save}
>
{isLoading ? 'Saving...' : 'Save'}

View File

@ -92,12 +92,7 @@ export const PlaylistForm = ({ onSubmit, playlist }: Props) => {
</div>
<HorizontalGroup>
<Button
type="submit"
variant="primary"
disabled={isDisabled}
icon={saving ? 'fa fa-spinner' : undefined}
>
<Button type="submit" variant="primary" disabled={isDisabled} icon={saving ? 'spinner' : undefined}>
<Trans i18nKey="playlist-edit.form.save">Save</Trans>
</Button>
<LinkButton variant="secondary" href={`${config.appSubUrl}/playlists`}>

View File

@ -132,7 +132,7 @@ export const MoveToFolderModal = ({ results, onMoveItems, onDismiss }: Props) =>
<Button variant="secondary" onClick={onDismiss} fill="outline">
Cancel
</Button>
<Button icon={moving ? 'fa fa-spinner' : undefined} variant="primary" onClick={moveTo}>
<Button icon={moving ? 'spinner' : undefined} variant="primary" onClick={moveTo}>
Move
</Button>
</HorizontalGroup>

View File

@ -138,7 +138,9 @@ describe('VisualMetricQueryEditor', () => {
expect(screen.getByText('metric.test_label')).toBeInTheDocument();
const service = await screen.findByLabelText('Service');
openMenu(service);
await select(service, 'Srv 2', { container: document.body });
await act(async () => {
await select(service, 'Srv 2', { container: document.body });
});
expect(onChange).toBeCalledWith(expect.objectContaining({ filters: ['metric.type', '=', 'type2'] }));
expect(query).toEqual(defaultQuery);
expect(screen.queryByText('metric.test_label')).not.toBeInTheDocument();

View File

@ -88,8 +88,12 @@ exports[`VariableQueryEditor renders correctly 1`] = `
<div
className="css-zyjsuv-input-suffix"
>
<div
className="css-1j2891d-Icon"
<svg
className="css-1d3xu67-Icon"
height={16}
id="public/img/icons/unicons/angle-down.svg"
title=""
width={16}
/>
</div>
</div>

View File

@ -107,7 +107,7 @@ const QueryHeader = ({
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
icon={data?.state === LoadingState.Loading ? 'spinner' : undefined}
disabled={data?.state === LoadingState.Loading || emptyLogsExpression}
>
Run queries

View File

@ -285,7 +285,7 @@ describe('LogGroupsSelector', () => {
/>
);
await userEvent.click(screen.getByText('Select log groups'));
await screen.getByRole('button', { name: 'select-clear-value' }).click();
await userEvent.click(screen.getByRole('button', { name: 'select-clear-value' }));
await userEvent.click(screen.getByText('Add log groups'));
expect(onChange).toHaveBeenCalledWith([
{

View File

@ -176,7 +176,7 @@ export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
icon={data?.state === LoadingState.Loading ? 'spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
{queries && queries.length > 1 ? `Run queries` : `Run query`}

View File

@ -130,7 +130,7 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
icon={data?.state === LoadingState.Loading ? 'spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run queries

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 24 24"><path d="M6.8042,15A.99956.99956,0,0,0,5.438,14.63379l-1.73242,1a1.00016,1.00016,0,0,0,1,1.73242l1.73242-1A1.00073,1.00073,0,0,0,6.8042,15ZM3.70557,8.36621l1.73242,1a1.00016,1.00016,0,1,0,1-1.73242l-1.73242-1a1.00016,1.00016,0,0,0-1,1.73242ZM6,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H5A1,1,0,0,0,6,12ZM17.1958,9a1.0006,1.0006,0,0,0,1.36621.36621l1.73242-1a1.00016,1.00016,0,1,0-1-1.73242l-1.73242,1A1.00073,1.00073,0,0,0,17.1958,9ZM15,6.8042A1.0006,1.0006,0,0,0,16.36621,6.438l1-1.73242a1.00016,1.00016,0,1,0-1.73242-1l-1,1.73242A1.00073,1.00073,0,0,0,15,6.8042Zm5.29443,8.82959-1.73242-1a1.00016,1.00016,0,1,0-1,1.73242l1.73242,1a1.00016,1.00016,0,0,0,1-1.73242ZM16.36621,17.562a1.00016,1.00016,0,1,0-1.73242,1l1,1.73242a1.00016,1.00016,0,1,0,1.73242-1ZM21,11H19a1,1,0,0,0,0,2h2a1,1,0,0,0,0-2Zm-9,7a1,1,0,0,0-1,1v2a1,1,0,0,0,2,0V19A1,1,0,0,0,12,18Zm-3-.8042a.99954.99954,0,0,0-1.36621.36621l-1,1.73242a1.00016,1.00016,0,0,0,1.73242,1l1-1.73242A1.00073,1.00073,0,0,0,9,17.1958ZM12,2a1,1,0,0,0-1,1V5a1,1,0,0,0,2,0V3A1,1,0,0,0,12,2Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M5.1,16c-0.3-0.5-0.9-0.6-1.4-0.4c-0.5,0.3-0.6,0.9-0.4,1.4c0.3,0.5,0.9,0.6,1.4,0.4C5.2,17.1,5.3,16.5,5.1,16C5.1,16,5.1,16,5.1,16z M4.7,6.6C4.2,6.4,3.6,6.5,3.3,7C3.1,7.5,3.2,8.1,3.7,8.4C4.2,8.6,4.8,8.5,5.1,8C5.3,7.5,5.2,6.9,4.7,6.6z M20.3,8.4c0.5-0.3,0.6-0.9,0.4-1.4c-0.3-0.5-0.9-0.6-1.4-0.4c-0.5,0.3-0.6,0.9-0.4,1.4C19.2,8.5,19.8,8.6,20.3,8.4z M4,12c0-0.6-0.4-1-1-1s-1,0.4-1,1s0.4,1,1,1S4,12.6,4,12z M7.2,18.8c-0.5,0.1-0.9,0.7-0.7,1.2c0.1,0.5,0.7,0.9,1.2,0.7c0.5-0.1,0.9-0.7,0.7-1.2C8.3,19,7.8,18.7,7.2,18.8z M21,11c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S21.6,11,21,11z M20.3,15.6c-0.5-0.3-1.1-0.1-1.4,0.4c-0.3,0.5-0.1,1.1,0.4,1.4c0.5,0.3,1.1,0.1,1.4-0.4c0,0,0,0,0,0C20.9,16.5,20.8,15.9,20.3,15.6z M17,3.3c-0.5-0.3-1.1-0.1-1.4,0.4c-0.3,0.5-0.1,1.1,0.4,1.4c0.5,0.3,1.1,0.1,1.4-0.4c0,0,0,0,0,0C17.6,4.2,17.5,3.6,17,3.3z M16.8,18.8c-0.5-0.1-1.1,0.2-1.2,0.7c-0.1,0.5,0.2,1.1,0.7,1.2c0.5,0.1,1.1-0.2,1.2-0.7C17.6,19.5,17.3,19,16.8,18.8z M12,20c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S12.6,20,12,20z M12,2c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S12.6,2,12,2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1664" height="1728" viewBox="0 0 1664 1728">
<path fill="currentColor" d="M462 1394q0 53-37.5 90.5T334 1522q-52 0-90-38t-38-90q0-53 37.5-90.5T334 1266t90.5 37.5T462 1394zm498 206q0 53-37.5 90.5T832 1728t-90.5-37.5T704 1600t37.5-90.5T832 1472t90.5 37.5T960 1600zM256 896q0 53-37.5 90.5T128 1024t-90.5-37.5T0 896t37.5-90.5T128 768t90.5 37.5T256 896zm1202 498q0 52-38 90t-90 38q-53 0-90.5-37.5T1202 1394t37.5-90.5t90.5-37.5t90.5 37.5t37.5 90.5zM494 398q0 66-47 113t-113 47t-113-47t-47-113t47-113t113-47t113 47t47 113zm1170 498q0 53-37.5 90.5T1536 1024t-90.5-37.5T1408 896t37.5-90.5T1536 768t90.5 37.5T1664 896zm-640-704q0 80-56 136t-136 56t-136-56t-56-136t56-136T832 0t136 56t56 136zm530 206q0 93-66 158.5T1330 622q-93 0-158.5-65.5T1106 398q0-92 65.5-158t158.5-66q92 0 158 66t66 158z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 840 B

View File

@ -0,0 +1,7 @@
import React from 'react';
export default function ReactInlineSVG({ src, innerRef, cacheRequests, preProcessor, ...rest }) {
return <svg id={src} ref={innerRef} {...rest} />;
}
export const cacheStore = {};