Sidemenu: Refactor Sidemenu to use emotion (#38995)

* Chore(Sidemenu): Refactor Sidemenu scss into emotion

* Fix unit test

* Sidemenu: use cx instead of template strings
This commit is contained in:
Ashley Harrison
2021-09-09 09:56:08 +01:00
committed by GitHub
parent 78b3847014
commit e4ca6f2445
12 changed files with 302 additions and 363 deletions

View File

@@ -1,4 +1,3 @@
<!-- 8.1.3 START -->
# 8.1.3 (2021-09-08)

View File

@@ -24,4 +24,3 @@ list = false
- **Plugins:** Track signed files + add warn log for plugin assets which are not signed. [#38938](https://github.com/grafana/grafana/pull/38938), [@wbrowne](https://github.com/wbrowne)
- **Postgres/MySQL/MSSQL:** Fix region annotations not displayed correctly. [#38936](https://github.com/grafana/grafana/pull/38936), [@marefr](https://github.com/marefr)
- **Prometheus:** Fix validate selector in metrics browser. [#38921](https://github.com/grafana/grafana/pull/38921), [@ivanahuckova](https://github.com/ivanahuckova)

View File

@@ -60,7 +60,9 @@ describe('BottomSection', () => {
it('creates the correct children for the help link', () => {
render(
<BrowserRouter>
<BottomSection />
<div className="sidemenu-open--xs">
<BottomSection />
</div>
</BrowserRouter>
);

View File

@@ -1,19 +1,22 @@
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 { useLocation } from 'react-router-dom';
import SideMenuItem from './SideMenuItem';
import { ShowModalReactEvent } from '../../../types/events';
import { cloneDeep } from 'lodash';
import { css } from '@emotion/css';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Icon, IconName, styleMixins, useTheme2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import appEvents from '../../app_events';
import { ShowModalReactEvent } from '../../../types/events';
import config from '../../config';
import { OrgSwitcher } from '../OrgSwitcher';
import { getFooterLinks } from '../Footer/Footer';
import { HelpModal } from '../help/HelpModal';
import config from '../../config';
import SideMenuItem from './SideMenuItem';
import { getForcedLoginUrl } from './utils';
export default function BottomSection() {
const theme = useTheme2();
const styles = getStyles(theme);
const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree);
const bottomNav = navTree.filter((item) => item.hideFromMenu);
const isSignedIn = contextSrv.isSignedIn;
@@ -39,7 +42,7 @@ export default function BottomSection() {
}
return (
<div data-testid="bottom-section-items" className="sidemenu__bottom">
<div data-testid="bottom-section-items" className={styles.container}>
{!isSignedIn && (
<SideMenuItem label="Sign In" target="_self" url={forcedLoginUrl}>
<Icon name="signout" size="xl" />
@@ -90,3 +93,18 @@ export default function BottomSection() {
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
display: none;
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
display: block;
margin-bottom: ${theme.spacing(2)};
}
.sidemenu-open--xs & {
display: block;
}
`,
});

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
export interface Props {
@@ -13,35 +14,28 @@ export interface Props {
const DropDownChild = ({ isDivider = false, icon, onClick, target, text, url }: Props) => {
const theme = useTheme2();
const iconClassName = css`
margin-right: ${theme.spacing(1)};
`;
const resetButtonStyles = css`
background-color: transparent;
border: none;
width: 100%;
`;
const styles = getStyles(theme);
const linkContent = (
<>
{icon && <Icon data-testid="dropdown-child-icon" name={icon} className={iconClassName} />}
{icon && <Icon data-testid="dropdown-child-icon" name={icon} className={styles.icon} />}
{text}
</>
);
let element = (
<button className={resetButtonStyles} onClick={onClick}>
<button className={styles.element} onClick={onClick}>
{linkContent}
</button>
);
if (url) {
element =
!target && url.startsWith('/') ? (
<Link onClick={onClick} href={url}>
<Link className={styles.element} onClick={onClick} href={url}>
{linkContent}
</Link>
) : (
<a href={url} target={target} rel="noopener" onClick={onClick}>
<a className={styles.element} href={url} target={target} rel="noopener" onClick={onClick}>
{linkContent}
</a>
);
@@ -51,3 +45,15 @@ const DropDownChild = ({ isDivider = false, icon, onClick, target, text, url }:
};
export default DropDownChild;
const getStyles = (theme: GrafanaTheme2) => ({
element: css`
background-color: transparent;
border: none;
display: flex;
width: 100%;
`,
icon: css`
margin-right: ${theme.spacing(1)};
`,
});

View File

@@ -1,16 +1,20 @@
import React, { FC, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, styleMixins, useTheme2 } from '@grafana/ui';
import appEvents from '../../app_events';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
import { Branding } from 'app/core/components/Branding/Branding';
import config from 'app/core/config';
import { CoreEvents, KioskMode } from 'app/types';
import { Branding } from 'app/core/components/Branding/Branding';
import { Icon } from '@grafana/ui';
import { useLocation } from 'react-router-dom';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
const homeUrl = config.appSubUrl || '/';
export const SideMenu: FC = React.memo(() => {
const theme = useTheme2();
const styles = getStyles(theme);
const location = useLocation();
const query = new URLSearchParams(location.search);
const kiosk = query.get('kiosk') as KioskMode;
@@ -24,21 +28,87 @@ export const SideMenu: FC = React.memo(() => {
}
return (
<nav className="sidemenu" data-testid="sidemenu" aria-label="Main menu">
<a href={homeUrl} className="sidemenu__logo" key="logo">
<nav className={cx(styles.sidemenu, 'sidemenu')} data-testid="sidemenu" aria-label="Main menu">
<a href={homeUrl} className={styles.homeLogo}>
<Branding.MenuLogo />
</a>
<div className="sidemenu__logo_small_breakpoint" onClick={toggleSideMenuSmallBreakpoint} key="hamburger">
<div className={styles.mobileSidemenuLogo} onClick={toggleSideMenuSmallBreakpoint} key="hamburger">
<Icon name="bars" size="xl" />
<span className="sidemenu__close">
<span className={styles.closeButton}>
<Icon name="times" />
&nbsp;Close
Close
</span>
</div>
<TopSection key="topsection" />
<BottomSection key="bottomsection" />
<TopSection />
<BottomSection />
</nav>
);
});
SideMenu.displayName = 'SideMenu';
const getStyles = (theme: GrafanaTheme2) => ({
sidemenu: css`
border-right: 1px solid ${theme.components.panel.borderColor};
display: flex;
flex-direction: column;
position: fixed;
width: ${theme.components.sidemenu.width}px;
z-index: ${theme.zIndex.sidemenu};
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
background-color: ${theme.colors.background.primary};
position: relative;
}
.sidemenu-hidden & {
display: none;
}
.sidemenu-open--xs & {
background-color: ${theme.colors.background.primary};
box-shadow: ${theme.shadows.z1};
height: auto;
position: absolute;
width: 100%;
}
`,
homeLogo: css`
display: none;
min-height: ${theme.components.sidemenu.width}px;
&:hover {
background-color: ${theme.colors.action.hover};
}
img {
width: ${theme.spacing(3.5)};
}
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
align-items: center;
display: flex;
justify-content: center;
}
`,
closeButton: css`
display: none;
.sidemenu-open--xs & {
display: block;
font-size: ${theme.typography.fontSize}px;
}
`,
mobileSidemenuLogo: css`
align-items: center;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ${theme.spacing(2)};
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
display: none;
}
`,
});

View File

@@ -1,8 +1,8 @@
import React from 'react';
import DropDownChild from './DropDownChild';
import { NavModelItem } from '@grafana/data';
import { IconName, Link } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { IconName, Link, useTheme2 } from '@grafana/ui';
import DropDownChild from './DropDownChild';
interface Props {
headerTarget?: HTMLAnchorElement['target'];
@@ -23,32 +23,30 @@ const SideMenuDropDown = ({
reverseDirection = false,
subtitleText,
}: Props) => {
const headerContent = <span className="sidemenu-item-text">{headerText}</span>;
const theme = useTheme2();
const styles = getStyles(theme, reverseDirection);
let header = (
<button onClick={onHeaderClick} className="side-menu-header-link">
{headerContent}
<button onClick={onHeaderClick} className={styles.header}>
{headerText}
</button>
);
if (headerUrl) {
header =
!headerTarget && headerUrl.startsWith('/') ? (
<Link href={headerUrl} onClick={onHeaderClick} className="side-menu-header-link">
{headerContent}
<Link href={headerUrl} onClick={onHeaderClick} className={styles.header}>
{headerText}
</Link>
) : (
<a href={headerUrl} target={headerTarget} onClick={onHeaderClick} className="side-menu-header-link">
{headerContent}
<a href={headerUrl} target={headerTarget} onClick={onHeaderClick} className={styles.header}>
{headerText}
</a>
);
}
const menuClass = css`
flex-direction: ${reverseDirection ? 'column-reverse' : 'column'};
`;
return (
<ul className={`${menuClass} dropdown-menu dropdown-menu--sidemenu`} role="menu">
<li className="side-menu-header">{header}</li>
<ul className={`${styles.menu} dropdown-menu dropdown-menu--sidemenu`} role="menu">
<li>{header}</li>
{items
.filter((item) => !item.hideFromMenu)
.map((child, index) => (
@@ -62,13 +60,59 @@ const SideMenuDropDown = ({
url={child.url}
/>
))}
{subtitleText && (
<li className="sidemenu-subtitle">
<span className="sidemenu-item-text">{subtitleText}</span>
</li>
)}
{subtitleText && <li className={styles.subtitle}>{subtitleText}</li>}
</ul>
);
};
export default SideMenuDropDown;
const getStyles = (theme: GrafanaTheme2, reverseDirection: Props['reverseDirection']) => ({
header: css`
background-color: ${theme.colors.background.secondary};
border: none;
color: ${theme.colors.text.primary};
font-size: ${theme.typography.h4.fontSize};
font-weight: ${theme.typography.h4.fontWeight};
padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(2)} !important;
white-space: nowrap;
width: 100%;
&:hover {
background-color: ${theme.colors.action.hover};
}
.sidemenu-open--xs & {
display: flex;
font-size: ${theme.typography.body.fontSize};
font-weight: ${theme.typography.body.fontWeight};
padding-left: ${theme.spacing(1)} !important;
}
`,
menu: css`
flex-direction: ${reverseDirection ? 'column-reverse' : 'column'};
.sidemenu-open--xs & {
display: flex;
flex-direction: column;
float: none;
margin-bottom: ${theme.spacing(1)};
position: unset;
width: 100%;
}
`,
subtitle: css`
border-bottom: 1px solid ${theme.colors.border.weak};
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.bodySmall.fontWeight};
margin-bottom: ${theme.spacing(1)};
padding: ${theme.spacing(1)} ${theme.spacing(2)} ${theme.spacing(1)};
white-space: nowrap;
.sidemenu-open--xs & {
border-bottom: none;
margin-bottom: 0;
}
`,
});

View File

@@ -1,8 +1,8 @@
import React, { ReactNode } from 'react';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Link, styleMixins, useTheme2 } from '@grafana/ui';
import SideMenuDropDown from './SideMenuDropDown';
import { Link } from '@grafana/ui';
import { NavModelItem } from '@grafana/data';
import { cx } from '@emotion/css';
export interface Props {
children: ReactNode;
@@ -25,9 +25,11 @@ const SideMenuItem = ({
target,
url,
}: Props) => {
const theme = useTheme2();
const styles = getStyles(theme);
let element = (
<button className="sidemenu-link" onClick={onClick} aria-label={label}>
<span className="icon-circle sidemenu-icon">{children}</span>
<button className={styles.element} onClick={onClick} aria-label={label}>
<span className={styles.icon}>{children}</span>
</button>
);
@@ -35,24 +37,24 @@ const SideMenuItem = ({
element =
!target && url.startsWith('/') ? (
<Link
className="sidemenu-link"
className={styles.element}
href={url}
target={target}
aria-label={label}
onClick={onClick}
aria-haspopup="true"
>
<span className="icon-circle sidemenu-icon">{children}</span>
<span className={styles.icon}>{children}</span>
</Link>
) : (
<a href={url} target={target} className="sidemenu-link" onClick={onClick} aria-label={label}>
<span className="icon-circle sidemenu-icon">{children}</span>
<a href={url} target={target} className={styles.element} onClick={onClick} aria-label={label}>
<span className={styles.icon}>{children}</span>
</a>
);
}
return (
<div className={cx('sidemenu-item', 'dropdown', { dropup: reverseMenuDirection })}>
<div className={cx(styles.container, 'dropdown', { dropup: reverseMenuDirection })}>
{element}
<SideMenuDropDown
headerTarget={target}
@@ -68,3 +70,76 @@ const SideMenuItem = ({
};
export default SideMenuItem;
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
position: relative;
@keyframes dropdown-anim {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
// needs to be in here to work on safari...
&:not(:hover) {
border-left: 2px solid transparent;
}
&:hover {
background-color: ${theme.colors.action.hover};
border-image: ${theme.colors.gradients.brandVertical};
border-image-slice: 1;
border-style: solid;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left-width: 2px;
.dropdown-menu {
animation: dropdown-anim 150ms ease-in-out 100ms forwards;
border: none;
display: flex;
// important to overlap it otherwise it can be hidden
// again by the mouse getting outside the hover space
left: ${theme.components.sidemenu.width - 3}px;
margin: 0;
opacity: 0;
top: 0;
z-index: ${theme.zIndex.sidemenu};
}
&.dropup .dropdown-menu {
top: auto;
}
}
}
`,
element: css`
background-color: transparent;
border: 1px solid transparent;
color: ${theme.colors.text.secondary};
display: block;
line-height: 42px;
text-align: center;
width: ${theme.components.sidemenu.width - 2}px;
.sidemenu-open--xs & {
display: none;
}
`,
icon: css`
height: 100%;
width: 100%;
img {
border-radius: 50%;
height: 28px;
width: 28px;
}
`,
});

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { cloneDeep } from 'lodash';
import { css } from '@emotion/css';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { Icon, IconName } from '@grafana/ui';
import SideMenuItem from './SideMenuItem';
import { Icon, IconName, styleMixins, useTheme2 } from '@grafana/ui';
import config from '../../config';
import { NavModelItem } from '@grafana/data';
import SideMenuItem from './SideMenuItem';
const TopSection = () => {
const theme = useTheme2();
const styles = getStyles(theme);
const navTree: NavModelItem[] = cloneDeep(config.bootData.navTree);
const mainLinks = navTree.filter((item) => !item.hideFromMenu);
@@ -15,7 +18,7 @@ const TopSection = () => {
};
return (
<div data-testid="top-section-items" className="sidemenu__top">
<div data-testid="top-section-items" className={styles.container}>
<SideMenuItem label="Search dashboards" onClick={onOpenSearch}>
<Icon name="search" size="xl" />
</SideMenuItem>
@@ -38,3 +41,19 @@ const TopSection = () => {
};
export default TopSection;
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
display: none;
flex-grow: 1;
@media ${styleMixins.mediaUp(`${theme.breakpoints.values.md}px`)} {
display: block;
margin-top: ${theme.spacing(5)};
}
.sidemenu-open--xs & {
display: block;
}
`,
});

View File

@@ -54,7 +54,6 @@
@import 'components/tables_lists';
@import 'components/search';
@import 'components/gf-form';
@import 'components/sidemenu';
@import 'components/navbar';
@import 'components/filter-controls';
@import 'components/filter-list';

View File

@@ -124,15 +124,6 @@
}
}
}
&--sidemenu {
li.sidemenu-org-switcher {
> a,
> button {
padding: 8px 10px 8px 15px;
}
}
}
}
.dropdown-item-text {

View File

@@ -1,283 +0,0 @@
$mobile-menu-breakpoint: md;
.sidemenu {
position: fixed;
display: flex;
flex-flow: column;
flex-direction: column;
width: $side-menu-width;
z-index: $zindex-sidemenu;
border-right: $panel-border;
a:focus {
text-decoration: none;
}
.sidemenu__logo_small_breakpoint {
display: none;
}
.sidemenu__close {
display: none;
}
@include media-breakpoint-up($mobile-menu-breakpoint) {
background: $side-menu-bg;
height: auto;
position: relative;
z-index: $zindex-sidemenu;
}
}
// body class that hides sidemenu
.sidemenu-hidden {
.sidemenu {
display: none;
}
}
.sidemenu__top {
padding-top: 40px;
flex-grow: 1;
}
.sidemenu__bottom {
padding-bottom: $spacer;
}
.sidemenu__top,
.sidemenu__bottom {
display: none;
@include media-breakpoint-up($mobile-menu-breakpoint) {
display: block;
}
}
.sidemenu-item {
position: relative;
@include left-brand-border();
@include media-breakpoint-up($mobile-menu-breakpoint) {
&.active,
&:hover {
background-color: $side-menu-item-hover-bg;
@include left-brand-border-gradient();
.dropdown-menu {
border: none;
margin: 0;
display: flex;
opacity: 0;
top: 0px;
// important to overlap it otherwise it can be hidden
// again by the mouse getting outside the hover space
left: $side-menu-width - 3px;
@include animation('dropdown-anim 150ms ease-in-out 100ms forwards');
z-index: $zindex-sidemenu;
}
}
}
}
.dropup.sidemenu-item:hover .dropdown-menu {
top: auto !important;
}
.sidemenu-link {
background-color: transparent;
color: $side-menu-icon-color !important;
line-height: 42px;
padding: 0px 10px 0px 10px;
display: block;
position: relative;
font-size: 16px;
border: 1px solid transparent;
text-align: center;
img {
border-radius: 50%;
width: 28px;
height: 28px;
}
}
@include keyframes(dropdown-anim) {
0% {
opacity: 0;
//transform: translate3d(-5%,0,0);
}
100% {
opacity: 1;
//transform: translate3d(0,0,0);
}
}
.icon-circle {
width: 35px;
height: 35px;
display: inline-block;
position: relative;
img {
position: relative;
}
}
.side-menu-header {
padding: 10px 10px 10px 20px;
white-space: nowrap;
background-color: $side-menu-item-hover-bg;
font-size: 17px;
color: $side-menu-header-color;
}
.side-menu-header-link {
background-color: transparent;
border: none !important;
color: $side-menu-header-color !important;
font-size: inherit;
padding: 0 !important;
}
.dropdown-menu--sidemenu > li > .side-menu-header-link:hover {
color: #fff !important;
background-color: $side-menu-item-hover-bg !important;
}
.sidemenu-subtitle {
padding: $space-sm $space-md $space-sm;
font-size: $font-size-sm;
color: $text-color-weak;
border-bottom: 1px solid $dropdownDividerBottom;
margin-bottom: $space-xs;
white-space: nowrap;
}
li.sidemenu-org-switcher {
border-bottom: 1px solid $dropdownDividerBottom;
}
.sidemenu-org-switcher__org-name {
font-size: $font-size-base;
}
.sidemenu-org-switcher__org-current {
font-size: $font-size-xs;
color: $text-color-weak;
position: relative;
top: -2px;
}
.sidemenu-org-switcher__switch {
font-size: $font-size-sm;
padding-left: $space-lg;
display: flex;
align-items: center;
> i.fa.fa-random {
margin-right: $space-xs;
top: 1px;
}
}
.sidemenu__logo {
display: block;
padding: 6px 14px 6px 9px;
min-height: $navbarHeight;
position: relative;
height: $navbarHeight - 1px;
&:hover {
background: lightOrDark($gray33, $gray25);
}
img {
width: 26px;
position: relative;
top: 5px;
left: 8px;
}
}
@include media-breakpoint-down(sm) {
.sidemenu-open--xs {
li {
font-size: $font-size-md;
}
.sidemenu {
width: 100%;
background: $side-menu-bg-mobile;
height: auto;
box-shadow: $side-menu-shadow;
position: absolute;
z-index: $zindex-sidemenu;
}
.sidemenu__close {
display: block;
font-size: $font-size-md;
position: relative;
top: -3px;
}
.sidemenu__top,
.sidemenu__bottom {
display: block;
}
.sidemenu-item {
border-right: 2px solid transparent;
}
}
.sidemenu {
.sidemenu__logo {
display: none;
}
.sidemenu__logo_small_breakpoint {
padding: 16px 13px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
cursor: pointer;
.fa-bars {
font-size: 25px;
}
}
.sidemenu__top {
padding-top: 0;
}
.side-menu-header {
padding-left: 10px;
}
.sidemenu-link {
display: none;
}
.dropdown-menu--sidemenu {
display: flex;
position: unset;
width: 100%;
float: none;
margin-bottom: $space-sm;
> li > a,
> li > button {
padding-left: 15px;
}
}
.sidemenu__bottom {
.dropdown-menu--sidemenu {
display: flex;
flex-direction: column;
}
}
}
}