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 ( -
- ); - })} - - ); - } -} - -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 && ( + + {newsItem.title} + + )} +
+ + +

{newsItem.title}

+
+
+
+
+ ); +} + +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 }; +}