TopNav: Move news into profile menu (#99535)

* remove news icon from topnav

Signed-off-by: bergquist <carl.bergquist@gmail.com>

* TopNav: Move rss feed and kiosk action into profile menu

* Update language keys

* Update

* review fixes

* Update

* Update

---------

Signed-off-by: bergquist <carl.bergquist@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Carl Bergquist 2025-01-30 14:43:26 +01:00 committed by GitHub
parent 6392542db4
commit a92c8145f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 164 additions and 132 deletions

View File

@ -1003,9 +1003,8 @@ exports[`better eslint`] = {
"public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/News/NewsContainer.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
"public/app/core/components/AppChrome/News/NewsDrawer.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/News/NewsWrapper.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
@ -1016,10 +1015,12 @@ exports[`better eslint`] = {
"public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/TopBar/ProfileButton.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/themes/mixins\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]

View File

@ -1,19 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NewsContainer } from './NewsContainer';
const setup = () => {
const { container } = render(<NewsContainer />);
return { container };
};
describe('News', () => {
it('should render the drawer when the drawer button is clicked', async () => {
setup();
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText('Latest from the blog')).toBeInTheDocument();
});
});

View File

@ -1,84 +0,0 @@
import { css } from '@emotion/css';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { IconButton, Drawer, ToolbarButton, useStyles2, Text } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { DEFAULT_FEED_URL } from 'app/plugins/panel/news/constants';
import { NewsWrapper } from './NewsWrapper';
interface NewsContainerProps {
className?: string;
}
export function NewsContainer({ className }: NewsContainerProps) {
const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false);
const styles = useStyles2(getStyles);
return (
<>
<ToolbarButton className={className} onClick={onToggleShowNewsDrawer} iconOnly icon="rss" aria-label="News" />
{showNewsDrawer && (
<Drawer
title={
<div className={styles.title}>
<Text element="h2">{t('news.title', 'Latest from the blog')}</Text>
<a
href="https://grafana.com/blog/"
target="_blank"
rel="noreferrer"
title="Go to Grafana labs blog"
className={styles.grot}
>
<img src="public/img/grot-news.svg" alt="Grot reading news" />
</a>
<div className={styles.actions}>
<IconButton
name="times"
variant="secondary"
onClick={onToggleShowNewsDrawer}
data-testid={selectors.components.Drawer.General.close}
tooltip={t(`news.drawer.close`, 'Close Drawer')}
/>
</div>
</div>
}
onClose={onToggleShowNewsDrawer}
size="md"
>
<NewsWrapper feedUrl={DEFAULT_FEED_URL} />
</Drawer>
)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
title: css({
display: `flex`,
alignItems: `center`,
justifyContent: `center`,
gap: theme.spacing(2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
grot: css({
display: `flex`,
alignItems: `center`,
justifyContent: `center`,
padding: theme.spacing(2, 0),
img: {
width: `75px`,
height: `75px`,
},
}),
actions: css({
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(2),
}),
};
};

View File

@ -0,0 +1,78 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { IconButton, Drawer, useStyles2, Text } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { DEFAULT_FEED_URL } from 'app/plugins/panel/news/constants';
import { NewsWrapper } from './NewsWrapper';
interface NewsContainerProps {
className?: string;
onClose: () => void;
}
export function NewsContainer({ onClose }: NewsContainerProps) {
const styles = useStyles2(getStyles);
return (
<Drawer
title={
<div className={styles.title}>
<Text element="h2">{t('news.title', 'Latest from the blog')}</Text>
<a
href="https://grafana.com/blog/"
target="_blank"
rel="noreferrer"
title="Go to Grafana labs blog"
className={styles.grot}
>
<img src="public/img/grot-news.svg" alt="Grot reading news" />
</a>
<div className={styles.actions}>
<IconButton
name="times"
variant="secondary"
onClick={onClose}
data-testid={selectors.components.Drawer.General.close}
tooltip={t(`news.drawer.close`, 'Close Drawer')}
/>
</div>
</div>
}
onClose={onClose}
size="md"
>
<NewsWrapper feedUrl={DEFAULT_FEED_URL} />
</Drawer>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
title: css({
display: `flex`,
alignItems: `center`,
justifyContent: `center`,
gap: theme.spacing(2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
grot: css({
display: `flex`,
alignItems: `center`,
justifyContent: `center`,
padding: theme.spacing(2, 0),
img: {
width: `75px`,
height: `75px`,
},
}),
actions: css({
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(2),
}),
};
};

View File

@ -0,0 +1,71 @@
import { css } from '@emotion/css';
import { cloneDeep } from 'lodash';
import { useToggle } from 'react-use';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Dropdown, Menu, MenuItem, ToolbarButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization';
import { enrichWithInteractionTracking } from '../MegaMenu/utils';
import { NewsContainer } from '../News/NewsDrawer';
import { TopNavBarMenu } from './TopNavBarMenu';
export interface Props {
profileNode: NavModelItem;
}
export function ProfileButton({ profileNode }: Props) {
const styles = useStyles2(getStyles);
const node = enrichWithInteractionTracking(cloneDeep(profileNode), false);
const [showNewsDrawer, onToggleShowNewsDrawer] = useToggle(false);
if (!node) {
return null;
}
const renderMenu = () => (
<TopNavBarMenu node={profileNode}>
{config.newsFeedEnabled && (
<>
<Menu.Divider />
<MenuItem
icon="rss"
onClick={onToggleShowNewsDrawer}
label={t('navigation.rss-button', 'Latest from the blog')}
/>
</>
)}
</TopNavBarMenu>
);
return (
<>
<Dropdown overlay={renderMenu} placement="bottom-end">
<ToolbarButton
className={styles.profileButton}
imgSrc={contextSrv.user.gravatarUrl}
imgAlt="User avatar"
aria-label="Profile"
/>
</Dropdown>
{showNewsDrawer && <NewsContainer onClose={onToggleShowNewsDrawer} />}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
profileButton: css({
padding: theme.spacing(0, 0.5),
img: {
borderRadius: theme.shape.radius.circle,
height: '24px',
marginRight: 0,
width: '24px',
},
}),
};
};

View File

@ -16,10 +16,10 @@ import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs';
import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
import { HistoryContainer } from '../History/HistoryContainer';
import { enrichHelpItem } from '../MegaMenu/utils';
import { NewsContainer } from '../News/NewsContainer';
import { QuickAdd } from '../QuickAdd/QuickAdd';
import { TOP_BAR_LEVEL_HEIGHT } from '../types';
import { ProfileButton } from './ProfileButton';
import { SignInLink } from './SignInLink';
import { TopNavBarMenu } from './TopNavBarMenu';
import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
@ -80,7 +80,6 @@ export const SingleTopBar = memo(function SingleTopBar({
<ToolbarButton iconOnly icon="question-circle" aria-label="Help" />
</Dropdown>
)}
{config.newsFeedEnabled && <NewsContainer />}
<ToolbarButton
icon="monitor"
className={styles.kioskToggle}
@ -88,16 +87,7 @@ export const SingleTopBar = memo(function SingleTopBar({
tooltip="Enable kiosk mode"
/>
{!contextSrv.user.isSignedIn && <SignInLink />}
{profileNode && (
<Dropdown overlay={() => <TopNavBarMenu node={profileNode} />} placement="bottom-end">
<ToolbarButton
className={styles.profileButton}
imgSrc={contextSrv.user.gravatarUrl}
imgAlt="User avatar"
aria-label="Profile"
/>
</Dropdown>
)}
{profileNode && <ProfileButton profileNode={profileNode} />}
</Stack>
</div>
);
@ -132,15 +122,6 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({
height: theme.spacing(3),
width: theme.spacing(3),
}),
profileButton: css({
padding: theme.spacing(0, 0.5),
img: {
borderRadius: theme.shape.radius.circle,
height: '24px',
marginRight: 0,
width: '24px',
},
}),
kioskToggle: css({
[theme.breakpoints.down('lg')]: {
display: 'none',

View File

@ -8,9 +8,10 @@ import { enrichWithInteractionTracking } from '../MegaMenu/utils';
export interface TopNavBarMenuProps {
node: NavModelItem;
children?: React.ReactNode;
}
export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) {
export function TopNavBarMenu({ node: nodePlain, children }: TopNavBarMenuProps) {
const styles = useStyles2(getStyles);
const node = enrichWithInteractionTracking(cloneDeep(nodePlain), false);
@ -37,6 +38,7 @@ export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) {
<MenuItem icon={item.icon} onClick={item.onClick} label={item.text} key={item.id} />
);
})}
{children}
</Menu>
);
}

View File

@ -2430,7 +2430,8 @@
"list-label": "Navigation",
"open": "Open menu",
"undock": "Undock menu"
}
},
"rss-button": "Latest from the blog"
},
"news": {
"drawer": {

View File

@ -2430,7 +2430,8 @@
"list-label": "Ńävįģäŧįőʼn",
"open": "Øpęʼn męʼnū",
"undock": "Ůʼnđőčĸ męʼnū"
}
},
"rss-button": "Ŀäŧęşŧ ƒřőm ŧĥę þľőģ"
},
"news": {
"drawer": {