diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx
index 3bc672f544a..9eabed2d788 100644
--- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx
+++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx
@@ -163,7 +163,14 @@ const getStyles = (theme: GrafanaTheme2) => {
.drawer-open .drawer-content-wrapper {
box-shadow: ${theme.shadows.z3};
}
+
z-index: ${theme.zIndex.dropdown};
+
+ ${theme.breakpoints.down('sm')} {
+ .drawer-content-wrapper {
+ width: 100% !important;
+ }
+ }
`,
header: css`
background-color: ${theme.colors.background.canvas};
diff --git a/public/app/core/components/AppChrome/News/NewsContainer.test.tsx b/public/app/core/components/AppChrome/News/NewsContainer.test.tsx
new file mode 100644
index 00000000000..6036d11d9f8
--- /dev/null
+++ b/public/app/core/components/AppChrome/News/NewsContainer.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import fs from 'fs';
+import React from 'react';
+
+import { NewsContainer } from './NewsContainer';
+
+const setup = () => {
+ const { container } = render();
+
+ return { container };
+};
+
+describe('News', () => {
+ const result = fs.readFileSync(`${__dirname}/fixtures/news.xml`, 'utf8');
+ beforeEach(() => {
+ jest.resetAllMocks();
+ window.fetch = jest.fn().mockResolvedValue({ text: () => result });
+ });
+
+ it('should render the drawer when the drawer button is clicked', async () => {
+ setup();
+
+ await userEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('article')).toBeInTheDocument();
+ expect(screen.getByRole('link')).toHaveAttribute('href', 'https://www.example.net/2022/02/10/something-fake/');
+ });
+});
diff --git a/public/app/core/components/AppChrome/News/NewsContainer.tsx b/public/app/core/components/AppChrome/News/NewsContainer.tsx
new file mode 100644
index 00000000000..7ff107020a7
--- /dev/null
+++ b/public/app/core/components/AppChrome/News/NewsContainer.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { useToggle } from 'react-use';
+
+import { Drawer, Icon } from '@grafana/ui';
+import { DEFAULT_FEED_URL } from 'app/plugins/panel/news/constants';
+
+import { NewsWrapper } from './NewsWrapper';
+
+interface NewsContainerProps {
+ buttonCss?: string;
+}
+
+export function NewsContainer({ buttonCss }: NewsContainerProps) {
+ const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false);
+
+ const onChildClick = () => {
+ onToggleShowNewsDrawer(true);
+ };
+
+ return (
+ <>
+
+ {showNewsDrawer && (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/public/app/core/components/AppChrome/News/NewsWrapper.tsx b/public/app/core/components/AppChrome/News/NewsWrapper.tsx
new file mode 100644
index 00000000000..cff91cbffe2
--- /dev/null
+++ b/public/app/core/components/AppChrome/News/NewsWrapper.tsx
@@ -0,0 +1,56 @@
+import { css } from '@emotion/css';
+import React, { useEffect } from 'react';
+import AutoSizer from 'react-virtualized-auto-sizer';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { LoadingPlaceholder, useStyles2 } from '@grafana/ui';
+import { News } from 'app/plugins/panel/news/component/News';
+import { useNewsFeed } from 'app/plugins/panel/news/useNewsFeed';
+
+interface NewsWrapperProps {
+ feedUrl: string;
+}
+export function NewsWrapper({ feedUrl }: NewsWrapperProps) {
+ const styles = useStyles2(getStyles);
+ const { state, getNews } = useNewsFeed(feedUrl);
+ useEffect(() => {
+ getNews();
+ }, [getNews]);
+
+ if (state.loading || state.error) {
+ return (
+
+ {state.loading && }
+ {state.error && state.error.message}
+
+ );
+ }
+
+ if (!state.value) {
+ return null;
+ }
+
+ return (
+
+ {({ width }) => (
+
+ {state.value.map((_, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ innerWrapper: css`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `,
+ };
+};
diff --git a/public/app/core/components/AppChrome/News/fixtures/news.xml b/public/app/core/components/AppChrome/News/fixtures/news.xml
new file mode 100644
index 00000000000..872da91715e
--- /dev/null
+++ b/public/app/core/components/AppChrome/News/fixtures/news.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/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx
index 94a505f1a78..432e7a5d792 100644
--- a/public/app/core/components/AppChrome/TopSearchBar.tsx
+++ b/public/app/core/components/AppChrome/TopSearchBar.tsx
@@ -6,6 +6,7 @@ import { Dropdown, Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { useSelector } from 'app/types';
+import { NewsContainer } from './News/NewsContainer';
import { TopNavBarMenu } from './TopBar/TopNavBarMenu';
import { TopSearchBarInput } from './TopSearchBarInput';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
@@ -36,11 +37,7 @@ export function TopSearchBar() {
)}
-
-
-
+
{signInNode && (
diff --git a/public/app/core/components/AppChrome/constants.ts b/public/app/core/components/AppChrome/constants.ts
new file mode 100644
index 00000000000..92ca1590811
--- /dev/null
+++ b/public/app/core/components/AppChrome/constants.ts
@@ -0,0 +1 @@
+export const NEWS_FEED = 'https://grafana.com/blog/news.xml';
diff --git a/public/app/core/components/AppChrome/types.ts b/public/app/core/components/AppChrome/types.ts
index 654334fdea6..d307c26d351 100644
--- a/public/app/core/components/AppChrome/types.ts
+++ b/public/app/core/components/AppChrome/types.ts
@@ -1,5 +1,4 @@
import { NavModelItem } from '@grafana/data';
-
export const TOP_BAR_LEVEL_HEIGHT = 40;
export interface ToolbarUpdateProps {
diff --git a/public/app/plugins/panel/news/NewsPanel.tsx b/public/app/plugins/panel/news/NewsPanel.tsx
index 7e3a58ff365..73ebd6adb22 100644
--- a/public/app/plugins/panel/news/NewsPanel.tsx
+++ b/public/app/plugins/panel/news/NewsPanel.tsx
@@ -1,185 +1,52 @@
-import { css, cx } from '@emotion/css';
-import React, { PureComponent } from 'react';
-import { Unsubscribable } from 'rxjs';
+import React, { useEffect } from 'react';
-import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme2, textUtil } from '@grafana/data';
+import { PanelProps } from '@grafana/data';
import { RefreshEvent } from '@grafana/runtime';
-import { CustomScrollbar, stylesFactory } from '@grafana/ui';
-import config from 'app/core/config';
+import { CustomScrollbar } from '@grafana/ui';
+import { News } from './component/News';
import { DEFAULT_FEED_URL } from './constants';
-import { loadFeed } from './feed';
import { PanelOptions } from './models.gen';
-import { NewsItem } from './types';
-import { feedToDataFrame } from './utils';
+import { useNewsFeed } from './useNewsFeed';
-interface Props extends PanelProps {}
+interface NewsPanelProps extends PanelProps {}
-interface State {
- news?: DataFrameView;
- isError?: boolean;
+export function NewsPanel(props: NewsPanelProps) {
+ const {
+ width,
+ options: { feedUrl = DEFAULT_FEED_URL, showImage },
+ } = props;
+
+ const { state, getNews } = useNewsFeed(feedUrl);
+
+ useEffect(() => {
+ const sub = props.eventBus.subscribe(RefreshEvent, getNews);
+
+ return () => {
+ sub.unsubscribe();
+ };
+ }, [getNews, props.eventBus]);
+
+ useEffect(() => {
+ getNews();
+ }, [getNews]);
+
+ if (state.error) {
+ return Error loading RSS feed.
;
+ }
+ if (state.loading) {
+ return Loading...
;
+ }
+
+ if (!state.value) {
+ return null;
+ }
+
+ return (
+
+ {state.value.map((_, index) => {
+ return ;
+ })}
+
+ );
}
-
-export class NewsPanel extends PureComponent {
- private refreshSubscription: Unsubscribable;
-
- constructor(props: Props) {
- super(props);
- this.refreshSubscription = this.props.eventBus.subscribe(RefreshEvent, this.loadChannel.bind(this));
- this.state = {};
- }
-
- componentDidMount(): void {
- this.loadChannel();
- }
-
- componentWillUnmount(): void {
- this.refreshSubscription.unsubscribe();
- }
-
- componentDidUpdate(prevProps: Props): void {
- if (this.props.options.feedUrl !== prevProps.options.feedUrl) {
- this.loadChannel();
- }
- }
-
- async loadChannel() {
- const { options } = this.props;
- try {
- const url = options.feedUrl || DEFAULT_FEED_URL;
-
- const feed = await loadFeed(url);
- const frame = feedToDataFrame(feed);
-
- this.setState({
- news: new DataFrameView(frame),
- isError: false,
- });
- } catch (err) {
- console.error('Error Loading News', err);
-
- this.setState({
- news: undefined,
- isError: true,
- });
- }
- }
-
- render() {
- const { width } = this.props;
- const { showImage } = this.props.options;
- const { isError, news } = this.state;
- const styles = getStyles(config.theme2);
- const useWideLayout = width > 600;
-
- if (isError) {
- return Error loading RSS feed.
;
- }
- if (!news) {
- return Loading...
;
- }
-
- return (
-
- {news.map((item, index) => {
- return (
-
- {showImage && item.ogImage && (
-
-
-
- )}
-
-
-
- {item.title}
-
-
-
-
- );
- })}
-
- );
- }
-}
-
-const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
- container: css`
- height: 100%;
- `,
- item: css`
- display: flex;
- padding: ${theme.spacing(1)};
- position: relative;
- margin-bottom: 4px;
- margin-right: ${theme.spacing(1)};
- border-bottom: 2px solid ${theme.colors.border.weak};
- background: ${theme.colors.background.primary};
- flex-direction: column;
- flex-shrink: 0;
- `,
- itemWide: css`
- flex-direction: row;
- `,
- body: css`
- display: flex;
- flex-direction: column;
- `,
- socialImage: css`
- display: flex;
- align-items: center;
- margin-bottom: ${theme.spacing(1)};
- > img {
- width: 100%;
- border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0;
- }
- `,
- socialImageWide: css`
- margin-right: ${theme.spacing(2)};
- margin-bottom: 0;
- > img {
- width: 250px;
- border-radius: ${theme.shape.borderRadius()};
- }
- `,
- link: css`
- color: ${theme.colors.text.link};
- display: inline-block;
-
- &:hover {
- color: ${theme.colors.text.link};
- text-decoration: underline;
- }
- `,
- title: css`
- font-size: 16px;
- margin-bottom: ${theme.spacing(0.5)};
- `,
- content: css`
- p {
- margin-bottom: 4px;
- color: ${theme.colors.text};
- }
- `,
- date: css`
- margin-bottom: ${theme.spacing(0.5)};
- font-weight: 500;
- border-radius: 0 0 0 3px;
- color: ${theme.colors.text.secondary};
- `,
-}));
diff --git a/public/app/plugins/panel/news/component/News.tsx b/public/app/plugins/panel/news/component/News.tsx
new file mode 100644
index 00000000000..48e7493877c
--- /dev/null
+++ b/public/app/plugins/panel/news/component/News.tsx
@@ -0,0 +1,112 @@
+import { css, cx } from '@emotion/css';
+import React from 'react';
+
+import { DataFrameView, GrafanaTheme2, textUtil, dateTimeFormat } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+
+import { NewsItem } from '../types';
+
+interface NewsItemProps {
+ width: number;
+ showImage?: boolean;
+ index: number;
+ data: DataFrameView;
+}
+
+export function News({ width, showImage, data, index }: NewsItemProps) {
+ const styles = useStyles2(getStyles);
+ const useWideLayout = width > 600;
+ const newsItem = data.get(index);
+
+ return (
+
+ {showImage && newsItem.ogImage && (
+
+
+
+ )}
+
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ container: css`
+ height: 100%;
+ `,
+ item: css`
+ display: flex;
+ padding: ${theme.spacing(1)};
+ position: relative;
+ margin-bottom: 4px;
+ margin-right: ${theme.spacing(1)};
+ border-bottom: 2px solid ${theme.colors.border.weak};
+ background: ${theme.colors.background.primary};
+ flex-direction: column;
+ flex-shrink: 0;
+ `,
+ itemWide: css`
+ flex-direction: row;
+ `,
+ body: css`
+ display: flex;
+ flex-direction: column;
+ `,
+ socialImage: css`
+ display: flex;
+ align-items: center;
+ margin-bottom: ${theme.spacing(1)};
+ > img {
+ width: 100%;
+ border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0;
+ }
+ `,
+ socialImageWide: css`
+ margin-right: ${theme.spacing(2)};
+ margin-bottom: 0;
+ > img {
+ width: 250px;
+ border-radius: ${theme.shape.borderRadius()};
+ }
+ `,
+ link: css`
+ color: ${theme.colors.text.link};
+ display: inline-block;
+
+ &:hover {
+ color: ${theme.colors.text.link};
+ text-decoration: underline;
+ }
+ `,
+ title: css`
+ font-size: 16px;
+ margin-bottom: ${theme.spacing(0.5)};
+ `,
+ content: css`
+ p {
+ margin-bottom: 4px;
+ color: ${theme.colors.text};
+ }
+ `,
+ date: css`
+ margin-bottom: ${theme.spacing(0.5)};
+ font-weight: 500;
+ border-radius: 0 0 0 3px;
+ color: ${theme.colors.text.secondary};
+ `,
+});
diff --git a/public/app/plugins/panel/news/useNewsFeed.tsx b/public/app/plugins/panel/news/useNewsFeed.tsx
new file mode 100644
index 00000000000..258f7473c12
--- /dev/null
+++ b/public/app/plugins/panel/news/useNewsFeed.tsx
@@ -0,0 +1,21 @@
+import { useAsyncFn } from 'react-use';
+
+import { DataFrameView } from '@grafana/data';
+
+import { loadFeed } from './feed';
+import { NewsItem } from './types';
+import { feedToDataFrame } from './utils';
+
+export function useNewsFeed(url: string) {
+ const [state, getNews] = useAsyncFn(
+ async () => {
+ const feed = await loadFeed(url);
+ const frame = feedToDataFrame(feed);
+ return new DataFrameView(frame);
+ },
+ [url],
+ { loading: true }
+ );
+
+ return { state, getNews };
+}