+
{!isSignedIn &&
}
{bottomNav.map((link, index) => {
- return
;
+ let menuItems = link.children || [];
+
+ if (link.id === 'help') {
+ menuItems = [
+ ...getFooterLinks(),
+ {
+ text: 'Keyboard shortcuts',
+ icon: 'keyboard',
+ onClick: onOpenShortcuts,
+ },
+ ];
+ }
+
+ if (link.showOrgSwitcher) {
+ menuItems = [
+ ...menuItems,
+ {
+ text: 'Switch organization',
+ icon: 'arrow-random',
+ onClick: toggleSwitcherModal,
+ },
+ ];
+ }
+
+ return (
+
+ {link.icon && }
+ {link.img &&
}
+
+ );
})}
+ {showSwitcherModal &&
}
);
}
diff --git a/public/app/core/components/sidemenu/SideMenuItem.test.tsx b/public/app/core/components/sidemenu/SideMenuItem.test.tsx
new file mode 100644
index 00000000000..e93f83b29b5
--- /dev/null
+++ b/public/app/core/components/sidemenu/SideMenuItem.test.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { BrowserRouter } from 'react-router-dom';
+import SideMenuItem from './SideMenuItem';
+
+describe('SideMenuItem', () => {
+ it('renders the children', () => {
+ const mockLabel = 'Hello';
+ render(
+
+
+
+
+
+ );
+
+ const child = screen.getByTestId('mockChild');
+ expect(child).toBeInTheDocument();
+ });
+
+ it('wraps the children in a link to the url if provided', () => {
+ const mockLabel = 'Hello';
+ const mockUrl = '/route';
+ render(
+
+
+
+
+
+ );
+
+ const child = screen.getByTestId('mockChild');
+ expect(child).toBeInTheDocument();
+ userEvent.click(child);
+ expect(window.location.pathname).toEqual(mockUrl);
+ });
+
+ it('wraps the children in an onClick if provided', () => {
+ const mockLabel = 'Hello';
+ const mockOnClick = jest.fn();
+ render(
+
+
+
+
+
+ );
+
+ const child = screen.getByTestId('mockChild');
+ expect(child).toBeInTheDocument();
+ userEvent.click(child);
+ expect(mockOnClick).toHaveBeenCalled();
+ });
+});
diff --git a/public/app/core/components/sidemenu/SideMenuItem.tsx b/public/app/core/components/sidemenu/SideMenuItem.tsx
new file mode 100644
index 00000000000..e4d815d1de9
--- /dev/null
+++ b/public/app/core/components/sidemenu/SideMenuItem.tsx
@@ -0,0 +1,67 @@
+import React, { ReactNode } from 'react';
+import SideMenuDropDown from './SideMenuDropDown';
+import { Link, useStyles2 } from '@grafana/ui';
+import { NavModelItem } from '@grafana/data';
+import { css, cx } from '@emotion/css';
+
+export interface Props {
+ children: ReactNode;
+ label: string;
+ menuItems?: NavModelItem[];
+ menuSubTitle?: string;
+ onClick?: () => void;
+ reverseMenuDirection?: boolean;
+ target?: HTMLAnchorElement['target'];
+ url?: string;
+}
+
+const SideMenuItem = ({
+ children,
+ label,
+ menuItems = [],
+ menuSubTitle,
+ onClick,
+ reverseMenuDirection = false,
+ target,
+ url,
+}: Props) => {
+ const resetButtonStyles = useStyles2(
+ () =>
+ css`
+ background-color: transparent;
+ `
+ );
+
+ const anchor = url ? (
+
+
{children}
+
+ ) : (
+
+ );
+
+ return (
+
+ {anchor}
+
+
+ );
+};
+
+export default SideMenuItem;
diff --git a/public/app/core/components/sidemenu/TopSection.tsx b/public/app/core/components/sidemenu/TopSection.tsx
index 8ee7669ecb2..549ec941087 100644
--- a/public/app/core/components/sidemenu/TopSection.tsx
+++ b/public/app/core/components/sidemenu/TopSection.tsx
@@ -1,16 +1,14 @@
-import React, { FC } from 'react';
-import { cloneDeep, filter } from 'lodash';
-import TopSectionItem from './TopSectionItem';
-import config from '../../config';
+import React from 'react';
+import { cloneDeep } from 'lodash';
import { locationService } from '@grafana/runtime';
+import { Icon, IconName } from '@grafana/ui';
+import SideMenuItem from './SideMenuItem';
+import config from '../../config';
+import { NavModelItem } from '@grafana/data';
-const TopSection: FC
= () => {
- const navTree = cloneDeep(config.bootData.navTree);
- const mainLinks = filter(navTree, (item) => !item.hideFromMenu);
- const searchLink = {
- text: 'Search dashboards',
- icon: 'search',
- };
+const TopSection = () => {
+ const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree);
+ const mainLinks = navTree.filter((item) => !item.hideFromMenu);
const onOpenSearch = () => {
locationService.partial({ search: 'open' });
@@ -18,9 +16,22 @@ const TopSection: FC = () => {
return (
-
+
+
+
{mainLinks.map((link, index) => {
- return
;
+ return (
+
+ {link.icon && }
+ {link.img &&
}
+
+ );
})}
);
diff --git a/public/app/core/components/sidemenu/TopSectionItem.test.tsx b/public/app/core/components/sidemenu/TopSectionItem.test.tsx
deleted file mode 100644
index 5fd71521ad7..00000000000
--- a/public/app/core/components/sidemenu/TopSectionItem.test.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import TopSectionItem from './TopSectionItem';
-import { MemoryRouter } from 'react-router-dom';
-
-const setup = (propOverrides?: object) => {
- const props = Object.assign(
- {
- link: {
- text: 'Hello',
- icon: 'cloud',
- url: '/asd',
- },
- },
- propOverrides
- );
-
- return render(
-
-
-
- );
-};
-
-describe('Render', () => {
- it('should render component', () => {
- setup();
- expect(screen.getByText('Hello')).toBeInTheDocument();
- expect(screen.getByRole('menu')).toHaveTextContent('Hello');
- });
-});
diff --git a/public/app/core/components/sidemenu/TopSectionItem.tsx b/public/app/core/components/sidemenu/TopSectionItem.tsx
deleted file mode 100644
index 19c8f63715d..00000000000
--- a/public/app/core/components/sidemenu/TopSectionItem.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { FC } from 'react';
-import SideMenuDropDown from './SideMenuDropDown';
-import { Icon, Link, useStyles2 } from '@grafana/ui';
-import { NavModelItem } from '@grafana/data';
-import { css, cx } from '@emotion/css';
-
-export interface Props {
- link: NavModelItem;
- onClick?: () => void;
-}
-
-const TopSectionItem: FC = ({ link, onClick }) => {
- const resetButtonStyles = useStyles2(
- () =>
- css`
- background-color: transparent;
- `
- );
-
- const linkContent = (
-
- {link.icon && }
- {link.img &&
}
-
- );
-
- const anchor = link.url ? (
-
- {linkContent}
-
- ) : (
-
- );
- return (
-
- {anchor}
-
-
- );
-};
-
-export default TopSectionItem;
diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap
deleted file mode 100644
index ea92e8b7343..00000000000
--- a/public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-
-
-
-
-
-
-`;