mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
92934af741
commit
205c672417
@ -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(
|
|
||||||
<BrowserRouter>
|
|
||||||
<BottomNavLinks link={mockLink} user={mockUser} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<BrowserRouter>
|
|
||||||
<BottomNavLinks link={mockLink} user={mockUser} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<BrowserRouter>
|
|
||||||
<BottomNavLinks link={mockLink} user={mockUser} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<BrowserRouter>
|
|
||||||
<BottomNavLinks link={mockLink} user={mockUser} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<BrowserRouter>
|
|
||||||
<BottomNavLinks link={mockLink} user={mockUser} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
const currentOrg = screen.getByText(new RegExp(mockUser.orgName, 'i'));
|
|
||||||
const orgSwitcher = screen.getByText('Switch organization');
|
|
||||||
expect(currentOrg).toBeInTheDocument();
|
|
||||||
expect(orgSwitcher).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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<Props, State> {
|
|
||||||
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 (
|
|
||||||
<div className="sidemenu-item dropdown dropup">
|
|
||||||
<Link href={link.url} className="sidemenu-link" target={link.target}>
|
|
||||||
<span className="icon-circle sidemenu-icon">
|
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" title="Help icon" />}
|
|
||||||
{link.img && <img src={link.img} alt="Profile picture" />}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<SideMenuDropDown
|
|
||||||
headerText={link.text}
|
|
||||||
headerUrl={link.url}
|
|
||||||
items={children}
|
|
||||||
reverseDirection
|
|
||||||
subtitleText={link.showOrgSwitcher ? `Current Org.: ${user.orgName}` : link.subTitle}
|
|
||||||
/>
|
|
||||||
{showSwitcherModal && <OrgSwitcher onDismiss={this.toggleSwitcherModal} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,14 @@
|
|||||||
import React from 'react';
|
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';
|
import BottomSection from './BottomSection';
|
||||||
|
|
||||||
|
jest.mock('../../app_events', () => ({
|
||||||
|
publish: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('../../config', () => ({
|
jest.mock('../../config', () => ({
|
||||||
bootData: {
|
bootData: {
|
||||||
navTree: [
|
navTree: [
|
||||||
@ -10,6 +17,7 @@ jest.mock('../../config', () => ({
|
|||||||
hideFromMenu: true,
|
hideFromMenu: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'help',
|
||||||
hideFromMenu: true,
|
hideFromMenu: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -20,25 +28,56 @@ jest.mock('../../config', () => ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
user: {
|
|
||||||
orgCount: 5,
|
|
||||||
orgName: 'Grafana',
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
contextSrv: {
|
contextSrv: {
|
||||||
sidemenu: true,
|
sidemenu: true,
|
||||||
isSignedIn: false,
|
isSignedIn: true,
|
||||||
isGrafanaAdmin: false,
|
isGrafanaAdmin: false,
|
||||||
hasEditPermissionFolders: false,
|
hasEditPermissionFolders: false,
|
||||||
|
user: {
|
||||||
|
orgCount: 5,
|
||||||
|
orgName: 'Grafana',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('BottomSection', () => {
|
||||||
it('should render component', () => {
|
it('should render the correct children', () => {
|
||||||
const wrapper = shallow(<BottomSection />);
|
render(<BottomSection />);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(screen.getByTestId('bottom-section-items').children.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates the correct children for the help link', () => {
|
||||||
|
render(<BottomSection />);
|
||||||
|
|
||||||
|
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(<BottomSection />);
|
||||||
|
|
||||||
|
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(<BottomSection />);
|
||||||
|
|
||||||
|
const currentOrg = screen.getByText(new RegExp('Grafana', 'i'));
|
||||||
|
const orgSwitcher = screen.getByText('Switch organization');
|
||||||
|
expect(currentOrg).toBeInTheDocument();
|
||||||
|
expect(orgSwitcher).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,30 +1,85 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { cloneDeep, find } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { SignIn } from './SignIn';
|
|
||||||
import BottomNavLinks from './BottomNavLinks';
|
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
|
||||||
import config from '../../config';
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
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() {
|
export default function BottomSection() {
|
||||||
const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree);
|
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 isSignedIn = contextSrv.isSignedIn;
|
||||||
const user = contextSrv.user;
|
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) {
|
if (user && user.orgCount > 1) {
|
||||||
const profileNode: any = find(bottomNav, { id: 'profile' });
|
const profileNode = bottomNav.find((bottomNavItem) => bottomNavItem.id === 'profile');
|
||||||
if (profileNode) {
|
if (profileNode) {
|
||||||
profileNode.showOrgSwitcher = true;
|
profileNode.showOrgSwitcher = true;
|
||||||
|
profileNode.subTitle = `Current Org.: ${user?.orgName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidemenu__bottom">
|
<div data-testid="bottom-section-items" className="sidemenu__bottom">
|
||||||
{!isSignedIn && <SignIn />}
|
{!isSignedIn && <SignIn />}
|
||||||
{bottomNav.map((link, index) => {
|
{bottomNav.map((link, index) => {
|
||||||
return <BottomNavLinks link={link} user={user} key={`${link.url}-${index}`} />;
|
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 (
|
||||||
|
<SideMenuItem
|
||||||
|
key={`${link.url}-${index}`}
|
||||||
|
label={link.text}
|
||||||
|
menuItems={menuItems}
|
||||||
|
menuSubTitle={link.subTitle}
|
||||||
|
onClick={link.onClick}
|
||||||
|
reverseMenuDirection
|
||||||
|
target={link.target}
|
||||||
|
url={link.url}
|
||||||
|
>
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
{link.img && <img src={link.img} />}
|
||||||
|
</SideMenuItem>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
55
public/app/core/components/sidemenu/SideMenuItem.test.tsx
Normal file
55
public/app/core/components/sidemenu/SideMenuItem.test.tsx
Normal file
@ -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(
|
||||||
|
<BrowserRouter>
|
||||||
|
<SideMenuItem label={mockLabel}>
|
||||||
|
<div data-testid="mockChild" />
|
||||||
|
</SideMenuItem>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<BrowserRouter>
|
||||||
|
<SideMenuItem label={mockLabel} url={mockUrl}>
|
||||||
|
<div data-testid="mockChild" />
|
||||||
|
</SideMenuItem>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<BrowserRouter>
|
||||||
|
<SideMenuItem label={mockLabel} onClick={mockOnClick}>
|
||||||
|
<div data-testid="mockChild" />
|
||||||
|
</SideMenuItem>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
const child = screen.getByTestId('mockChild');
|
||||||
|
expect(child).toBeInTheDocument();
|
||||||
|
userEvent.click(child);
|
||||||
|
expect(mockOnClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
67
public/app/core/components/sidemenu/SideMenuItem.tsx
Normal file
67
public/app/core/components/sidemenu/SideMenuItem.tsx
Normal file
@ -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 ? (
|
||||||
|
<Link
|
||||||
|
className="sidemenu-link"
|
||||||
|
href={url}
|
||||||
|
target={target}
|
||||||
|
aria-label={label}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-haspopup="true"
|
||||||
|
>
|
||||||
|
<span className="icon-circle sidemenu-icon">{children}</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button className={cx(resetButtonStyles, 'sidemenu-link')} onClick={onClick} aria-label={label}>
|
||||||
|
<span className="icon-circle sidemenu-icon">{children}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('sidemenu-item', 'dropdown', { dropup: reverseMenuDirection })}>
|
||||||
|
{anchor}
|
||||||
|
<SideMenuDropDown
|
||||||
|
headerText={label}
|
||||||
|
headerUrl={url}
|
||||||
|
items={menuItems}
|
||||||
|
onHeaderClick={onClick}
|
||||||
|
reverseDirection={reverseMenuDirection}
|
||||||
|
subtitleText={menuSubTitle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideMenuItem;
|
@ -1,16 +1,14 @@
|
|||||||
import React, { FC } from 'react';
|
import React from 'react';
|
||||||
import { cloneDeep, filter } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import TopSectionItem from './TopSectionItem';
|
|
||||||
import config from '../../config';
|
|
||||||
import { locationService } from '@grafana/runtime';
|
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<any> = () => {
|
const TopSection = () => {
|
||||||
const navTree = cloneDeep(config.bootData.navTree);
|
const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree);
|
||||||
const mainLinks = filter(navTree, (item) => !item.hideFromMenu);
|
const mainLinks = navTree.filter((item) => !item.hideFromMenu);
|
||||||
const searchLink = {
|
|
||||||
text: 'Search dashboards',
|
|
||||||
icon: 'search',
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenSearch = () => {
|
const onOpenSearch = () => {
|
||||||
locationService.partial({ search: 'open' });
|
locationService.partial({ search: 'open' });
|
||||||
@ -18,9 +16,22 @@ const TopSection: FC<any> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="top-section-items" className="sidemenu__top">
|
<div data-testid="top-section-items" className="sidemenu__top">
|
||||||
<TopSectionItem link={searchLink} onClick={onOpenSearch} />
|
<SideMenuItem label="Search dashboards" onClick={onOpenSearch}>
|
||||||
|
<Icon name="search" size="xl" />
|
||||||
|
</SideMenuItem>
|
||||||
{mainLinks.map((link, index) => {
|
{mainLinks.map((link, index) => {
|
||||||
return <TopSectionItem link={link} key={`${link.id}-${index}`} />;
|
return (
|
||||||
|
<SideMenuItem
|
||||||
|
key={`${link.id}-${index}`}
|
||||||
|
label={link.text}
|
||||||
|
menuItems={link.children}
|
||||||
|
target={link.target}
|
||||||
|
url={link.url}
|
||||||
|
>
|
||||||
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
|
{link.img && <img src={link.img} />}
|
||||||
|
</SideMenuItem>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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(
|
|
||||||
<MemoryRouter initialEntries={[{ pathname: '/', key: 'testKey' }]}>
|
|
||||||
<TopSectionItem {...props} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Render', () => {
|
|
||||||
it('should render component', () => {
|
|
||||||
setup();
|
|
||||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('menu')).toHaveTextContent('Hello');
|
|
||||||
});
|
|
||||||
});
|
|
@ -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<Props> = ({ link, onClick }) => {
|
|
||||||
const resetButtonStyles = useStyles2(
|
|
||||||
() =>
|
|
||||||
css`
|
|
||||||
background-color: transparent;
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const linkContent = (
|
|
||||||
<span className="icon-circle sidemenu-icon">
|
|
||||||
{link.icon && <Icon name={link.icon as any} size="xl" />}
|
|
||||||
{link.img && <img src={link.img} />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const anchor = link.url ? (
|
|
||||||
<Link
|
|
||||||
className="sidemenu-link"
|
|
||||||
href={link.url}
|
|
||||||
target={link.target}
|
|
||||||
aria-label={link.text}
|
|
||||||
onClick={onClick}
|
|
||||||
aria-haspopup="true"
|
|
||||||
>
|
|
||||||
{linkContent}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<button className={cx(resetButtonStyles, 'sidemenu-link')} onClick={onClick} aria-label={link.text}>
|
|
||||||
{linkContent}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="sidemenu-item dropdown">
|
|
||||||
{anchor}
|
|
||||||
<SideMenuDropDown items={link.children} headerText={link.text} headerUrl={link.url} onHeaderClick={onClick} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopSectionItem;
|
|
@ -1,34 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
<div
|
|
||||||
className="sidemenu__bottom"
|
|
||||||
>
|
|
||||||
<SignIn />
|
|
||||||
<BottomNavLinks
|
|
||||||
key="undefined-0"
|
|
||||||
link={
|
|
||||||
Object {
|
|
||||||
"hideFromMenu": true,
|
|
||||||
"id": "profile",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<BottomNavLinks
|
|
||||||
key="undefined-1"
|
|
||||||
link={
|
|
||||||
Object {
|
|
||||||
"hideFromMenu": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<BottomNavLinks
|
|
||||||
key="undefined-2"
|
|
||||||
link={
|
|
||||||
Object {
|
|
||||||
"hideFromMenu": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
Loading…
Reference in New Issue
Block a user