mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +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
@@ -72,6 +72,29 @@ export const MenuItem = styled(AntdMenu.Item)`
|
||||
&.ant-menu-item {
|
||||
height: ${({ theme }) => theme.gridUnit * 7}px;
|
||||
line-height: ${({ theme }) => theme.gridUnit * 7}px;
|
||||
a {
|
||||
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;
|
||||
@media (max-width: 767px) {
|
||||
background-color: ${({ theme }) => theme.colors.primary.light5};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-menu-item,
|
||||
@@ -84,10 +107,94 @@ export const MenuItem = styled(AntdMenu.Item)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledNav = styled(AntdMenu)`
|
||||
line-height: 51px;
|
||||
border: none;
|
||||
|
||||
& > .ant-menu-item,
|
||||
& > .ant-menu-submenu {
|
||||
vertical-align: inherit;
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
}
|
||||
}
|
||||
&:not(.ant-menu-dark) > .ant-menu-submenu,
|
||||
&:not(.ant-menu-dark) > .ant-menu-item {
|
||||
margin: 0px;
|
||||
&:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > .ant-menu-item > a {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
.ant-menu-submenu-open,
|
||||
.ant-menu-submenu-active {
|
||||
background-color: ${({ theme }) => theme.colors.primary.light5};
|
||||
.ant-menu-submenu-title {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
background-color: ${({ theme }) => theme.colors.primary.light5};
|
||||
border-bottom: none;
|
||||
margin: 0;
|
||||
&:after {
|
||||
opacity: 1;
|
||||
width: calc(100% - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledSubMenu = styled(AntdMenu.SubMenu)`
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
border-bottom: none;
|
||||
.ant-menu-submenu-title {
|
||||
&: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};
|
||||
}
|
||||
}
|
||||
& > .ant-menu-submenu-title {
|
||||
padding: 0 ${({ theme }) => theme.gridUnit * 6}px 0
|
||||
${({ theme }) => theme.gridUnit * 3}px !important;
|
||||
svg {
|
||||
position: absolute;
|
||||
top: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
right: ${({ theme }) => theme.gridUnit}px;
|
||||
width: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
}
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export declare type MenuMode =
|
||||
| 'vertical'
|
||||
| 'vertical-left'
|
||||
| 'vertical-right'
|
||||
| 'horizontal'
|
||||
| 'inline';
|
||||
export const Menu = Object.assign(AntdMenu, {
|
||||
Item: MenuItem,
|
||||
});
|
||||
|
||||
export const MainNav = Object.assign(StyledNav, {
|
||||
Item: MenuItem,
|
||||
SubMenu: StyledSubMenu,
|
||||
Divider: AntdMenu.Divider,
|
||||
ItemGroup: AntdMenu.ItemGroup,
|
||||
});
|
||||
|
||||
export const Input = styled(AntdInput)`
|
||||
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Menu } from './Menu';
|
||||
import { dropdownItems } from './MenuRight';
|
||||
|
||||
const mockedProps = {
|
||||
data: {
|
||||
@@ -157,18 +159,19 @@ test('should render all the top navbar menu items', () => {
|
||||
} = mockedProps;
|
||||
render(<Menu {...mockedProps} />);
|
||||
menu.forEach(item => {
|
||||
const menuItem = screen.getByText(item.label);
|
||||
expect(menuItem).toHaveAttribute('href', item.url);
|
||||
expect(screen.getByText(item.label)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render the top navbar child menu items', () => {
|
||||
test('should render the top navbar child menu items', async () => {
|
||||
const {
|
||||
data: { menu },
|
||||
} = mockedProps;
|
||||
render(<Menu {...mockedProps} />);
|
||||
const datasets = screen.getByText('Datasets');
|
||||
const databases = screen.getByText('Databases');
|
||||
const sources = screen.getByText('Sources');
|
||||
userEvent.hover(sources);
|
||||
const datasets = await screen.findByText('Datasets');
|
||||
const databases = await screen.findByText('Databases');
|
||||
const dataset = menu[1].childs![0] as { url: string };
|
||||
const database = menu[1].childs![2] as { url: string };
|
||||
|
||||
@@ -176,23 +179,53 @@ test('should render the top navbar child menu items', () => {
|
||||
expect(databases).toHaveAttribute('href', database.url);
|
||||
});
|
||||
|
||||
test('should render the Settings', () => {
|
||||
render(<Menu {...mockedProps} />);
|
||||
const settings = screen.getByText('Settings');
|
||||
expect(settings).toHaveAttribute('href', '#');
|
||||
test('should render the dropdown items', async () => {
|
||||
render(<Menu {...notanonProps} />);
|
||||
const dropdown = screen.getByTestId('new-dropdown-icon');
|
||||
userEvent.hover(dropdown);
|
||||
expect(await screen.findByText(dropdownItems[0].label)).toHaveAttribute(
|
||||
'href',
|
||||
dropdownItems[0].url,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(`menu-item-${dropdownItems[0].label}`),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText(dropdownItems[1].label)).toHaveAttribute(
|
||||
'href',
|
||||
dropdownItems[1].url,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(`menu-item-${dropdownItems[1].label}`),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText(dropdownItems[2].label)).toHaveAttribute(
|
||||
'href',
|
||||
dropdownItems[2].url,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(`menu-item-${dropdownItems[2].label}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Settings menu item', () => {
|
||||
test('should render the Settings', async () => {
|
||||
render(<Menu {...mockedProps} />);
|
||||
expect(screen.getByText('Security')).toBeInTheDocument();
|
||||
const settings = await screen.findByText('Settings');
|
||||
expect(settings).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Settings dropdown child menu items', () => {
|
||||
test('should render the Settings menu item', async () => {
|
||||
render(<Menu {...mockedProps} />);
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const label = await screen.findByText('Security');
|
||||
expect(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Settings dropdown child menu items', async () => {
|
||||
const {
|
||||
data: { settings },
|
||||
} = mockedProps;
|
||||
render(<Menu {...mockedProps} />);
|
||||
const listUsers = screen.getByText('List Users');
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const listUsers = await screen.findByText('List Users');
|
||||
expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url);
|
||||
});
|
||||
|
||||
@@ -206,7 +239,7 @@ test('should NOT render the plus menu (+) when user is anonymous', () => {
|
||||
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the user actions when user is not anonymous', () => {
|
||||
test('should render the user actions when user is not anonymous', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { user_info_url, user_logout_url },
|
||||
@@ -214,10 +247,12 @@ test('should render the user actions when user is not anonymous', () => {
|
||||
} = mockedProps;
|
||||
|
||||
render(<Menu {...notanonProps} />);
|
||||
expect(screen.getByText('User')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const user = await screen.findByText('User');
|
||||
expect(user).toBeInTheDocument();
|
||||
|
||||
const info = screen.getByText('Info');
|
||||
const logout = screen.getByText('Logout');
|
||||
const info = await screen.findByText('Info');
|
||||
const logout = await screen.findByText('Logout');
|
||||
|
||||
expect(info).toHaveAttribute('href', user_info_url);
|
||||
expect(logout).toHaveAttribute('href', user_logout_url);
|
||||
@@ -228,7 +263,7 @@ test('should NOT render the user actions when user is anonymous', () => {
|
||||
expect(screen.queryByText('User')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Profile link when available', () => {
|
||||
test('should render the Profile link when available', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { user_profile_url },
|
||||
@@ -236,11 +271,13 @@ test('should render the Profile link when available', () => {
|
||||
} = mockedProps;
|
||||
|
||||
render(<Menu {...notanonProps} />);
|
||||
const profile = screen.getByText('Profile');
|
||||
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const profile = await screen.findByText('Profile');
|
||||
expect(profile).toHaveAttribute('href', user_profile_url);
|
||||
});
|
||||
|
||||
test('should render the About section and version_string or sha when available', () => {
|
||||
test('should render the About section and version_string or sha when available', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { version_sha, version_string },
|
||||
@@ -248,24 +285,28 @@ test('should render the About section and version_string or sha when available',
|
||||
} = mockedProps;
|
||||
|
||||
render(<Menu {...mockedProps} />);
|
||||
expect(screen.getByText('About')).toBeInTheDocument();
|
||||
expect(screen.getByText(`Version: ${version_string}`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`SHA: ${version_sha}`)).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const about = await screen.findByText('About');
|
||||
const version = await screen.findByText(`Version: ${version_string}`);
|
||||
const sha = await screen.findByText(`SHA: ${version_sha}`);
|
||||
expect(about).toBeInTheDocument();
|
||||
expect(version).toBeInTheDocument();
|
||||
expect(sha).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Documentation link when available', () => {
|
||||
test('should render the Documentation link when available', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { documentation_url },
|
||||
},
|
||||
} = mockedProps;
|
||||
|
||||
render(<Menu {...mockedProps} />);
|
||||
const doc = screen.getByTitle('Documentation');
|
||||
expect(doc).toHaveAttribute('href', documentation_url);
|
||||
userEvent.hover(screen.getByText('Settings'));
|
||||
const doc = await screen.findByTitle('Documentation');
|
||||
expect(doc.firstChild).toHaveAttribute('href', documentation_url);
|
||||
});
|
||||
|
||||
test('should render the Bug Report link when available', () => {
|
||||
test('should render the Bug Report link when available', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { bug_report_url },
|
||||
@@ -273,8 +314,8 @@ test('should render the Bug Report link when available', () => {
|
||||
} = mockedProps;
|
||||
|
||||
render(<Menu {...mockedProps} />);
|
||||
const bugReport = screen.getByTitle('Report a Bug');
|
||||
expect(bugReport).toHaveAttribute('href', bug_report_url);
|
||||
const bugReport = await screen.findByTitle('Report a Bug');
|
||||
expect(bugReport.firstChild).toHaveAttribute('href', bug_report_url);
|
||||
});
|
||||
|
||||
test('should render the Login link when user is anonymous', () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { NavItem } from 'react-bootstrap';
|
||||
import { Menu } from 'src/common/components';
|
||||
import NavDropdown from '../NavDropdown';
|
||||
|
||||
export interface MenuObjectChildProps {
|
||||
label: string;
|
||||
name?: string;
|
||||
icon: string;
|
||||
index: number;
|
||||
url?: string;
|
||||
isFrontendRoute?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuObjectProps extends MenuObjectChildProps {
|
||||
childs?: (MenuObjectChildProps | string)[];
|
||||
isHeader?: boolean;
|
||||
}
|
||||
|
||||
export default function MenuObject({
|
||||
label,
|
||||
childs,
|
||||
url,
|
||||
index,
|
||||
isFrontendRoute,
|
||||
}: MenuObjectProps) {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
if (url && isFrontendRoute) {
|
||||
return (
|
||||
<li role="presentation">
|
||||
<Link role="button" to={url}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (url) {
|
||||
return (
|
||||
<NavItem eventKey={index} href={url}>
|
||||
{label}
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavDropdown
|
||||
id={`menu-dropdown-${label}`}
|
||||
title={label}
|
||||
onMouseEnter={() => setDropdownOpen(true)}
|
||||
onMouseLeave={() => setDropdownOpen(false)}
|
||||
onToggle={value => setDropdownOpen(value)}
|
||||
open={dropdownOpen}
|
||||
>
|
||||
<Menu>
|
||||
{childs?.map((child: MenuObjectChildProps | string, index1: number) => {
|
||||
if (typeof child === 'string' && child === '-') {
|
||||
return <Menu.Divider key={`$${index1}`} />;
|
||||
}
|
||||
if (typeof child !== 'string') {
|
||||
return (
|
||||
<Menu.Item key={`${child.label}`}>
|
||||
{child.isFrontendRoute ? (
|
||||
<Link to={child.url || ''}>{child.label}</Link>
|
||||
) : (
|
||||
<a href={child.url}>{child.label}</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Menu>
|
||||
</NavDropdown>
|
||||
);
|
||||
}
|
||||
181
superset-frontend/src/components/Menu/MenuRight.tsx
Normal file
181
superset-frontend/src/components/Menu/MenuRight.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { MainNav as Menu } from 'src/common/components';
|
||||
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Icon from 'src/components/Icon';
|
||||
import LanguagePicker from './LanguagePicker';
|
||||
import { NavBarProps, MenuObjectProps } from './Menu';
|
||||
|
||||
export const dropdownItems = [
|
||||
{
|
||||
label: t('SQL query'),
|
||||
url: '/superset/sqllab',
|
||||
icon: 'fa-fw fa-search',
|
||||
},
|
||||
{
|
||||
label: t('Chart'),
|
||||
url: '/chart/add',
|
||||
icon: 'fa-fw fa-bar-chart',
|
||||
},
|
||||
{
|
||||
label: t('Dashboard'),
|
||||
url: '/dashboard/new',
|
||||
icon: 'fa-fw fa-dashboard',
|
||||
},
|
||||
];
|
||||
|
||||
const versionInfoStyles = (theme: SupersetTheme) => css`
|
||||
padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 4}px
|
||||
${theme.gridUnit * 1.5}px ${theme.gridUnit * 7}px;
|
||||
color: ${theme.colors.grayscale.base};
|
||||
font-size: ${theme.typography.sizes.xs}px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
const StyledI = styled.div`
|
||||
color: ${({ theme }) => theme.colors.primary.dark1};
|
||||
`;
|
||||
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
interface RightMenuProps {
|
||||
settings: MenuObjectProps[];
|
||||
navbarRight: NavBarProps;
|
||||
isFrontendRoute: (path?: string) => boolean;
|
||||
}
|
||||
|
||||
const RightMenu = ({
|
||||
settings,
|
||||
navbarRight,
|
||||
isFrontendRoute,
|
||||
}: RightMenuProps) => (
|
||||
<Menu className="navbar-right" mode="horizontal">
|
||||
{!navbarRight.user_is_anonymous && (
|
||||
<SubMenu
|
||||
data-test="new-dropdown"
|
||||
title={<StyledI data-test="new-dropdown-icon" className="fa fa-plus" />}
|
||||
icon={<Icon name="triangle-down" />}
|
||||
>
|
||||
{dropdownItems.map((menu, i) => (
|
||||
<Menu.Item key={i}>
|
||||
<a href={menu.url}>
|
||||
<i
|
||||
data-test={`menu-item-${menu.label}`}
|
||||
className={`fa ${menu.icon}`}
|
||||
/>{' '}
|
||||
{menu.label}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</SubMenu>
|
||||
)}
|
||||
<SubMenu title="Settings" icon={<Icon name="triangle-down" />}>
|
||||
{settings.map((section, index) => [
|
||||
<Menu.ItemGroup key={`${section.label}`} title={section.label}>
|
||||
{section.childs?.map(child => {
|
||||
if (typeof child !== 'string') {
|
||||
return (
|
||||
<Menu.Item key={`${child.label}`}>
|
||||
{isFrontendRoute(child.url) ? (
|
||||
<Link to={child.url || ''}>{child.label}</Link>
|
||||
) : (
|
||||
<a href={child.url}>{child.label}</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Menu.ItemGroup>,
|
||||
index < settings.length - 1 && <Menu.Divider />,
|
||||
])}
|
||||
|
||||
{!navbarRight.user_is_anonymous && [
|
||||
<Menu.Divider key="user-divider" />,
|
||||
<Menu.ItemGroup key="user-section" title={t('User')}>
|
||||
{navbarRight.user_profile_url && (
|
||||
<Menu.Item key="profile">
|
||||
<a href={navbarRight.user_profile_url}>{t('Profile')}</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="info">
|
||||
<a href={navbarRight.user_info_url}>{t('Info')}</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout">
|
||||
<a href={navbarRight.user_logout_url}>{t('Logout')}</a>
|
||||
</Menu.Item>
|
||||
</Menu.ItemGroup>,
|
||||
]}
|
||||
{(navbarRight.version_string || navbarRight.version_sha) && [
|
||||
<Menu.Divider key="version-info-divider" />,
|
||||
<Menu.ItemGroup key="about-section" title={t('About')}>
|
||||
<div className="about-section">
|
||||
{navbarRight.version_string && (
|
||||
<li css={versionInfoStyles}>
|
||||
Version: {navbarRight.version_string}
|
||||
</li>
|
||||
)}
|
||||
{navbarRight.version_sha && (
|
||||
<li css={versionInfoStyles}>SHA: {navbarRight.version_sha}</li>
|
||||
)}
|
||||
</div>
|
||||
</Menu.ItemGroup>,
|
||||
]}
|
||||
</SubMenu>
|
||||
{navbarRight.documentation_url && (
|
||||
<Menu.Item title="Documentation">
|
||||
<a
|
||||
href={navbarRight.documentation_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i className="fa fa-question" />
|
||||
|
||||
</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{navbarRight.bug_report_url && (
|
||||
<Menu.Item title="Report a Bug">
|
||||
<a href={navbarRight.bug_report_url} target="_blank" rel="noreferrer">
|
||||
<i className="fa fa-bug" />
|
||||
</a>
|
||||
|
||||
</Menu.Item>
|
||||
)}
|
||||
{navbarRight.show_language_picker && (
|
||||
<Menu.Item>
|
||||
<LanguagePicker
|
||||
locale={navbarRight.locale}
|
||||
languages={navbarRight.languages}
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{navbarRight.user_is_anonymous && (
|
||||
<Menu.Item>
|
||||
<a href={navbarRight.user_login_url}>
|
||||
<i className="fa fa-fw fa-sign-in" />
|
||||
{t('Login')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
export default RightMenu;
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import NewMenu, { dropdownItems } from './NewMenu';
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<NewMenu />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the dropdown items', () => {
|
||||
render(<NewMenu />);
|
||||
dropdownItems.forEach(item => {
|
||||
expect(screen.getByText(item.label)).toHaveAttribute('href', item.url);
|
||||
expect(screen.getByTestId(`menu-item-${item.label}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { Menu } from 'src/common/components';
|
||||
import NavDropdown from 'src/components/NavDropdown';
|
||||
|
||||
export const dropdownItems = [
|
||||
{
|
||||
label: t('SQL query'),
|
||||
url: '/superset/sqllab',
|
||||
icon: 'fa-fw fa-search',
|
||||
},
|
||||
{
|
||||
label: t('Chart'),
|
||||
url: '/chart/add',
|
||||
icon: 'fa-fw fa-bar-chart',
|
||||
},
|
||||
{
|
||||
label: t('Dashboard'),
|
||||
url: '/dashboard/new',
|
||||
icon: 'fa-fw fa-dashboard',
|
||||
},
|
||||
];
|
||||
const StyledI = styled.div`
|
||||
color: ${({ theme }) => theme.colors.primary.dark1};
|
||||
`;
|
||||
|
||||
export default function NewMenu() {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<NavDropdown
|
||||
id="new-dropdown"
|
||||
data-test="new-dropdown"
|
||||
title={<StyledI className="fa fa-plus" />}
|
||||
onMouseEnter={() => setDropdownOpen(true)}
|
||||
onMouseLeave={() => setDropdownOpen(false)}
|
||||
onToggle={value => setDropdownOpen(value)}
|
||||
open={dropdownOpen}
|
||||
>
|
||||
<Menu>
|
||||
{dropdownItems.map((menu, i) => (
|
||||
<Menu.Item key={i}>
|
||||
<a href={menu.url}>
|
||||
<i
|
||||
data-test={`menu-item-${menu.label}`}
|
||||
className={`fa ${menu.icon}`}
|
||||
/>{' '}
|
||||
{menu.label}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</NavDropdown>
|
||||
);
|
||||
}
|
||||
@@ -16,35 +16,65 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useState, useEffect } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import cx from 'classnames';
|
||||
import { Nav, Navbar } from 'react-bootstrap';
|
||||
import { debounce } from 'lodash';
|
||||
import { Col, Row } from 'antd';
|
||||
import { Menu, MenuMode } from 'src/common/components';
|
||||
import Button, { OnClickHandler } from 'src/components/Button';
|
||||
|
||||
const StyledHeader = styled.header`
|
||||
const StyledHeader = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
.navbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.navbar-header .navbar-brand {
|
||||
.header {
|
||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 3}px;
|
||||
text-align: left;
|
||||
font-size: 18px;
|
||||
padding: ${({ theme }) => theme.gridUnit * 3}px;
|
||||
display: inline-block;
|
||||
line-height: ${({ theme }) => theme.gridUnit * 9}px;
|
||||
}
|
||||
.navbar-right {
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
margin-right: 0;
|
||||
padding: 14px 0;
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 3}px;
|
||||
float: right;
|
||||
}
|
||||
.navbar-nav {
|
||||
.nav-right-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 0;
|
||||
margin-right: 0;
|
||||
float: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.menu {
|
||||
background-color: white;
|
||||
.ant-menu-horizontal {
|
||||
line-height: inherit;
|
||||
.ant-menu-item {
|
||||
&:hover {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-menu {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item {
|
||||
margin: 0 ${({ theme }) => theme.gridUnit + 1}px;
|
||||
}
|
||||
|
||||
.menu .ant-menu-item {
|
||||
li {
|
||||
a,
|
||||
div {
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
padding: ${({ theme }) => theme.gridUnit * 2}px 0;
|
||||
margin: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
color: ${({ theme }) => theme.colors.secondary.dark1};
|
||||
|
||||
a {
|
||||
@@ -68,23 +98,23 @@ const StyledHeader = styled.header`
|
||||
border-bottom: none;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
.navbar-inverse {
|
||||
.navbar-nav {
|
||||
& > .active > a {
|
||||
background: ${({ theme }) => theme.colors.secondary.light4};
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.colors.secondary.light4};
|
||||
}
|
||||
}
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.ant-menu-horizontal {
|
||||
border: none;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.header,
|
||||
.nav-right {
|
||||
float: left;
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type MenuChild = {
|
||||
@@ -119,9 +149,16 @@ export interface SubMenuProps {
|
||||
* ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
|
||||
* otherwise, a 'You should not use <Link> outside a <Router>' error will be thrown */
|
||||
usesRouter?: boolean;
|
||||
color?: string;
|
||||
headerSize?: number;
|
||||
}
|
||||
|
||||
const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
|
||||
const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
|
||||
const [showMenu, setMenu] = useState<MenuMode>('horizontal');
|
||||
const [navRightStyle, setNavRightStyle] = useState('nav-right');
|
||||
const [navRightCol, setNavRightCol] = useState(8);
|
||||
|
||||
const { headerSize = 2 } = props;
|
||||
let hasHistory = true;
|
||||
// If no parent <Router> component exists, useHistory throws an error
|
||||
try {
|
||||
@@ -130,64 +167,101 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
|
||||
// If error is thrown, we know not to use <Link> in render
|
||||
hasHistory = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
if (window.innerWidth <= 767) setMenu('inline');
|
||||
else setMenu('horizontal');
|
||||
|
||||
if (
|
||||
props.buttons &&
|
||||
props.buttons.length >= 3 &&
|
||||
window.innerWidth >= 795
|
||||
) {
|
||||
setNavRightCol(8);
|
||||
setNavRightStyle('nav-right');
|
||||
} else if (
|
||||
props.buttons &&
|
||||
props.buttons.length >= 3 &&
|
||||
window.innerWidth <= 795
|
||||
) {
|
||||
setNavRightCol(24);
|
||||
setNavRightStyle('nav-right-collapse');
|
||||
}
|
||||
}
|
||||
handleResize();
|
||||
const resize = debounce(handleResize, 10);
|
||||
window.addEventListener('resize', resize);
|
||||
return () => window.removeEventListener('resize', resize);
|
||||
}, [props.buttons]);
|
||||
|
||||
const offset = props.name ? headerSize : 0;
|
||||
|
||||
return (
|
||||
<StyledHeader>
|
||||
<Navbar inverse fluid role="navigation">
|
||||
<Navbar.Header>
|
||||
<Navbar.Brand>{props.name}</Navbar.Brand>
|
||||
</Navbar.Header>
|
||||
<Nav>
|
||||
{props.tabs &&
|
||||
props.tabs.map(tab => {
|
||||
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
||||
return (
|
||||
<React.Fragment key={tab.label}>
|
||||
<li
|
||||
role="tab"
|
||||
data-test={tab['data-test']}
|
||||
className={tab.name === props.activeChild ? 'active' : ''}
|
||||
>
|
||||
<div>
|
||||
<Link to={tab.url || ''}>{tab.label}</Link>
|
||||
</div>
|
||||
</li>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
<Row className="menu" role="navigation">
|
||||
{props.name && (
|
||||
<Col md={offset} xs={24}>
|
||||
<div className="header">{props.name}</div>
|
||||
</Col>
|
||||
)}
|
||||
<Col md={16 - offset} sm={24} xs={24}>
|
||||
<Menu mode={showMenu} style={{ backgroundColor: 'transparent' }}>
|
||||
{props.tabs &&
|
||||
props.tabs.map(tab => {
|
||||
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
||||
return (
|
||||
<Menu.Item key={tab.label}>
|
||||
<li
|
||||
role="tab"
|
||||
data-test={tab['data-test']}
|
||||
className={
|
||||
tab.name === props.activeChild ? 'active' : ''
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Link to={tab.url || ''}>{tab.label}</Link>
|
||||
</div>
|
||||
</li>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={tab.label}>
|
||||
<li
|
||||
className={cx('no-router', {
|
||||
active: tab.name === props.activeChild,
|
||||
})}
|
||||
role="tab"
|
||||
>
|
||||
<a href={tab.url} onClick={tab.onClick}>
|
||||
{tab.label}
|
||||
</a>
|
||||
</li>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Nav>
|
||||
<Nav className="navbar-right">
|
||||
{props.buttons?.map((btn, i) => (
|
||||
<React.Fragment key={`${i}`}>
|
||||
return (
|
||||
<Menu.Item key={tab.label}>
|
||||
<li
|
||||
className={cx('no-router', {
|
||||
active: tab.name === props.activeChild,
|
||||
})}
|
||||
role="tab"
|
||||
>
|
||||
<a href={tab.url} onClick={tab.onClick}>
|
||||
{tab.label}
|
||||
</a>
|
||||
</li>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</Col>
|
||||
<Col lg={8} md={navRightCol} sm={24} xs={24}>
|
||||
<div className={navRightStyle}>
|
||||
{props.buttons?.map((btn, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
buttonStyle={btn.buttonStyle}
|
||||
onClick={btn.onClick}
|
||||
data-test={btn['data-test']}
|
||||
>
|
||||
{btn.name}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Nav>
|
||||
</Navbar>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
{props.children}
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubMenu;
|
||||
export default SubMenuComponent;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
/* header has mysterious extra margin */
|
||||
header.top {
|
||||
margin-bottom: -20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -344,7 +344,11 @@ function AnnotationLayersList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu name={t('Annotation layers')} buttons={subMenuButtons} />
|
||||
<SubMenu
|
||||
headerSize={8}
|
||||
name={t('Annotation layers')}
|
||||
buttons={subMenuButtons}
|
||||
/>
|
||||
<AnnotationLayerModal
|
||||
addDangerToast={addDangerToast}
|
||||
layer={currentAnnotationLayer}
|
||||
|
||||
@@ -303,7 +303,7 @@ function CssTemplatesList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu {...menuData} />
|
||||
<SubMenu headerSize={8} {...menuData} />
|
||||
<CssTemplateModal
|
||||
addDangerToast={addDangerToast}
|
||||
cssTemplate={currentCssTemplate}
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('ActivityTable', () => {
|
||||
expect(wrapper.find(ActivityTable)).toExist();
|
||||
});
|
||||
it('renders tabs with three buttons', () => {
|
||||
expect(wrapper.find('li')).toHaveLength(3);
|
||||
expect(wrapper.find('li.no-router')).toHaveLength(3);
|
||||
});
|
||||
it('renders ActivityCards', async () => {
|
||||
expect(wrapper.find('ListViewCard')).toExist();
|
||||
@@ -119,7 +119,7 @@ describe('ActivityTable', () => {
|
||||
expect(chartCall).toHaveLength(1);
|
||||
expect(dashboardCall).toHaveLength(1);
|
||||
});
|
||||
it('show empty state if there is data', () => {
|
||||
it('show empty state if there is no data', () => {
|
||||
const activityProps = {
|
||||
activeChild: 'Created',
|
||||
activityData: {},
|
||||
|
||||
@@ -239,14 +239,12 @@ export default function ActivityTable({
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
tabs={tabs}
|
||||
/>
|
||||
<>
|
||||
{activityData[activeChild]?.length > 0 ||
|
||||
(activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? (
|
||||
<ActivityContainer>{renderActivity()}</ActivityContainer>
|
||||
) : (
|
||||
<EmptyState tableName="RECENTS" tab={activeChild} />
|
||||
)}
|
||||
</>
|
||||
{activityData[activeChild]?.length > 0 ||
|
||||
(activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? (
|
||||
<ActivityContainer>{renderActivity()}</ActivityContainer>
|
||||
) : (
|
||||
<EmptyState tableName="RECENTS" tab={activeChild} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('DashboardTable', () => {
|
||||
|
||||
it('render a submenu with clickable tabs and buttons', async () => {
|
||||
expect(wrapper.find('SubMenu')).toExist();
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('li.no-router')).toHaveLength(2);
|
||||
expect(wrapper.find('Button')).toHaveLength(4);
|
||||
act(() => {
|
||||
const handler = wrapper.find('li.no-router a').at(1).prop('onClick');
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('SavedQueries', () => {
|
||||
|
||||
it('renders a submenu with clickable tables and buttons', async () => {
|
||||
expect(wrapper.find(SubMenu)).toExist();
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('li.no-router')).toHaveLength(1);
|
||||
expect(wrapper.find('button')).toHaveLength(2);
|
||||
clickTab(0);
|
||||
await waitForComponentToPaint(wrapper);
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface ActivityData {
|
||||
|
||||
const WelcomeContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
nav {
|
||||
.ant-row.menu {
|
||||
margin-top: -15px;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
&:after {
|
||||
@@ -64,25 +64,18 @@ const WelcomeContainer = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
margin: 0px ${({ theme }) => theme.gridUnit * 6}px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
${[mq[1]]} {
|
||||
margin-top: 5px;
|
||||
margin: 0px 2px;
|
||||
}
|
||||
}
|
||||
.nav.navbar-nav {
|
||||
& > li:nth-of-type(1),
|
||||
& > li:nth-of-type(2),
|
||||
& > li:nth-of-type(3) {
|
||||
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
.ant-menu.ant-menu-light.ant-menu-root.ant-menu-horizontal {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 10}px;
|
||||
}
|
||||
button {
|
||||
padding: 3px 21px;
|
||||
}
|
||||
.navbar-right {
|
||||
position: relative;
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
.ant-card.ant-card-bordered {
|
||||
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
@@ -92,7 +85,6 @@ const WelcomeContainer = styled.div`
|
||||
const WelcomeNav = styled.div`
|
||||
height: 50px;
|
||||
background-color: white;
|
||||
margin-top: ${({ theme }) => theme.gridUnit * -4 - 1}px;
|
||||
.navbar-brand {
|
||||
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||
|
||||
@@ -582,3 +582,28 @@ hr {
|
||||
background-image: url('../images/icons/error_solid_small_red.svg') !important;
|
||||
background-position: -2px center !important;
|
||||
}
|
||||
|
||||
// AntD overrides since these are injected as inline styles and can't
|
||||
// be overriden in emotion
|
||||
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light.ant-menu-submenu-placement-bottomLeft {
|
||||
top: 51px !important;
|
||||
margin-left: -2px !important;
|
||||
@media (max-width: 767px) {
|
||||
top: 269px !important;
|
||||
}
|
||||
& > .ant-menu {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light {
|
||||
top: 51px !important;
|
||||
border-radius: 0px !important;
|
||||
@media (max-width: 767px) {
|
||||
top: 269px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-dropdown.ant-dropdown-placement-bottomRight {
|
||||
top: 133px !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user