mirror of
https://github.com/apache/superset.git
synced 2026-04-27 20:14:54 +00:00
refactor(navbar): migrate Bootstrap navbar to AntD menus (#14184)
* initial commit * more polish * fix types and remove tests * fix tests, update menu css, add oetc * fix lint and precommit * fix test * update css, address comments * fix lint * update submenu for extra buttons * remove block and lint * fix lint * remove block * adjust margin * test round 2 * test round 3 * about section * src/components/Menu/Menu.test.tsx * remove redundant test * fmore pointed test * fix lint * remove unused css * fix dashboard nav view * update comments * use suggestion * lint-fix * move css, fix dropdown and text * lint * rearchitect main nav component * run lint fix * nit
This commit is contained in:
committed by
GitHub
parent
e7a4734742
commit
e16c4d856e
@@ -16,21 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Nav, Navbar, NavItem } from 'react-bootstrap';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
|
||||
import { Menu as DropdownMenu } from 'src/common/components';
|
||||
import NavDropdown from 'src/components/NavDropdown';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
|
||||
import MenuObject, {
|
||||
MenuObjectProps,
|
||||
MenuObjectChildProps,
|
||||
} from './MenuObject';
|
||||
import LanguagePicker, { Languages } from './LanguagePicker';
|
||||
import NewMenu from './NewMenu';
|
||||
import { MainNav as DropdownMenu, MenuMode } from 'src/common/components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Row, Col } from 'antd';
|
||||
import Icon from 'src/components/Icon';
|
||||
import RightMenu from './MenuRight';
|
||||
import { Languages } from './LanguagePicker';
|
||||
|
||||
interface BrandProps {
|
||||
path: string;
|
||||
@@ -39,7 +34,7 @@ interface BrandProps {
|
||||
width: string | number;
|
||||
}
|
||||
|
||||
interface NavBarProps {
|
||||
export interface NavBarProps {
|
||||
bug_report_url?: string;
|
||||
version_string?: string;
|
||||
version_sha?: string;
|
||||
@@ -64,7 +59,24 @@ export interface MenuProps {
|
||||
isFrontendRoute?: (path?: string) => boolean;
|
||||
}
|
||||
|
||||
interface MenuObjectChildProps {
|
||||
label: string;
|
||||
name?: string;
|
||||
icon: string;
|
||||
index: number;
|
||||
url?: string;
|
||||
isFrontendRoute?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuObjectProps extends MenuObjectChildProps {
|
||||
childs?: (MenuObjectChildProps | string)[];
|
||||
isHeader?: boolean;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.header`
|
||||
background-color: white;
|
||||
margin-bottom: 2px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.colors.grayscale.light4}px;
|
||||
&:nth-last-of-type(2) nav {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
@@ -72,59 +84,39 @@ const StyledHeader = styled.header`
|
||||
.caret {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-inverse {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
padding: ${({ theme }) => theme.gridUnit * 1.5}px
|
||||
${({ theme }) => theme.gridUnit * 4}px
|
||||
${({ theme }) => theme.gridUnit * 1.5}px
|
||||
${({ theme }) => theme.gridUnit * 7}px;
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.xs}px;
|
||||
|
||||
div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav > li > a {
|
||||
@media (max-width: 767px) {
|
||||
.navbar-brand {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
.ant-menu-horizontal .ant-menu-item {
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
}
|
||||
/*.ant-menu > .ant-menu-item > a {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
.dropdown-header {
|
||||
text-transform: uppercase;
|
||||
padding-left: 12px;
|
||||
}*/
|
||||
@media (max-width: 767px) {
|
||||
.ant-menu > .ant-menu-item > a {
|
||||
padding: 0px;
|
||||
}
|
||||
.main-nav .ant-menu-submenu-title > svg:nth-child(1) {
|
||||
display: none;
|
||||
}
|
||||
.ant-menu-item-active > a {
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.primary.base} !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-inverse .navbar-nav li a {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
border-bottom: none;
|
||||
transition: background-color ${({ theme }) => theme.transitionTiming}s;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
transition: all ${({ theme }) => theme.transitionTiming}s;
|
||||
background-color: ${({ theme }) => theme.colors.primary.base};
|
||||
}
|
||||
&:focus {
|
||||
border-bottom: none;
|
||||
background-color: transparent;
|
||||
/* background-color: ${({ theme }) => theme.colors.primary.light5}; */
|
||||
}
|
||||
.ant-menu-item a {
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
background-color: ${({ theme }) => theme.colors.primary.light5};
|
||||
@@ -136,169 +128,116 @@ const StyledHeader = styled.header`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
.ant-menu-item-group-title {
|
||||
padding-bottom: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
.ant-menu-item {
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
.about-section {
|
||||
margin: ${({ theme }) => theme.gridUnit}px 0
|
||||
${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { SubMenu } = DropdownMenu;
|
||||
|
||||
export function Menu({
|
||||
data: { menu, brand, navbar_right: navbarRight, settings },
|
||||
isFrontendRoute = () => false,
|
||||
}: MenuProps) {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [showMenu, setMenu] = useState<MenuMode>('horizontal');
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
if (window.innerWidth <= 767) {
|
||||
setMenu('inline');
|
||||
} else setMenu('horizontal');
|
||||
}
|
||||
handleResize();
|
||||
const windowResize = debounce(() => handleResize(), 10);
|
||||
window.addEventListener('resize', windowResize);
|
||||
return () => window.removeEventListener('resize', windowResize);
|
||||
}, []);
|
||||
|
||||
// would useQueryParam here but not all apps provide a router context
|
||||
const standalone = getUrlParam('standalone', 'boolean');
|
||||
if (standalone) return <></>;
|
||||
|
||||
const renderSubMenu = ({
|
||||
label,
|
||||
childs,
|
||||
url,
|
||||
index,
|
||||
isFrontendRoute,
|
||||
}: MenuObjectProps) => {
|
||||
if (url && isFrontendRoute) {
|
||||
return (
|
||||
<DropdownMenu.Item key={label} role="presentation">
|
||||
<Link role="button" to={url}>
|
||||
{label}
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
if (url) {
|
||||
return (
|
||||
<DropdownMenu.Item key={label}>
|
||||
<a href={url}>{label}</a>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SubMenu key={index} title={label} icon={<Icon name="triangle-down" />}>
|
||||
{childs?.map((child: MenuObjectChildProps | string, index1: number) => {
|
||||
if (typeof child === 'string' && child === '-') {
|
||||
return <DropdownMenu.Divider key={`$${index1}`} />;
|
||||
}
|
||||
if (typeof child !== 'string') {
|
||||
return (
|
||||
<DropdownMenu.Item key={`${child.label}`}>
|
||||
{child.isFrontendRoute ? (
|
||||
<Link to={child.url || ''}>{child.label}</Link>
|
||||
) : (
|
||||
<a href={child.url}>{child.label}</a>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</SubMenu>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<StyledHeader className="top" id="main-menu">
|
||||
<Navbar inverse fluid staticTop role="navigation">
|
||||
<Navbar.Header>
|
||||
<Navbar.Brand>
|
||||
<a className="navbar-brand" href={brand.path}>
|
||||
<img width={brand.width} src={brand.icon} alt={brand.alt} />
|
||||
</a>
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle />
|
||||
</Navbar.Header>
|
||||
<Nav data-test="navbar-top">
|
||||
{menu.map((item, index) => {
|
||||
const props = {
|
||||
...item,
|
||||
isFrontendRoute: isFrontendRoute(item.url),
|
||||
childs: item.childs?.map(c => {
|
||||
if (typeof c === 'string') {
|
||||
return c;
|
||||
}
|
||||
|
||||
return {
|
||||
...c,
|
||||
isFrontendRoute: isFrontendRoute(c.url),
|
||||
};
|
||||
}),
|
||||
};
|
||||
return <MenuObject {...props} key={item.label} index={index + 1} />;
|
||||
})}
|
||||
</Nav>
|
||||
<Nav className="navbar-right">
|
||||
{!navbarRight.user_is_anonymous && <NewMenu />}
|
||||
<NavDropdown
|
||||
id="settings-dropdown"
|
||||
title={t('Settings')}
|
||||
onMouseEnter={() => setDropdownOpen(true)}
|
||||
onMouseLeave={() => setDropdownOpen(false)}
|
||||
onToggle={value => setDropdownOpen(value)}
|
||||
open={dropdownOpen}
|
||||
<StyledHeader className="top" id="main-menu" role="navigation">
|
||||
<Row>
|
||||
<Col lg={19} md={19} sm={24} xs={24}>
|
||||
<a className="navbar-brand" href={brand.path}>
|
||||
<img width={brand.width} src={brand.icon} alt={brand.alt} />
|
||||
</a>
|
||||
<DropdownMenu
|
||||
mode={showMenu}
|
||||
data-test="navbar-top"
|
||||
className="main-nav"
|
||||
>
|
||||
<DropdownMenu>
|
||||
{settings.map((section, index) => [
|
||||
<DropdownMenu.ItemGroup
|
||||
key={`${section.label}`}
|
||||
title={section.label}
|
||||
>
|
||||
{section.childs?.map(child => {
|
||||
if (typeof child !== 'string') {
|
||||
return (
|
||||
<DropdownMenu.Item key={`${child.label}`}>
|
||||
{isFrontendRoute(child.url) ? (
|
||||
<Link to={child.url || ''}>{child.label}</Link>
|
||||
) : (
|
||||
<a href={child.url}>{child.label}</a>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</DropdownMenu.ItemGroup>,
|
||||
index < settings.length - 1 && <DropdownMenu.Divider />,
|
||||
])}
|
||||
{menu.map((item, index) => {
|
||||
const props = {
|
||||
...item,
|
||||
isFrontendRoute: isFrontendRoute(item.url),
|
||||
childs: item.childs?.map(c => {
|
||||
if (typeof c === 'string') {
|
||||
return c;
|
||||
}
|
||||
|
||||
{!navbarRight.user_is_anonymous && [
|
||||
<DropdownMenu.Divider key="user-divider" />,
|
||||
<DropdownMenu.ItemGroup key="user-section" title={t('User')}>
|
||||
{navbarRight.user_profile_url && (
|
||||
<DropdownMenu.Item key="profile">
|
||||
<a href={navbarRight.user_profile_url}>{t('Profile')}</a>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Item key="info">
|
||||
<a href={navbarRight.user_info_url}>{t('Info')}</a>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item key="logout">
|
||||
<a href={navbarRight.user_logout_url}>{t('Logout')}</a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.ItemGroup>,
|
||||
]}
|
||||
{(navbarRight.version_string || navbarRight.version_sha) && [
|
||||
<DropdownMenu.Divider key="version-info-divider" />,
|
||||
<DropdownMenu.ItemGroup key="about-section" title={t('About')}>
|
||||
<div className="about-section">
|
||||
{navbarRight.version_string && (
|
||||
<li className="version-info">
|
||||
<span>Version: {navbarRight.version_string}</span>
|
||||
</li>
|
||||
)}
|
||||
{navbarRight.version_sha && (
|
||||
<li className="version-info">
|
||||
<span>SHA: {navbarRight.version_sha}</span>
|
||||
</li>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenu.ItemGroup>,
|
||||
]}
|
||||
</DropdownMenu>
|
||||
</NavDropdown>
|
||||
{navbarRight.documentation_url && (
|
||||
<NavItem
|
||||
href={navbarRight.documentation_url}
|
||||
target="_blank"
|
||||
title="Documentation"
|
||||
>
|
||||
<i className="fa fa-question" />
|
||||
|
||||
</NavItem>
|
||||
)}
|
||||
{navbarRight.bug_report_url && (
|
||||
<NavItem
|
||||
href={navbarRight.bug_report_url}
|
||||
target="_blank"
|
||||
title="Report a Bug"
|
||||
>
|
||||
<i className="fa fa-bug" />
|
||||
|
||||
</NavItem>
|
||||
)}
|
||||
{navbarRight.show_language_picker && (
|
||||
<LanguagePicker
|
||||
locale={navbarRight.locale}
|
||||
languages={navbarRight.languages}
|
||||
/>
|
||||
)}
|
||||
{navbarRight.user_is_anonymous && (
|
||||
<NavItem href={navbarRight.user_login_url}>
|
||||
<i className="fa fa-fw fa-sign-in" />
|
||||
{t('Login')}
|
||||
</NavItem>
|
||||
)}
|
||||
</Nav>
|
||||
</Navbar>
|
||||
return {
|
||||
...c,
|
||||
isFrontendRoute: isFrontendRoute(c.url),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return renderSubMenu(props);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</Col>
|
||||
<Col lg={5} md={5} sm={24} xs={24}>
|
||||
<RightMenu
|
||||
settings={settings}
|
||||
navbarRight={navbarRight}
|
||||
isFrontendRoute={isFrontendRoute}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</StyledHeader>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user