mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
NewsPanel: add news as a builtin panel (#21128)
This commit is contained in:
@@ -73,4 +73,12 @@ export class DataFrameView<T = any> implements Vector<T> {
|
||||
iterator(this.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
map<V>(iterator: (item: T, index: number) => V) {
|
||||
const acc: V[] = [];
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
acc.push(iterator(this.get(i), i));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,9 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
|
||||
textFaint: string;
|
||||
textEmphasis: string;
|
||||
|
||||
// panel
|
||||
panelBg: string;
|
||||
|
||||
// TODO: move to background section
|
||||
bodyBg: string;
|
||||
pageBg: string;
|
||||
|
||||
@@ -154,6 +154,7 @@ exports[`TimePicker renders buttons correctly 1`] = `
|
||||
"orangeDark": "#ff780a",
|
||||
"pageBg": "#161719",
|
||||
"pageHeaderBorder": "#343436",
|
||||
"panelBg": "#212124",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
@@ -460,6 +461,7 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
|
||||
"orangeDark": "#ff780a",
|
||||
"pageBg": "#161719",
|
||||
"pageHeaderBorder": "#343436",
|
||||
"panelBg": "#212124",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
|
||||
@@ -111,7 +111,7 @@ $hr-border-color: $dark-9;
|
||||
|
||||
// Panel
|
||||
// -------------------------
|
||||
$panel-bg: $dark-4;
|
||||
$panel-bg: ${theme.colors.panelBg};
|
||||
$panel-border: solid 1px $dark-1;
|
||||
$panel-header-hover-bg: $dark-9;
|
||||
$panel-corner: $panel-bg;
|
||||
|
||||
@@ -103,7 +103,7 @@ $hr-border-color: $gray-4 !default;
|
||||
|
||||
// Panel
|
||||
// -------------------------
|
||||
$panel-bg: $white;
|
||||
$panel-bg: ${theme.colors.panelBg};
|
||||
$panel-border: solid 1px $gray-5;
|
||||
$panel-header-hover-bg: $gray-6;
|
||||
$panel-corner: $gray-4;
|
||||
|
||||
@@ -75,6 +75,7 @@ const darkTheme: GrafanaTheme = {
|
||||
linkExternal: basicColors.blue,
|
||||
headingColor: basicColors.gray4,
|
||||
pageHeaderBorder: basicColors.dark9,
|
||||
panelBg: basicColors.dark4,
|
||||
|
||||
// Next-gen forms functional colors
|
||||
formLabel: basicColors.gray70,
|
||||
|
||||
@@ -3,3 +3,6 @@ import { getTheme, mockTheme } from './getTheme';
|
||||
import { selectThemeVariant } from './selectThemeVariant';
|
||||
export { stylesFactory } from './stylesFactory';
|
||||
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext };
|
||||
|
||||
import * as styleMixins from './mixins';
|
||||
export { styleMixins };
|
||||
|
||||
@@ -64,12 +64,16 @@ const lightTheme: GrafanaTheme = {
|
||||
critical: basicColors.redShade,
|
||||
bodyBg: basicColors.gray7,
|
||||
pageBg: basicColors.gray7,
|
||||
|
||||
// Text colors
|
||||
body: basicColors.gray1,
|
||||
text: basicColors.gray1,
|
||||
textStrong: basicColors.dark2,
|
||||
textWeak: basicColors.gray2,
|
||||
textEmphasis: basicColors.dark5,
|
||||
textFaint: basicColors.dark4,
|
||||
|
||||
// Link colors
|
||||
link: basicColors.gray1,
|
||||
linkDisabled: basicColors.gray3,
|
||||
linkHover: basicColors.dark1,
|
||||
@@ -77,6 +81,9 @@ const lightTheme: GrafanaTheme = {
|
||||
headingColor: basicColors.gray1,
|
||||
pageHeaderBorder: basicColors.gray4,
|
||||
|
||||
// panel
|
||||
panelBg: basicColors.white,
|
||||
|
||||
// Next-gen forms functional colors
|
||||
formLabel: basicColors.gray33,
|
||||
formDescription: basicColors.gray33,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
export function cardChrome(theme: GrafanaTheme): string {
|
||||
if (theme.isDark) {
|
||||
return `
|
||||
background: linear-gradient(135deg, ${theme.colors.dark8}, ${theme.colors.dark6});
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, ${theme.colors.dark9}, ${theme.colors.dark6});
|
||||
}
|
||||
box-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
background: linear-gradient(135deg, ${theme.colors.gray6}, ${theme.colors.gray5});
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, ${theme.colors.dark5}, ${theme.colors.gray6});
|
||||
}
|
||||
box-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as pieChartPanel from 'app/plugins/panel/piechart/module';
|
||||
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
import * as logsPanel from 'app/plugins/panel/logs/module';
|
||||
import * as newsPanel from 'app/plugins/panel/news/module';
|
||||
|
||||
const exampleApp = async () => await import(/* webpackChunkName: "exampleApp" */ 'app/plugins/app/example-app/module');
|
||||
|
||||
@@ -85,6 +86,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/panel/heatmap/module': heatmapPanel,
|
||||
'app/plugins/panel/table/module': tablePanel,
|
||||
'app/plugins/panel/table2/module': table2Panel,
|
||||
'app/plugins/panel/news/module': newsPanel,
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/stat/module': singlestatPanel2,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css } from 'emotion';
|
||||
|
||||
// Utils & Services
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, CustomScrollbar, styleMixins } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import { feedToDataFrame } from './utils';
|
||||
import { sanitize } from 'app/core/utils/text';
|
||||
import { loadRSSFeed } from './rss';
|
||||
|
||||
// Types
|
||||
import { PanelProps, DataFrameView, dateTime } from '@grafana/data';
|
||||
import { NewsOptions, NewsItem, DEFAULT_FEED_URL } from './types';
|
||||
|
||||
interface Props extends PanelProps<NewsOptions> {}
|
||||
|
||||
interface State {
|
||||
news?: DataFrameView<NewsItem>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export class NewsPanel extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.loadFeed();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props): void {
|
||||
if (this.props.options.feedUrl !== prevProps.options.feedUrl) {
|
||||
this.loadFeed();
|
||||
}
|
||||
}
|
||||
|
||||
async loadFeed() {
|
||||
const { options } = this.props;
|
||||
try {
|
||||
const url = options.feedUrl ?? DEFAULT_FEED_URL;
|
||||
const res = await loadRSSFeed(url);
|
||||
const frame = feedToDataFrame(res);
|
||||
this.setState({
|
||||
news: new DataFrameView<NewsItem>(frame),
|
||||
isError: false,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error Loading News', err);
|
||||
this.setState({
|
||||
news: undefined,
|
||||
isError: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isError, news } = this.state;
|
||||
const styles = getStyles(config.theme);
|
||||
|
||||
if (isError) {
|
||||
return <div>Error Loading News</div>;
|
||||
}
|
||||
if (!news) {
|
||||
return <div>loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CustomScrollbar>
|
||||
{news.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={styles.item}>
|
||||
<a href={item.link} target="_blank">
|
||||
<div className={styles.title}>{item.title}</div>
|
||||
<div className={styles.date}>{dateTime(item.date).format('MMM DD')} </div>
|
||||
<div className={styles.content} dangerouslySetInnerHTML={{ __html: sanitize(item.content) }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
container: css`
|
||||
height: 100%;
|
||||
`,
|
||||
item: css`
|
||||
${styleMixins.cardChrome(theme)}
|
||||
padding: ${theme.spacing.sm};
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 3px;
|
||||
margin-right: ${theme.spacing.sm};
|
||||
`,
|
||||
title: css`
|
||||
color: ${theme.colors.linkExternal};
|
||||
max-width: calc(100% - 70px);
|
||||
font-size: 16px;
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
content: css`
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`,
|
||||
date: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: ${theme.colors.bodyBg};
|
||||
width: 55px;
|
||||
text-align: right;
|
||||
padding: ${theme.spacing.xs};
|
||||
font-weight: 500;
|
||||
border-radius: 0 0 0 3px;
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
}));
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormField, PanelOptionsGroup, Button } from '@grafana/ui';
|
||||
import { PanelEditorProps } from '@grafana/data';
|
||||
import { NewsOptions, DEFAULT_FEED_URL } from './types';
|
||||
|
||||
const PROXY_PREFIX = 'https://cors-anywhere.herokuapp.com/';
|
||||
|
||||
interface State {
|
||||
feedUrl: string;
|
||||
}
|
||||
|
||||
export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>, State> {
|
||||
constructor(props: PanelEditorProps<NewsOptions>) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
feedUrl: props.options.feedUrl,
|
||||
};
|
||||
}
|
||||
|
||||
onUpdatePanel = () =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
feedUrl: this.state.feedUrl,
|
||||
});
|
||||
|
||||
onFeedUrlChange = ({ target }: any) => this.setState({ feedUrl: target.value });
|
||||
|
||||
onSetProxyPrefix = () => {
|
||||
const feedUrl = PROXY_PREFIX + this.state.feedUrl;
|
||||
this.setState({ feedUrl });
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
feedUrl,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const feedUrl = this.state.feedUrl || '';
|
||||
const suggestProxy = feedUrl && !feedUrl.startsWith(PROXY_PREFIX);
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGroup title="Feed">
|
||||
<div className="gf-form">
|
||||
<FormField
|
||||
label="URL"
|
||||
labelWidth={4}
|
||||
inputWidth={30}
|
||||
value={feedUrl || ''}
|
||||
placeholder={DEFAULT_FEED_URL}
|
||||
onChange={this.onFeedUrlChange}
|
||||
onBlur={this.onUpdatePanel}
|
||||
/>
|
||||
</div>
|
||||
{suggestProxy && (
|
||||
<div>
|
||||
<br />
|
||||
<div>If the feed is unable to connect, consider a CORS proxy</div>
|
||||
<Button variant="inverse" onClick={this.onSetProxyPrefix}>
|
||||
Use Proxy
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PanelOptionsGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 395.569 395.569"
|
||||
style="enable-background:new 0 0 395.569 395.569; fill:#1573B9"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M365.11,81.124c-2.3-29.794-27.261-53.339-57.635-53.339H57.826C25.941,27.785,0,53.726,0,85.61v224.35
|
||||
c0,31.884,25.941,57.825,57.826,57.825h279.918c31.885,0,57.826-25.941,57.826-57.825V132.03
|
||||
C395.569,110.043,383.225,90.899,365.11,81.124z M333.99,309.96c0,14.619-11.894,26.514-26.514,26.514H57.826
|
||||
c-14.62,0-26.514-11.895-26.514-26.514V85.61c0-14.619,11.894-26.514,26.514-26.514h249.65c14.62,0,26.514,11.895,26.514,26.514
|
||||
V309.96z"/>
|
||||
<path d="M62.901,145.138h0.652c4.505,0,8.156-3.651,8.156-8.157v-28.756l24.358,33.548c1.536,2.113,3.988,3.365,6.601,3.365h0.791
|
||||
c0.03,0,0.06-0.006,0.088-0.006c0.041,0,0.081,0.006,0.123,0.006h0.652c4.504,0,8.156-3.651,8.156-8.157V83.399
|
||||
c0-4.505-3.652-8.157-8.156-8.157h-0.652c-4.506,0-8.157,3.652-8.157,8.157v28.755L71.156,78.606
|
||||
c-1.535-2.113-3.989-3.364-6.6-3.364h-0.792c-0.03,0-0.058,0.006-0.088,0.006c-0.042,0-0.082-0.006-0.123-0.006h-0.652
|
||||
c-4.505,0-8.156,3.652-8.156,8.157v53.582C54.745,141.487,58.395,145.138,62.901,145.138z"/>
|
||||
<path d="M162.419,128.824h-25.441v-10.971h20.764c4.504,0,8.156-3.651,8.156-8.155v-0.653c0-4.506-3.652-8.157-8.156-8.157h-20.764
|
||||
v-8.681h24.349c4.505,0,8.157-3.651,8.157-8.156v-0.652c0-4.505-3.651-8.157-8.157-8.157h-33.158c-4.505,0-8.157,3.652-8.157,8.157
|
||||
v53.582c0,4.506,3.652,8.157,8.157,8.157h34.251c4.504,0,8.157-3.651,8.157-8.157C170.576,132.477,166.924,128.824,162.419,128.824
|
||||
z"/>
|
||||
<path d="M198.958,145.138h0.501c0.011,0,0.021-0.002,0.032-0.002c0.01,0,0.02,0.002,0.032,0.002h0.421
|
||||
c3.502,0,6.612-2.235,7.731-5.553l10.196-30.274l10.148,30.265c1.114,3.324,4.227,5.563,7.733,5.563h0.501
|
||||
c0.011,0,0.02-0.002,0.032-0.002s0.022,0.002,0.033,0.002h0.421c3.495,0,6.602-2.227,7.724-5.537l18.168-53.583
|
||||
c0.845-2.489,0.437-5.235-1.095-7.371c-1.531-2.137-4-3.405-6.628-3.405h-0.835c-3.516,0-6.636,2.253-7.742,5.59L236.268,111.2
|
||||
l-10.11-30.376c-1.109-3.333-4.227-5.581-7.739-5.581h-1.14c-3.517,0-6.636,2.253-7.742,5.59L199.474,111.2l-10.109-30.376
|
||||
c-1.109-3.333-4.227-5.581-7.74-5.581h-0.834c-2.629,0-5.097,1.268-6.63,3.405c-1.532,2.136-1.94,4.882-1.095,7.371l18.168,53.583
|
||||
C192.355,142.911,195.462,145.138,198.958,145.138z"/>
|
||||
<path d="M289.496,145.138c18.084,0,26.178-10.525,26.177-20.893c0.125-16.176-13.955-20.431-22.368-22.973
|
||||
c-9.335-2.822-10.215-3.955-10.215-6.244c0-1.362,3.264-2.82,8.12-2.82c3.25,0,7.099,0.954,9.36,2.318
|
||||
c3.816,2.306,8.773,1.12,11.135-2.661l0.299-0.479c1.155-1.848,1.521-4.082,1.018-6.202c-0.501-2.121-1.832-3.952-3.693-5.085
|
||||
c-4.992-3.041-11.765-4.857-18.119-4.857c-17.33,0-25.087,9.937-25.087,19.786c0,15.75,13.332,19.788,22.153,22.459
|
||||
c9.605,2.909,10.453,4.065,10.432,6.699c0,2.739-4.776,3.986-9.212,3.986c-4.31,0-9.038-1.84-11.766-4.579
|
||||
c-1.529-1.536-3.605-2.399-5.77-2.402c-0.003,0-0.005,0-0.008,0c-2.162,0-4.239,0.86-5.768,2.39l-0.445,0.446
|
||||
c-1.543,1.543-2.404,3.64-2.389,5.822c0.015,2.183,0.904,4.269,2.467,5.791C271.838,141.499,280.911,145.138,289.496,145.138z"/>
|
||||
<path d="M147.338,168.909H69.755c-8.646,0-15.656,7.009-15.656,15.656c0,8.646,7.009,15.656,15.656,15.656h77.582
|
||||
c8.646,0,15.656-7.01,15.656-15.656C162.994,175.918,155.984,168.909,147.338,168.909z"/>
|
||||
<path d="M147.338,221.094H69.755c-8.646,0-15.656,7.01-15.656,15.656c0,8.646,7.009,15.656,15.656,15.656h77.582
|
||||
c8.646,0,15.656-7.009,15.656-15.656C162.994,228.104,155.984,221.094,147.338,221.094z"/>
|
||||
<path d="M147.338,273.281H69.755c-8.646,0-15.656,7.009-15.656,15.656c0,8.646,7.009,15.656,15.656,15.656h77.582
|
||||
c8.646,0,15.656-7.01,15.656-15.656C162.994,280.29,155.984,273.281,147.338,273.281z"/>
|
||||
<path d="M306.61,166.698H186.967c-5.005,0-9.064,4.059-9.064,9.063V297.74c0,5.005,4.059,9.063,9.064,9.063H306.61
|
||||
c5.006,0,9.064-4.059,9.064-9.063V175.761C315.674,170.756,311.616,166.698,306.61,166.698z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
Executable
+6
@@ -0,0 +1,6 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { NewsPanel } from './NewsPanel';
|
||||
import { NewsPanelEditor } from './NewsPanelEditor';
|
||||
import { defaults, NewsOptions } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<NewsOptions>(NewsPanel).setDefaults(defaults).setEditor(NewsPanelEditor);
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "News Panel",
|
||||
"id": "news",
|
||||
|
||||
"skipDataQuery": true,
|
||||
|
||||
"state": "alpha",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/news.svg",
|
||||
"large": "img/news.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RssFeed, RssItem } from './types';
|
||||
|
||||
export async function loadRSSFeed(url: string): Promise<RssFeed> {
|
||||
const rsp = await fetch(url);
|
||||
const txt = await rsp.text();
|
||||
const domParser = new DOMParser();
|
||||
const doc = domParser.parseFromString(txt, 'text/xml');
|
||||
const feed: RssFeed = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
doc.querySelectorAll('item').forEach(node => {
|
||||
const item: RssItem = {
|
||||
title: node.querySelector('title').textContent,
|
||||
link: node.querySelector('link').textContent,
|
||||
content: node.querySelector('description').textContent,
|
||||
pubDate: node.querySelector('pubDate').textContent,
|
||||
};
|
||||
|
||||
feed.items.push(item);
|
||||
});
|
||||
|
||||
return feed;
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
// TODO: when grafana blog has CORS headers updated, remove the cors-anywhere prefix
|
||||
export const DEFAULT_FEED_URL = 'https://cors-anywhere.herokuapp.com/' + 'https://grafana.com/blog/index.xml';
|
||||
|
||||
export interface NewsOptions {
|
||||
feedUrl?: string;
|
||||
}
|
||||
|
||||
export const defaults: NewsOptions = {
|
||||
// will default to grafana blog
|
||||
};
|
||||
|
||||
export interface NewsItem {
|
||||
date: number;
|
||||
title: string;
|
||||
link: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for rss-parser
|
||||
*/
|
||||
export interface RssFeed {
|
||||
title?: string;
|
||||
description?: string;
|
||||
items: RssItem[];
|
||||
}
|
||||
|
||||
export interface RssItem {
|
||||
title: string;
|
||||
link: string;
|
||||
pubDate?: string;
|
||||
content?: string;
|
||||
contentSnippet?: string;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { feedToDataFrame } from './utils';
|
||||
import { RssFeed, NewsItem } from './types';
|
||||
import { DataFrameView } from '@grafana/data';
|
||||
|
||||
describe('news', () => {
|
||||
test('convert RssFeed to DataFrame', () => {
|
||||
const frame = feedToDataFrame(grafana20191216);
|
||||
expect(frame.length).toBe(5);
|
||||
|
||||
// Iterate the links
|
||||
const view = new DataFrameView<NewsItem>(frame);
|
||||
const links = view.map((item: NewsItem) => {
|
||||
return item.link;
|
||||
});
|
||||
expect(links).toEqual([
|
||||
'https://grafana.com/blog/2019/12/13/meet-the-grafana-labs-team-aengus-rooney/',
|
||||
'https://grafana.com/blog/2019/12/12/register-now-grafanacon-2020-is-coming-to-amsterdam-may-13-14/',
|
||||
'https://grafana.com/blog/2019/12/10/pro-tips-dashboard-navigation-using-links/',
|
||||
'https://grafana.com/blog/2019/12/09/how-to-do-automatic-annotations-with-grafana-and-loki/',
|
||||
'https://grafana.com/blog/2019/12/06/meet-the-grafana-labs-team-ward-bekker/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const grafana20191216 = {
|
||||
items: [
|
||||
{
|
||||
title: 'Meet the Grafana Labs Team: Aengus Rooney',
|
||||
link: 'https://grafana.com/blog/2019/12/13/meet-the-grafana-labs-team-aengus-rooney/',
|
||||
pubDate: 'Fri, 13 Dec 2019 00:00:00 +0000',
|
||||
content: '\n\n<p>As Grafana Labs continues to grow, we’d like you to get to know the team members...',
|
||||
},
|
||||
{
|
||||
title: 'Register Now! GrafanaCon 2020 Is Coming to Amsterdam May 13-14',
|
||||
link: 'https://grafana.com/blog/2019/12/12/register-now-grafanacon-2020-is-coming-to-amsterdam-may-13-14/',
|
||||
pubDate: 'Thu, 12 Dec 2019 00:00:00 +0000',
|
||||
content: '\n\n<p>Amsterdam, we’re coming back!</p>\n\n<p>Mark your calendars for May 13-14, 2020....',
|
||||
},
|
||||
{
|
||||
title: 'Pro Tips: Dashboard Navigation Using Links',
|
||||
link: 'https://grafana.com/blog/2019/12/10/pro-tips-dashboard-navigation-using-links/',
|
||||
pubDate: 'Tue, 10 Dec 2019 00:00:00 +0000',
|
||||
content:
|
||||
'\n\n<p>Great dashboards answer a limited set of related questions. If you try to answer too many questions in a single dashboard, it can become overly complex. ...',
|
||||
},
|
||||
{
|
||||
title: 'How to Do Automatic Annotations with Grafana and Loki',
|
||||
link: 'https://grafana.com/blog/2019/12/09/how-to-do-automatic-annotations-with-grafana-and-loki/',
|
||||
pubDate: 'Mon, 09 Dec 2019 00:00:00 +0000',
|
||||
content:
|
||||
'\n\n<p>Grafana annotations are great! They clearly mark the occurrence of an event to help operators and devs correlate events with metrics. You may not be aware of this, but Grafana can automatically annotate graphs by ...',
|
||||
},
|
||||
{
|
||||
title: 'Meet the Grafana Labs Team: Ward Bekker',
|
||||
link: 'https://grafana.com/blog/2019/12/06/meet-the-grafana-labs-team-ward-bekker/',
|
||||
pubDate: 'Fri, 06 Dec 2019 00:00:00 +0000',
|
||||
content:
|
||||
'\n\n<p>As Grafana Labs continues to grow, we’d like you to get to know the team members who are building the cool stuff you’re using. Check out the latest of our Friday team profiles.</p>\n\n<h2 id="meet-ward">Meet Ward!</h2>\n\n<p><strong>Name:</strong> Ward...',
|
||||
},
|
||||
],
|
||||
feedUrl: 'https://grafana.com/blog/index.xml',
|
||||
title: 'Blog on Grafana Labs',
|
||||
description: 'Recent content in Blog on Grafana Labs',
|
||||
generator: 'Hugo -- gohugo.io',
|
||||
link: 'https://grafana.com/blog/',
|
||||
language: 'en-us',
|
||||
lastBuildDate: 'Fri, 13 Dec 2019 00:00:00 +0000',
|
||||
} as RssFeed;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { RssFeed } from './types';
|
||||
import { ArrayVector, FieldType, DataFrame, dateTime } from '@grafana/data';
|
||||
|
||||
export function feedToDataFrame(feed: RssFeed): DataFrame {
|
||||
const date = new ArrayVector<number>([]);
|
||||
const title = new ArrayVector<string>([]);
|
||||
const link = new ArrayVector<string>([]);
|
||||
const content = new ArrayVector<string>([]);
|
||||
|
||||
for (const item of feed.items) {
|
||||
const val = dateTime(item.pubDate);
|
||||
|
||||
try {
|
||||
date.buffer.push(val.valueOf());
|
||||
title.buffer.push(item.title);
|
||||
link.buffer.push(item.link);
|
||||
|
||||
let body = item.content.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
|
||||
if (body && body.length > 300) {
|
||||
body = body.substr(0, 300);
|
||||
}
|
||||
|
||||
content.buffer.push(body);
|
||||
} catch (err) {
|
||||
console.warn('Error reading news item:', err, item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fields: [
|
||||
{ name: 'date', type: FieldType.time, config: { title: 'Date' }, values: date },
|
||||
{ name: 'title', type: FieldType.string, config: {}, values: title },
|
||||
{ name: 'link', type: FieldType.string, config: {}, values: link },
|
||||
{ name: 'content', type: FieldType.string, config: {}, values: content },
|
||||
],
|
||||
length: date.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user