NewsPanel.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { css, cx } from '@emotion/css';
  2. import React, { PureComponent } from 'react';
  3. import { Unsubscribable } from 'rxjs';
  4. import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme2, textUtil } from '@grafana/data';
  5. import { RefreshEvent } from '@grafana/runtime';
  6. import { CustomScrollbar, stylesFactory } from '@grafana/ui';
  7. import config from 'app/core/config';
  8. import { DEFAULT_FEED_URL } from './constants';
  9. import { loadFeed } from './feed';
  10. import { PanelOptions } from './models.gen';
  11. import { NewsItem } from './types';
  12. import { feedToDataFrame } from './utils';
  13. interface Props extends PanelProps<PanelOptions> {}
  14. interface State {
  15. news?: DataFrameView<NewsItem>;
  16. isError?: boolean;
  17. }
  18. export class NewsPanel extends PureComponent<Props, State> {
  19. private refreshSubscription: Unsubscribable;
  20. constructor(props: Props) {
  21. super(props);
  22. this.refreshSubscription = this.props.eventBus.subscribe(RefreshEvent, this.loadChannel.bind(this));
  23. this.state = {};
  24. }
  25. componentDidMount(): void {
  26. this.loadChannel();
  27. }
  28. componentWillUnmount(): void {
  29. this.refreshSubscription.unsubscribe();
  30. }
  31. componentDidUpdate(prevProps: Props): void {
  32. if (this.props.options.feedUrl !== prevProps.options.feedUrl) {
  33. this.loadChannel();
  34. }
  35. }
  36. async loadChannel() {
  37. const { options } = this.props;
  38. try {
  39. const url = options.feedUrl || DEFAULT_FEED_URL;
  40. const feed = await loadFeed(url);
  41. const frame = feedToDataFrame(feed);
  42. this.setState({
  43. news: new DataFrameView<NewsItem>(frame),
  44. isError: false,
  45. });
  46. } catch (err) {
  47. console.error('Error Loading News', err);
  48. this.setState({
  49. news: undefined,
  50. isError: true,
  51. });
  52. }
  53. }
  54. render() {
  55. const { width } = this.props;
  56. const { showImage } = this.props.options;
  57. const { isError, news } = this.state;
  58. const styles = getStyles(config.theme2);
  59. const useWideLayout = width > 600;
  60. if (isError) {
  61. return <div>Error loading RSS feed.</div>;
  62. }
  63. if (!news) {
  64. return <div>Loading...</div>;
  65. }
  66. return (
  67. <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
  68. {news.map((item, index) => {
  69. return (
  70. <article key={index} className={cx(styles.item, useWideLayout && styles.itemWide)}>
  71. {showImage && item.ogImage && (
  72. <a
  73. tabIndex={-1}
  74. href={textUtil.sanitizeUrl(item.link)}
  75. target="_blank"
  76. rel="noopener noreferrer"
  77. className={cx(styles.socialImage, useWideLayout && styles.socialImageWide)}
  78. aria-hidden
  79. >
  80. <img src={item.ogImage} alt={item.title} />
  81. </a>
  82. )}
  83. <div className={styles.body}>
  84. <time className={styles.date} dateTime={dateTimeFormat(item.date, { format: 'MMM DD' })}>
  85. {dateTimeFormat(item.date, { format: 'MMM DD' })}{' '}
  86. </time>
  87. <a
  88. className={styles.link}
  89. href={textUtil.sanitizeUrl(item.link)}
  90. target="_blank"
  91. rel="noopener noreferrer"
  92. >
  93. <h3 className={styles.title}>{item.title}</h3>
  94. </a>
  95. <div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
  96. </div>
  97. </article>
  98. );
  99. })}
  100. </CustomScrollbar>
  101. );
  102. }
  103. }
  104. const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
  105. container: css`
  106. height: 100%;
  107. `,
  108. item: css`
  109. display: flex;
  110. padding: ${theme.spacing(1)};
  111. position: relative;
  112. margin-bottom: 4px;
  113. margin-right: ${theme.spacing(1)};
  114. border-bottom: 2px solid ${theme.colors.border.weak};
  115. background: ${theme.colors.background.primary};
  116. flex-direction: column;
  117. flex-shrink: 0;
  118. `,
  119. itemWide: css`
  120. flex-direction: row;
  121. `,
  122. body: css`
  123. display: flex;
  124. flex-direction: column;
  125. `,
  126. socialImage: css`
  127. display: flex;
  128. align-items: center;
  129. margin-bottom: ${theme.spacing(1)};
  130. > img {
  131. width: 100%;
  132. border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0;
  133. }
  134. `,
  135. socialImageWide: css`
  136. margin-right: ${theme.spacing(2)};
  137. margin-bottom: 0;
  138. > img {
  139. width: 250px;
  140. border-radius: ${theme.shape.borderRadius()};
  141. }
  142. `,
  143. link: css`
  144. color: ${theme.colors.text.link};
  145. display: inline-block;
  146. &:hover {
  147. color: ${theme.colors.text.link};
  148. text-decoration: underline;
  149. }
  150. `,
  151. title: css`
  152. font-size: 16px;
  153. margin-bottom: ${theme.spacing(0.5)};
  154. `,
  155. content: css`
  156. p {
  157. margin-bottom: 4px;
  158. color: ${theme.colors.text};
  159. }
  160. `,
  161. date: css`
  162. margin-bottom: ${theme.spacing(0.5)};
  163. font-weight: 500;
  164. border-radius: 0 0 0 3px;
  165. color: ${theme.colors.text.secondary};
  166. `,
  167. }));