mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -06:00
NewsPanel: Add support for Atom feeds (#45390)
This commit is contained in:
parent
146eed400a
commit
e1ff4dc9fe
@ -6,7 +6,7 @@ import { CustomScrollbar, stylesFactory } from '@grafana/ui';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import { feedToDataFrame } from './utils';
|
||||
import { loadRSSFeed } from './rss';
|
||||
import { loadFeed } from './feed';
|
||||
|
||||
// Types
|
||||
import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme2, textUtil } from '@grafana/data';
|
||||
@ -55,8 +55,9 @@ export class NewsPanel extends PureComponent<Props, State> {
|
||||
? `${PROXY_PREFIX}${options.feedUrl}`
|
||||
: options.feedUrl
|
||||
: DEFAULT_FEED_URL;
|
||||
const res = await loadRSSFeed(url);
|
||||
const frame = feedToDataFrame(res);
|
||||
|
||||
const feed = await loadFeed(url);
|
||||
const frame = feedToDataFrame(feed);
|
||||
this.setState({
|
||||
news: new DataFrameView<NewsItem>(frame),
|
||||
isError: false,
|
||||
|
16
public/app/plugins/panel/news/atom.test.ts
Normal file
16
public/app/plugins/panel/news/atom.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { parseAtomFeed } from './atom';
|
||||
import fs from 'fs';
|
||||
|
||||
describe('Atom feed parser', () => {
|
||||
it('should successfully parse an atom feed', async () => {
|
||||
const atomFile = fs.readFileSync(`${__dirname}/fixtures/atom.xml`, 'utf8');
|
||||
const parsedFeed = parseAtomFeed(atomFile);
|
||||
expect(parsedFeed.items).toHaveLength(1);
|
||||
expect(parsedFeed.items[0].title).toBe('Why Testing Is The Best');
|
||||
expect(parsedFeed.items[0].link).toBe('https://www.example.com/2022/02/12/why-testing-is-the-best/');
|
||||
expect(parsedFeed.items[0].pubDate).toBe('2022-02-12T08:00:00+00:00');
|
||||
expect(parsedFeed.items[0].content).toMatch(
|
||||
/Testing is the best because it lets you know your code isn't broken, probably./
|
||||
);
|
||||
});
|
||||
});
|
19
public/app/plugins/panel/news/atom.ts
Normal file
19
public/app/plugins/panel/news/atom.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { getProperty } from './feed';
|
||||
import { Feed } from './types';
|
||||
|
||||
export function parseAtomFeed(txt: string): Feed {
|
||||
const domParser = new DOMParser();
|
||||
const doc = domParser.parseFromString(txt, 'text/xml');
|
||||
|
||||
const feed: Feed = {
|
||||
items: Array.from(doc.querySelectorAll('entry')).map((node) => ({
|
||||
title: getProperty(node, 'title'),
|
||||
link: node.querySelector('link')?.getAttribute('href') ?? '',
|
||||
content: getProperty(node, 'content'),
|
||||
pubDate: getProperty(node, 'published'),
|
||||
ogImage: node.querySelector("meta[property='og:image']")?.getAttribute('content'),
|
||||
})),
|
||||
};
|
||||
|
||||
return feed;
|
||||
}
|
25
public/app/plugins/panel/news/feed.ts
Normal file
25
public/app/plugins/panel/news/feed.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { parseAtomFeed } from './atom';
|
||||
import { parseRSSFeed } from './rss';
|
||||
|
||||
export async function fetchFeedText(url: string) {
|
||||
const rsp = await fetch(url);
|
||||
const txt = await rsp.text();
|
||||
return txt;
|
||||
}
|
||||
|
||||
export function isAtomFeed(txt: string) {
|
||||
const domParser = new DOMParser();
|
||||
const doc = domParser.parseFromString(txt, 'text/xml');
|
||||
return doc.querySelector('feed') !== null;
|
||||
}
|
||||
|
||||
export function getProperty(node: Element, property: string): string {
|
||||
const propNode = node.querySelector(property);
|
||||
return propNode?.textContent ?? '';
|
||||
}
|
||||
|
||||
export async function loadFeed(url: string) {
|
||||
const res = await fetchFeedText(url);
|
||||
const parsedFeed = isAtomFeed(res) ? parseAtomFeed(res) : parseRSSFeed(res);
|
||||
return parsedFeed;
|
||||
}
|
28
public/app/plugins/panel/news/fixtures/atom.xml
Normal file
28
public/app/plugins/panel/news/fixtures/atom.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<generator uri="https://jekyllrb.com/" version="3.9.0">Jekyll</generator>
|
||||
<link href="https://www.example.com/feed.xml" rel="self" type="application/atom+xml" />
|
||||
<link href="https://www.example.com/" rel="alternate" type="text/html" />
|
||||
<updated>2022-02-15T07:00:47+00:00</updated>
|
||||
<id>https://www.example.com/feed.xml</id>
|
||||
<title type="html">Test Feed</title>
|
||||
<subtitle>An example of an atom feed, for testing</subtitle>
|
||||
<author>
|
||||
<name>Bobby Test</name>
|
||||
</author>
|
||||
<entry>
|
||||
<title type="html">Why Testing Is The Best</title>
|
||||
<link href="https://www.example.com/2022/02/12/why-testing-is-the-best/" rel="alternate" type="text/html" title="Why Testing Is The Best" />
|
||||
<published>2022-02-12T08:00:00+00:00</published>
|
||||
<updated>2022-02-12T08:00:00+00:00</updated>
|
||||
<id>https://www.example.com/2022/02/12/why-testing-is-the-best</id>
|
||||
<content type="html" xml:base="https://www.hugohaggmark.com/2022/02/12/why-testing-is-the-best/">
|
||||
Testing is the best because it lets you know your code isn't broken, probably.
|
||||
</content>
|
||||
<author>
|
||||
<name>Bobby Test</name>
|
||||
</author>
|
||||
<category term="Testing" />
|
||||
<summary type="html">An example of a summary.</summary>
|
||||
</entry>
|
||||
</feed>
|
21
public/app/plugins/panel/news/fixtures/rss.xml
Normal file
21
public/app/plugins/panel/news/fixtures/rss.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
|
||||
|
||||
<channel>
|
||||
<title>RSS Feed Example</title>
|
||||
<atom:link href="https://www.example.net/feed" rel="self" type="application/rss+xml" />
|
||||
<link>https://www.example.net</link>
|
||||
<description>A small description of this feed</description>
|
||||
<language>en-US</language>
|
||||
<item>
|
||||
<title>A fake item</title>
|
||||
<link>https://www.example.net/2022/02/10/something-fake/</link>
|
||||
|
||||
<dc:creator>Bill Test</dc:creator>
|
||||
<pubDate>Thu, 10 Feb 2022 16:00:17 +0000</pubDate>
|
||||
<category>Fake</category>
|
||||
|
||||
<description>A description of a fake blog post</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -9,7 +9,7 @@ export const plugin = new PanelPlugin<PanelOptions>(NewsPanel).setPanelOptions((
|
||||
.addTextInput({
|
||||
path: 'feedUrl',
|
||||
name: 'URL',
|
||||
description: 'Only RSS feed formats are supported (not Atom).',
|
||||
description: 'Supports RSS and Atom feeds',
|
||||
settings: {
|
||||
placeholder: DEFAULT_FEED_URL,
|
||||
},
|
||||
|
14
public/app/plugins/panel/news/rss.test.ts
Normal file
14
public/app/plugins/panel/news/rss.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { parseRSSFeed } from './rss';
|
||||
import fs from 'fs';
|
||||
|
||||
describe('RSS feed parser', () => {
|
||||
it('should successfully parse an rss feed', async () => {
|
||||
const rssFile = fs.readFileSync(`${__dirname}/fixtures/rss.xml`, 'utf8');
|
||||
const parsedFeed = parseRSSFeed(rssFile);
|
||||
expect(parsedFeed.items).toHaveLength(1);
|
||||
expect(parsedFeed.items[0].title).toBe('A fake item');
|
||||
expect(parsedFeed.items[0].link).toBe('https://www.example.net/2022/02/10/something-fake/');
|
||||
expect(parsedFeed.items[0].pubDate).toBe('Thu, 10 Feb 2022 16:00:17 +0000');
|
||||
expect(parsedFeed.items[0].content).toBe('A description of a fake blog post');
|
||||
});
|
||||
});
|
@ -1,37 +1,19 @@
|
||||
import { RssFeed, RssItem } from './types';
|
||||
import { getProperty } from './feed';
|
||||
import { Feed } from './types';
|
||||
|
||||
export async function loadRSSFeed(url: string): Promise<RssFeed> {
|
||||
const rsp = await fetch(url);
|
||||
const txt = await rsp.text();
|
||||
export function parseRSSFeed(txt: string): Feed {
|
||||
const domParser = new DOMParser();
|
||||
const doc = domParser.parseFromString(txt, 'text/xml');
|
||||
const feed: RssFeed = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
const getProperty = (node: Element, property: string) => {
|
||||
const propNode = node.querySelector(property);
|
||||
if (propNode) {
|
||||
return propNode.textContent ?? '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
doc.querySelectorAll('item').forEach((node) => {
|
||||
const item: RssItem = {
|
||||
const feed: Feed = {
|
||||
items: Array.from(doc.querySelectorAll('item')).map((node) => ({
|
||||
title: getProperty(node, 'title'),
|
||||
link: getProperty(node, 'link'),
|
||||
content: getProperty(node, 'description'),
|
||||
pubDate: getProperty(node, 'pubDate'),
|
||||
};
|
||||
|
||||
const imageNode = node.querySelector("meta[property='og:image']");
|
||||
if (imageNode) {
|
||||
item.ogImage = imageNode.getAttribute('content');
|
||||
}
|
||||
|
||||
feed.items.push(item);
|
||||
});
|
||||
ogImage: node.querySelector("meta[property='og:image']")?.getAttribute('content'),
|
||||
})),
|
||||
};
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
@ -7,15 +7,15 @@ export interface NewsItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for rss-parser
|
||||
* Helper interface for feed parser
|
||||
*/
|
||||
export interface RssFeed {
|
||||
export interface Feed {
|
||||
title?: string;
|
||||
description?: string;
|
||||
items: RssItem[];
|
||||
items: FeedItem[];
|
||||
}
|
||||
|
||||
export interface RssItem {
|
||||
export interface FeedItem {
|
||||
title: string;
|
||||
link: string;
|
||||
pubDate?: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { feedToDataFrame } from './utils';
|
||||
import { RssFeed, NewsItem } from './types';
|
||||
import { Feed, NewsItem } from './types';
|
||||
import { DataFrameView } from '@grafana/data';
|
||||
|
||||
describe('news', () => {
|
||||
@ -65,4 +65,4 @@ const grafana20191216 = {
|
||||
link: 'https://grafana.com/blog/',
|
||||
language: 'en-us',
|
||||
lastBuildDate: 'Fri, 13 Dec 2019 00:00:00 +0000',
|
||||
} as RssFeed;
|
||||
} as Feed;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RssFeed } from './types';
|
||||
import { Feed } from './types';
|
||||
import { ArrayVector, FieldType, DataFrame, dateTime } from '@grafana/data';
|
||||
|
||||
export function feedToDataFrame(feed: RssFeed): DataFrame {
|
||||
export function feedToDataFrame(feed: Feed): DataFrame {
|
||||
const date = new ArrayVector<number>([]);
|
||||
const title = new ArrayVector<string>([]);
|
||||
const link = new ArrayVector<string>([]);
|
||||
|
Loading…
Reference in New Issue
Block a user