NewsPanel: Add support for showing social image before content (#33949)

* NewsPanel: Add support for showing social image before content

* Fix link and spacing

* Add wide layout
This commit is contained in:
Torkel Ödegaard 2021-05-11 21:16:36 +02:00 committed by GitHub
parent 95a356a840
commit f346bafdc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 29 deletions

View File

@ -9,11 +9,11 @@ import { feedToDataFrame } from './utils';
import { loadRSSFeed } from './rss'; import { loadRSSFeed } from './rss';
// Types // Types
import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme, textUtil } from '@grafana/data'; import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme2, textUtil } from '@grafana/data';
import { NewsItem } from './types'; import { NewsItem } from './types';
import { PanelOptions } from './models.gen'; import { PanelOptions } from './models.gen';
import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants'; import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants';
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
interface Props extends PanelProps<PanelOptions> {} interface Props extends PanelProps<PanelOptions> {}
@ -63,8 +63,11 @@ export class NewsPanel extends PureComponent<Props, State> {
} }
render() { render() {
const { width } = this.props;
const { showImage } = this.props.options;
const { isError, news } = this.state; const { isError, news } = this.state;
const styles = getStyles(config.theme); const styles = getStyles(config.theme2);
const useWideLayout = width > 600;
if (isError) { if (isError) {
return <div>Error Loading News</div>; return <div>Error Loading News</div>;
@ -77,17 +80,29 @@ export class NewsPanel extends PureComponent<Props, State> {
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%"> <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
{news.map((item, index) => { {news.map((item, index) => {
return ( return (
<div key={index} className={styles.item}> <div key={index} className={cx(styles.item, useWideLayout && styles.itemWide)}>
<a {showImage && item.ogImage && (
className={styles.link} <a
href={textUtil.sanitizeUrl(item.link)} href={textUtil.sanitizeUrl(item.link)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> className={cx(styles.socialImage, useWideLayout && styles.socialImageWide)}
<div className={styles.title}>{item.title}</div> >
<img src={item.ogImage} />
</a>
)}
<div className={styles.body}>
<div className={styles.date}>{dateTimeFormat(item.date, { format: 'MMM DD' })} </div> <div className={styles.date}>{dateTimeFormat(item.date, { format: 'MMM DD' })} </div>
</a> <a
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} /> className={styles.link}
href={textUtil.sanitizeUrl(item.link)}
target="_blank"
rel="noopener noreferrer"
>
<div className={styles.title}>{item.title}</div>
</a>
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
</div>
</div> </div>
); );
})} })}
@ -96,29 +111,53 @@ export class NewsPanel extends PureComponent<Props, State> {
} }
} }
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
container: css` container: css`
height: 100%; height: 100%;
`, `,
item: css` item: css`
padding: ${theme.spacing.sm}; display: flex;
padding: ${theme.spacing(1)};
position: relative; position: relative;
margin-bottom: 4px; margin-bottom: 4px;
margin-right: ${theme.spacing.sm}; margin-right: ${theme.spacing(1)};
border-bottom: 2px solid ${theme.colors.border1}; border-bottom: 2px solid ${theme.colors.border.weak};
background: ${theme.colors.background.primary};
flex-direction: column;
`,
itemWide: css`
flex-direction: row;
`,
body: css``,
socialImage: css`
display: flex;
align-items: center;
margin-bottom: ${theme.spacing(1)};
> img {
width: 100%;
border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0;
}
`,
socialImageWide: css`
margin-right: ${theme.spacing(2)};
margin-bottom: 0;
> img {
width: 250px;
border-radius: ${theme.shape.borderRadius()};
}
`, `,
link: css` link: css`
color: ${theme.colors.linkExternal}; color: ${theme.colors.text.link};
&:hover { &:hover {
color: ${theme.colors.linkExternal}; color: ${theme.colors.text.link};
text-decoration: underline; text-decoration: underline;
} }
`, `,
title: css` title: css`
max-width: calc(100% - 70px); max-width: calc(100% - 70px);
font-size: 16px; font-size: 16px;
margin-bottom: ${theme.spacing.sm}; margin-bottom: ${theme.spacing(0.5)};
`, `,
content: css` content: css`
p { p {
@ -127,15 +166,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
} }
`, `,
date: css` date: css`
position: absolute; margin-bottom: ${theme.spacing(0.5)};
top: 0;
right: 0;
background: ${theme.colors.panelBg};
width: 55px;
text-align: right;
padding: ${theme.spacing.xs};
font-weight: 500; font-weight: 500;
border-radius: 0 0 0 3px; border-radius: 0 0 0 3px;
color: ${theme.colors.textWeak}; color: ${theme.colors.text.secondary};
`, `,
})); }));

