From 205c6724173795c96224782078af376cf245bdc3 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 1 Sep 2021 17:16:50 +0100 Subject: [PATCH] Sidemenu: Refactor TopSectionItem and BottomNavLinks into SideMenuItem (#38755) * Sidemenu: Refactor TopSectionItem and BottomNavLinks into SideMenuItem * Update failing snapshot * BottomSection: Convert tests to RTL + add some extra unit tests --- .../sidemenu/BottomNavLinks.test.tsx | 115 ------------------ .../components/sidemenu/BottomNavLinks.tsx | 83 ------------- .../sidemenu/BottomSection.test.tsx | 61 ++++++++-- .../components/sidemenu/BottomSection.tsx | 75 ++++++++++-- .../components/sidemenu/SideMenuItem.test.tsx | 55 +++++++++ .../core/components/sidemenu/SideMenuItem.tsx | 67 ++++++++++ .../core/components/sidemenu/TopSection.tsx | 37 ++++-- .../sidemenu/TopSectionItem.test.tsx | 31 ----- .../components/sidemenu/TopSectionItem.tsx | 51 -------- .../__snapshots__/BottomSection.test.tsx.snap | 34 ------ 10 files changed, 261 insertions(+), 348 deletions(-) delete mode 100644 public/app/core/components/sidemenu/BottomNavLinks.test.tsx delete mode 100644 public/app/core/components/sidemenu/BottomNavLinks.tsx create mode 100644 public/app/core/components/sidemenu/SideMenuItem.test.tsx create mode 100644 public/app/core/components/sidemenu/SideMenuItem.tsx delete mode 100644 public/app/core/components/sidemenu/TopSectionItem.test.tsx delete mode 100644 public/app/core/components/sidemenu/TopSectionItem.tsx delete mode 100644 public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap diff --git a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx b/public/app/core/components/sidemenu/BottomNavLinks.test.tsx deleted file mode 100644 index f7d991feece..00000000000 --- a/public/app/core/components/sidemenu/BottomNavLinks.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -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 appEvents from '../../app_events'; -import { ShowModalReactEvent } from '../../../types/events'; -import { HelpModal } from '../help/HelpModal'; -import BottomNavLinks from './BottomNavLinks'; - -jest.mock('../../app_events', () => ({ - publish: jest.fn(), -})); - -describe('BottomNavLinks', () => { - const mockUser = { - id: 1, - isGrafanaAdmin: false, - isSignedIn: false, - orgCount: 2, - orgRole: '', - orgId: 1, - login: 'hello', - orgName: 'mockOrganization', - timezone: 'UTC', - helpFlags1: 1, - lightTheme: false, - hasEditPermissionInFolders: false, - }; - - it('renders the link text', () => { - const mockLink = { - text: 'Hello', - }; - - render( - - - - ); - const linkText = screen.getByText(mockLink.text); - expect(linkText).toBeInTheDocument(); - }); - - it('attaches the link url to the text if provided', () => { - const mockLink = { - text: 'Hello', - url: '/route', - }; - - render( - - - - ); - const link = screen.getByRole('link', { name: mockLink.text }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', mockLink.url); - }); - - it('creates the correct children for the help link', () => { - const mockLink = { - id: 'help', - text: 'Hello', - }; - - render( - - - - ); - const documentation = screen.getByRole('link', { name: 'Documentation' }); - const support = screen.getByRole('link', { name: 'Support' }); - const community = screen.getByRole('link', { name: 'Community' }); - const keyboardShortcuts = screen.getByText('Keyboard shortcuts'); - - expect(documentation).toBeInTheDocument(); - expect(support).toBeInTheDocument(); - expect(community).toBeInTheDocument(); - expect(keyboardShortcuts).toBeInTheDocument(); - }); - - it('clicking the keyboard shortcuts button shows the modal', () => { - const mockLink = { - id: 'help', - text: 'Hello', - }; - - render( - - - - ); - const keyboardShortcuts = screen.getByText('Keyboard shortcuts'); - expect(keyboardShortcuts).toBeInTheDocument(); - userEvent.click(keyboardShortcuts); - expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: HelpModal })); - }); - - it('shows the current organization and organization switcher if showOrgSwitcher is true', () => { - const mockLink = { - showOrgSwitcher: true, - text: 'Hello', - }; - - render( - - - - ); - const currentOrg = screen.getByText(new RegExp(mockUser.orgName, 'i')); - const orgSwitcher = screen.getByText('Switch organization'); - expect(currentOrg).toBeInTheDocument(); - expect(orgSwitcher).toBeInTheDocument(); - }); -}); diff --git a/public/app/core/components/sidemenu/BottomNavLinks.tsx b/public/app/core/components/sidemenu/BottomNavLinks.tsx deleted file mode 100644 index 9b2b0801ab3..00000000000 --- a/public/app/core/components/sidemenu/BottomNavLinks.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { PureComponent } from 'react'; -import appEvents from '../../app_events'; -import { User } from '../../services/context_srv'; -import { NavModelItem } from '@grafana/data'; -import { Icon, IconName, Link } from '@grafana/ui'; -import { OrgSwitcher } from '../OrgSwitcher'; -import { getFooterLinks } from '../Footer/Footer'; -import { ShowModalReactEvent } from '../../../types/events'; -import { HelpModal } from '../help/HelpModal'; -import SideMenuDropDown from './SideMenuDropDown'; - -export interface Props { - link: NavModelItem; - user: User; -} - -interface State { - showSwitcherModal: boolean; -} - -export default class BottomNavLinks extends PureComponent { - state: State = { - showSwitcherModal: false, - }; - - onOpenShortcuts = () => { - appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); - }; - - toggleSwitcherModal = () => { - this.setState((prevState) => ({ - showSwitcherModal: !prevState.showSwitcherModal, - })); - }; - - render() { - const { link, user } = this.props; - const { showSwitcherModal } = this.state; - - let children = link.children || []; - - if (link.id === 'help') { - children = [ - ...getFooterLinks(), - { - text: 'Keyboard shortcuts', - icon: 'keyboard', - onClick: this.onOpenShortcuts, - }, - ]; - } - - if (link.showOrgSwitcher) { - children = [ - ...children, - { - text: 'Switch organization', - icon: 'arrow-random', - onClick: this.toggleSwitcherModal, - }, - ]; - } - - return ( -
- - - {link.icon && } - {link.img && Profile picture} - - - - {showSwitcherModal && } -
- ); - } -} diff --git a/public/app/core/components/sidemenu/BottomSection.test.tsx b/public/app/core/components/sidemenu/BottomSection.test.tsx index 40307895c57..e0dca14ef2c 100644 --- a/public/app/core/components/sidemenu/BottomSection.test.tsx +++ b/public/app/core/components/sidemenu/BottomSection.test.tsx @@ -1,7 +1,14 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ShowModalReactEvent } from '../../../types/events'; +import { HelpModal } from '../help/HelpModal'; +import appEvents from '../../app_events'; import BottomSection from './BottomSection'; +jest.mock('../../app_events', () => ({ + publish: jest.fn(), +})); jest.mock('../../config', () => ({ bootData: { navTree: [ @@ -10,6 +17,7 @@ jest.mock('../../config', () => ({ hideFromMenu: true, }, { + id: 'help', hideFromMenu: true, }, { @@ -20,25 +28,56 @@ jest.mock('../../config', () => ({ }, ], }, - user: { - orgCount: 5, - orgName: 'Grafana', - }, })); - jest.mock('app/core/services/context_srv', () => ({ contextSrv: { sidemenu: true, - isSignedIn: false, + isSignedIn: true, isGrafanaAdmin: false, hasEditPermissionFolders: false, + user: { + orgCount: 5, + orgName: 'Grafana', + }, }, })); -describe('Render', () => { - it('should render component', () => { - const wrapper = shallow(); +describe('BottomSection', () => { + it('should render the correct children', () => { + render(); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByTestId('bottom-section-items').children.length).toBe(3); + }); + + it('creates the correct children for the help link', () => { + render(); + + const documentation = screen.getByRole('link', { name: 'Documentation' }); + const support = screen.getByRole('link', { name: 'Support' }); + const community = screen.getByRole('link', { name: 'Community' }); + const keyboardShortcuts = screen.getByText('Keyboard shortcuts'); + expect(documentation).toBeInTheDocument(); + expect(support).toBeInTheDocument(); + expect(community).toBeInTheDocument(); + expect(keyboardShortcuts).toBeInTheDocument(); + }); + + it('clicking the keyboard shortcuts button shows the modal', () => { + render(); + + const keyboardShortcuts = screen.getByText('Keyboard shortcuts'); + expect(keyboardShortcuts).toBeInTheDocument(); + + userEvent.click(keyboardShortcuts); + expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: HelpModal })); + }); + + it('shows the current organization and organization switcher if showOrgSwitcher is true', () => { + render(); + + const currentOrg = screen.getByText(new RegExp('Grafana', 'i')); + const orgSwitcher = screen.getByText('Switch organization'); + expect(currentOrg).toBeInTheDocument(); + expect(orgSwitcher).toBeInTheDocument(); }); }); diff --git a/public/app/core/components/sidemenu/BottomSection.tsx b/public/app/core/components/sidemenu/BottomSection.tsx index 7d0014299a8..9c43c5de126 100644 --- a/public/app/core/components/sidemenu/BottomSection.tsx +++ b/public/app/core/components/sidemenu/BottomSection.tsx @@ -1,30 +1,85 @@ -import React from 'react'; -import { cloneDeep, find } from 'lodash'; -import { SignIn } from './SignIn'; -import BottomNavLinks from './BottomNavLinks'; -import { contextSrv } from 'app/core/services/context_srv'; -import config from '../../config'; +import React, { useState } from 'react'; +import { cloneDeep } from 'lodash'; import { NavModelItem } from '@grafana/data'; +import { Icon, IconName } from '@grafana/ui'; +import appEvents from '../../app_events'; +import { SignIn } from './SignIn'; +import SideMenuItem from './SideMenuItem'; +import { ShowModalReactEvent } from '../../../types/events'; +import { contextSrv } from 'app/core/services/context_srv'; +import { OrgSwitcher } from '../OrgSwitcher'; +import { getFooterLinks } from '../Footer/Footer'; +import { HelpModal } from '../help/HelpModal'; +import config from '../../config'; export default function BottomSection() { const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree); - const bottomNav: NavModelItem[] = navTree.filter((item) => item.hideFromMenu); + const bottomNav = navTree.filter((item) => item.hideFromMenu); const isSignedIn = contextSrv.isSignedIn; const user = contextSrv.user; + const [showSwitcherModal, setShowSwitcherModal] = useState(false); + + const toggleSwitcherModal = () => { + setShowSwitcherModal(!showSwitcherModal); + }; + + const onOpenShortcuts = () => { + appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); + }; if (user && user.orgCount > 1) { - const profileNode: any = find(bottomNav, { id: 'profile' }); + const profileNode = bottomNav.find((bottomNavItem) => bottomNavItem.id === 'profile'); if (profileNode) { profileNode.showOrgSwitcher = true; + profileNode.subTitle = `Current Org.: ${user?.orgName}`; } } return ( -
+
{!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`] = ` -
- - - - -
-`;