From e1ff4dc9fe60d708d2976d8d715237cdbab0822c Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 15 Feb 2022 12:26:59 +0000 Subject: [PATCH] NewsPanel: Add support for Atom feeds (#45390) --- public/app/plugins/panel/news/NewsPanel.tsx | 7 ++-- public/app/plugins/panel/news/atom.test.ts | 16 +++++++++ public/app/plugins/panel/news/atom.ts | 19 +++++++++++ public/app/plugins/panel/news/feed.ts | 25 ++++++++++++++ .../app/plugins/panel/news/fixtures/atom.xml | 28 +++++++++++++++ .../app/plugins/panel/news/fixtures/rss.xml | 21 ++++++++++++ public/app/plugins/panel/news/module.tsx | 2 +- public/app/plugins/panel/news/rss.test.ts | 14 ++++++++ public/app/plugins/panel/news/rss.ts | 34 +++++-------------- public/app/plugins/panel/news/types.ts | 8 ++--- public/app/plugins/panel/news/utils.test.ts | 4 +-- public/app/plugins/panel/news/utils.ts | 4 +-- 12 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 public/app/plugins/panel/news/atom.test.ts create mode 100644 public/app/plugins/panel/news/atom.ts create mode 100644 public/app/plugins/panel/news/feed.ts create mode 100644 public/app/plugins/panel/news/fixtures/atom.xml create mode 100644 public/app/plugins/panel/news/fixtures/rss.xml create mode 100644 public/app/plugins/panel/news/rss.test.ts diff --git a/public/app/plugins/panel/news/NewsPanel.tsx b/public/app/plugins/panel/news/NewsPanel.tsx index e44151cf27a..c96dc1e5ac5 100755 --- a/public/app/plugins/panel/news/NewsPanel.tsx +++ b/public/app/plugins/panel/news/NewsPanel.tsx @@ -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 { ? `${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(frame), isError: false, diff --git a/public/app/plugins/panel/news/atom.test.ts b/public/app/plugins/panel/news/atom.test.ts new file mode 100644 index 00000000000..731c597187d --- /dev/null +++ b/public/app/plugins/panel/news/atom.test.ts @@ -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./ + ); + }); +}); diff --git a/public/app/plugins/panel/news/atom.ts b/public/app/plugins/panel/news/atom.ts new file mode 100644 index 00000000000..3e048752e8c --- /dev/null +++ b/public/app/plugins/panel/news/atom.ts @@ -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; +} diff --git a/public/app/plugins/panel/news/feed.ts b/public/app/plugins/panel/news/feed.ts new file mode 100644 index 00000000000..84056db4b18 --- /dev/null +++ b/public/app/plugins/panel/news/feed.ts @@ -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; +} diff --git a/public/app/plugins/panel/news/fixtures/atom.xml b/public/app/plugins/panel/news/fixtures/atom.xml new file mode 100644 index 00000000000..922e405c0f7 --- /dev/null +++ b/public/app/plugins/panel/news/fixtures/atom.xml @@ -0,0 +1,28 @@ + + + Jekyll + + + 2022-02-15T07:00:47+00:00 + https://www.example.com/feed.xml + Test Feed + An example of an atom feed, for testing + + Bobby Test + + + Why Testing Is The Best + + 2022-02-12T08:00:00+00:00 + 2022-02-12T08:00:00+00:00 + https://www.example.com/2022/02/12/why-testing-is-the-best + + Testing is the best because it lets you know your code isn't broken, probably. + + + Bobby Test + + + An example of a summary. + + diff --git a/public/app/plugins/panel/news/fixtures/rss.xml b/public/app/plugins/panel/news/fixtures/rss.xml new file mode 100644 index 00000000000..872da91715e --- /dev/null +++ b/public/app/plugins/panel/news/fixtures/rss.xml @@ -0,0 +1,21 @@ + + + + + RSS Feed Example + + https://www.example.net + A small description of this feed + en-US + + A fake item + https://www.example.net/2022/02/10/something-fake/ + + Bill Test + Thu, 10 Feb 2022 16:00:17 +0000 + Fake + + A description of a fake blog post + + + diff --git a/public/app/plugins/panel/news/module.tsx b/public/app/plugins/panel/news/module.tsx index 441f84452dc..34f0447b44e 100755 --- a/public/app/plugins/panel/news/module.tsx +++ b/public/app/plugins/panel/news/module.tsx @@ -9,7 +9,7 @@ export const plugin = new PanelPlugin(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, }, diff --git a/public/app/plugins/panel/news/rss.test.ts b/public/app/plugins/panel/news/rss.test.ts new file mode 100644 index 00000000000..5c30bcc0e00 --- /dev/null +++ b/public/app/plugins/panel/news/rss.test.ts @@ -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'); + }); +}); diff --git a/public/app/plugins/panel/news/rss.ts b/public/app/plugins/panel/news/rss.ts index 974548e03a4..11968637bb4 100644 --- a/public/app/plugins/panel/news/rss.ts +++ b/public/app/plugins/panel/news/rss.ts @@ -1,37 +1,19 @@ -import { RssFeed, RssItem } from './types'; +import { getProperty } from './feed'; +import { Feed } from './types'; -export async function loadRSSFeed(url: string): Promise { - 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; } diff --git a/public/app/plugins/panel/news/types.ts b/public/app/plugins/panel/news/types.ts index d87f1f8b977..1ebaa5f11a1 100755 --- a/public/app/plugins/panel/news/types.ts +++ b/public/app/plugins/panel/news/types.ts @@ -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; diff --git a/public/app/plugins/panel/news/utils.test.ts b/public/app/plugins/panel/news/utils.test.ts index 9a808bedc2b..fee1cbf2ffb 100644 --- a/public/app/plugins/panel/news/utils.test.ts +++ b/public/app/plugins/panel/news/utils.test.ts @@ -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; diff --git a/public/app/plugins/panel/news/utils.ts b/public/app/plugins/panel/news/utils.ts index 17d6ce7cbbb..1b737221548 100644 --- a/public/app/plugins/panel/news/utils.ts +++ b/public/app/plugins/panel/news/utils.ts @@ -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([]); const title = new ArrayVector([]); const link = new ArrayVector([]);