View File

@ -8,6 +8,7 @@ Family: {
// empty/missing will default to grafana blog // empty/missing will default to grafana blog
feedUrl?: string feedUrl?: string
useProxy?: bool useProxy?: bool
showImage?: bool | *true
} }
} }
] ]

View File

@ -8,6 +8,9 @@ export const modelVersion = Object.freeze([1, 0]);
export interface PanelOptions { export interface PanelOptions {
feedUrl?: string; feedUrl?: string;
useProxy?: boolean; useProxy?: boolean;
showImage?: boolean;
} }
export const defaultPanelOptions: PanelOptions = {}; export const defaultPanelOptions: PanelOptions = {
showImage: true,
};

View File

@ -15,6 +15,15 @@ export const plugin = new PanelPlugin<PanelOptions>(NewsPanel).setPanelOptions((
}, },
defaultValue: defaultPanelOptions.feedUrl, defaultValue: defaultPanelOptions.feedUrl,
}) })
.addBooleanSwitch({
path: 'showImage',
name: 'Show image',
description: 'Controls if the news item social (og:image) image is shown above text content',
showIf: (currentConfig: PanelOptions) => {
return isString(currentConfig.feedUrl) && !currentConfig.feedUrl.startsWith(PROXY_PREFIX);
},
defaultValue: defaultPanelOptions.showImage,
})
.addBooleanSwitch({ .addBooleanSwitch({
path: 'useProxy', path: 'useProxy',
name: 'Use Proxy', name: 'Use Proxy',

View File

@ -25,6 +25,11 @@ export async function loadRSSFeed(url: string): Promise<RssFeed> {
pubDate: getProperty(node, 'pubDate'), pubDate: getProperty(node, 'pubDate'),
}; };
const imageNode = node.querySelector("meta[property='og:image']");
if (imageNode) {
item.ogImage = imageNode.getAttribute('content');
}
feed.items.push(item); feed.items.push(item);
}); });

View File

@ -3,6 +3,7 @@ export interface NewsItem {
title: string; title: string;
link: string; link: string;
content: string; content: string;
ogImage?: string | null;
} }
/** /**
@ -20,4 +21,5 @@ export interface RssItem {
pubDate?: string; pubDate?: string;
content?: string; content?: string;
contentSnippet?: string; contentSnippet?: string;
ogImage?: string | null;
} }

View File

@ -6,6 +6,7 @@ export function feedToDataFrame(feed: RssFeed): DataFrame {
const title = new ArrayVector<string>([]); const title = new ArrayVector<string>([]);
const link = new ArrayVector<string>([]); const link = new ArrayVector<string>([]);
const content = new ArrayVector<string>([]); const content = new ArrayVector<string>([]);
const ogImage = new ArrayVector<string | undefined | null>([]);
for (const item of feed.items) { for (const item of feed.items) {
const val = dateTime(item.pubDate); const val = dateTime(item.pubDate);
@ -14,6 +15,7 @@ export function feedToDataFrame(feed: RssFeed): DataFrame {
date.buffer.push(val.valueOf()); date.buffer.push(val.valueOf());
title.buffer.push(item.title); title.buffer.push(item.title);
link.buffer.push(item.link); link.buffer.push(item.link);
ogImage.buffer.push(item.ogImage);
if (item.content) { if (item.content) {
const body = item.content.replace(/<\/?[^>]+(>|$)/g, ''); const body = item.content.replace(/<\/?[^>]+(>|$)/g, '');
@ -30,6 +32,7 @@ export function feedToDataFrame(feed: RssFeed): DataFrame {
{ name: 'title', type: FieldType.string, config: {}, values: title }, { name: 'title', type: FieldType.string, config: {}, values: title },
{ name: 'link', type: FieldType.string, config: {}, values: link }, { name: 'link', type: FieldType.string, config: {}, values: link },
{ name: 'content', type: FieldType.string, config: {}, values: content }, { name: 'content', type: FieldType.string, config: {}, values: content },
{ name: 'ogImage', type: FieldType.string, config: {}, values: ogImage },
], ],
length: date.length, length: date.length,
}; };