mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AnnotationList: Support html content (#54916)
* support html content in annolistpanel * improve panel tests * add RenderUserContentAsHTML ui * sanitize content
This commit is contained in:
parent
6969354490
commit
a91d77003d
@ -0,0 +1,22 @@
|
||||
import { Props } from '@storybook/addon-docs/blocks';
|
||||
import { RenderUserContentAsHTML } from './RenderUserContentAsHTML';
|
||||
|
||||
<Meta title="MDX|RenderUserContentAsHTML" component={RenderUserContentAsHTML} />
|
||||
|
||||
# RenderUserContentAsHTML
|
||||
|
||||
Abstraction layer component for sanitizing and rendering an html content.
|
||||
|
||||
### When to use
|
||||
|
||||
This should be use as replacement for `dangerouslySetInnerHTML` as this centralizes the sanitation of contents over the app.
|
||||
|
||||
### Usage
|
||||
|
||||
```jsx
|
||||
<RenderUserContentAsHTML content="sample content" />
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<Props of={RenderUserContentAsHTML} />
|
@ -0,0 +1,33 @@
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { RenderUserContentAsHTML } from './RenderUserContentAsHTML';
|
||||
import mdx from './RenderUserContentAsHTML.mdx';
|
||||
|
||||
const meta: ComponentMeta<typeof RenderUserContentAsHTML> = {
|
||||
title: 'General/RenderUserContentAsHTML',
|
||||
component: RenderUserContentAsHTML,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
content: {
|
||||
control: { type: 'text' },
|
||||
},
|
||||
component: {
|
||||
control: { type: 'text' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic: ComponentStory<typeof RenderUserContentAsHTML> = (props) => {
|
||||
return <RenderUserContentAsHTML {...props} />;
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
content: '<a href="#">sample html anchor tag link</a>',
|
||||
};
|
||||
|
||||
export default meta;
|
@ -0,0 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { RenderUserContentAsHTML } from './RenderUserContentAsHTML';
|
||||
|
||||
describe('RenderUserContentAsHTML', () => {
|
||||
it('should render html content', () => {
|
||||
render(<RenderUserContentAsHTML content='<a href="#">sample content</a>' />);
|
||||
expect(screen.getByRole('link', { name: /sample content/ })).toBeInTheDocument();
|
||||
});
|
||||
it('should render a raw string content', () => {
|
||||
render(<RenderUserContentAsHTML content="sample content" />);
|
||||
expect(screen.getByText(/sample content/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import React, { HTMLAttributes, PropsWithChildren } from 'react';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
|
||||
export interface RenderUserContentAsHTMLProps<T = HTMLSpanElement>
|
||||
extends Omit<HTMLAttributes<T>, 'dangerouslySetInnerHTML'> {
|
||||
component?: keyof React.ReactHTML;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function RenderUserContentAsHTML<T>({
|
||||
component,
|
||||
content,
|
||||
...rest
|
||||
}: PropsWithChildren<RenderUserContentAsHTMLProps<T>>): JSX.Element {
|
||||
return React.createElement(component || 'span', {
|
||||
dangerouslySetInnerHTML: { __html: textUtil.sanitize(content) },
|
||||
...rest,
|
||||
});
|
||||
}
|
@ -84,6 +84,7 @@ export { Tab } from './Tabs/Tab';
|
||||
export { VerticalTab } from './Tabs/VerticalTab';
|
||||
export { TabContent } from './Tabs/TabContent';
|
||||
export { Counter } from './Tabs/Counter';
|
||||
export { RenderUserContentAsHTML } from './RenderUserContentAsHTML/RenderUserContentAsHTML';
|
||||
|
||||
// Visualizations
|
||||
export {
|
||||
|
@ -132,6 +132,16 @@ describe('AnnoListPanel', () => {
|
||||
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders annotation item's html content", async () => {
|
||||
const { getMock } = await setupTestContext({
|
||||
results: [{ ...defaultResult, text: '<a href="">test link </a> ' }],
|
||||
});
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByRole('link')).toBeInTheDocument();
|
||||
expect(getMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('and login property is missing in annotation', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({ results: [{ ...defaultResult, login: undefined }] });
|
||||
@ -203,8 +213,8 @@ describe('AnnoListPanel', () => {
|
||||
const { getMock, pushSpy } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText(/result text/i));
|
||||
expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: /result text/i }));
|
||||
await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/api/search', { dashboardUIDs: '7MeksYbmk' });
|
||||
@ -218,8 +228,9 @@ describe('AnnoListPanel', () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Result tag B'));
|
||||
|
||||
expect(screen.getByRole('button', { name: /result tag b/i })).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: /result tag b/i }));
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import React, { FC, MouseEvent } from 'react';
|
||||
|
||||
import { AnnotationEvent, DateTimeInput, GrafanaTheme2, PanelProps } from '@grafana/data';
|
||||
import { Card, TagList, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Card, TagList, Tooltip, RenderUserContentAsHTML, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PanelOptions } from './models.gen';
|
||||
|
||||
@ -24,7 +24,7 @@ export const AnnotationListItem: FC<Props> = ({
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { showUser, showTags, showTime } = options;
|
||||
const { text, login, email, avatarUrl, tags, time, timeEnd } = annotation;
|
||||
const { text = '', login, email, avatarUrl, tags, time, timeEnd } = annotation;
|
||||
const onItemClick = () => {
|
||||
onClick(annotation);
|
||||
};
|
||||
@ -38,7 +38,13 @@ export const AnnotationListItem: FC<Props> = ({
|
||||
return (
|
||||
<Card className={styles.card} onClick={onItemClick}>
|
||||
<Card.Heading>
|
||||
<span>{text}</span>
|
||||
<RenderUserContentAsHTML
|
||||
className={styles.heading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
content={text}
|
||||
/>
|
||||
</Card.Heading>
|
||||
{showTimeStamp && (
|
||||
<Card.Description className={styles.timestamp}>
|
||||
@ -118,6 +124,16 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
margin: theme.spacing(0.5),
|
||||
width: 'inherit',
|
||||
}),
|
||||
heading: css({
|
||||
a: {
|
||||
zIndex: 1,
|
||||
position: 'relative',
|
||||
color: theme.colors.text.link,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
}),
|
||||
meta: css({
|
||||
margin: 0,
|
||||
position: 'relative',
|
||||
|
Loading…
Reference in New Issue
Block a user