mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 10:50:37 -06:00
NewsPanel: add news as a builtin panel (#21128)
This commit is contained in:
parent
22ff0eab15
commit
b8c0924ab1
@ -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,
|
||||
|
21
packages/grafana-ui/src/themes/mixins.ts
Normal file
21
packages/grafana-ui/src/themes/mixins.ts
Normal file
@ -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,
|
||||
|
126
public/app/plugins/panel/news/NewsPanel.tsx
Executable file
126
public/app/plugins/panel/news/NewsPanel.tsx
Executable file
@ -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
public/app/plugins/panel/news/NewsPanelEditor.tsx
Executable file
68
public/app/plugins/panel/news/NewsPanelEditor.tsx
Executable file
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
48
public/app/plugins/panel/news/img/news.svg
Normal file
48
public/app/plugins/panel/news/img/news.svg
Normal file
@ -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 |
6
public/app/plugins/panel/news/module.tsx
Executable file
6
public/app/plugins/panel/news/module.tsx
Executable file
@ -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);
|
20
public/app/plugins/panel/news/plugin.json
Executable file
20
public/app/plugins/panel/news/plugin.json
Executable file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
24
public/app/plugins/panel/news/rss.ts
Normal file
24
public/app/plugins/panel/news/rss.ts
Normal file
@ -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;
|
||||
}
|
34
public/app/plugins/panel/news/types.ts
Executable file
34
public/app/plugins/panel/news/types.ts
Executable file
@ -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;
|
||||
}
|
68
public/app/plugins/panel/news/utils.test.ts
Normal file
68
public/app/plugins/panel/news/utils.test.ts
Normal file
@ -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;
|
39
public/app/plugins/panel/news/utils.ts
Normal file
39
public/app/plugins/panel/news/utils.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user