368 lines
9.8 KiB
TypeScript
368 lines
9.8 KiB
TypeScript
/**
|
|
* 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, useEffect } from 'react';
|
|
import { styled, css } from '@superset-ui/core';
|
|
import { debounce } from 'lodash';
|
|
import { Global } from '@emotion/react';
|
|
import { getUrlParam } from 'src/utils/urlUtils';
|
|
import {
|
|
MainNav as DropdownMenu,
|
|
MenuMode,
|
|
Row,
|
|
Col,
|
|
Grid,
|
|
} from 'src/common/components';
|
|
import { Tooltip } from 'src/components/Tooltip';
|
|
import { Link } from 'react-router-dom';
|
|
import Icons from 'src/components/Icons';
|
|
import { useUiConfig } from 'src/components/UiConfigContext';
|
|
import { URL_PARAMS } from 'src/constants';
|
|
import RightMenu from './MenuRight';
|
|
import { Languages } from './LanguagePicker';
|
|
|
|
interface BrandProps {
|
|
path: string;
|
|
icon: string;
|
|
alt: string;
|
|
width: string | number;
|
|
tooltip: string;
|
|
text: string;
|
|
}
|
|
|
|
export interface NavBarProps {
|
|
show_watermark: boolean;
|
|
bug_report_url?: string;
|
|
version_string?: string;
|
|
version_sha?: string;
|
|
build_number?: string;
|
|
documentation_url?: string;
|
|
languages: Languages;
|
|
show_language_picker: boolean;
|
|
user_is_anonymous: boolean;
|
|
user_info_url: string;
|
|
user_login_url: string;
|
|
user_logout_url: string;
|
|
user_profile_url: string | null;
|
|
locale: string;
|
|
}
|
|
|
|
export interface MenuProps {
|
|
data: {
|
|
menu: MenuObjectProps[];
|
|
brand: BrandProps;
|
|
navbar_right: NavBarProps;
|
|
settings: MenuObjectProps[];
|
|
};
|
|
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;
|
|
&:nth-last-of-type(2) nav {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.caret {
|
|
display: none;
|
|
}
|
|
.navbar-brand {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
.navbar-brand-text {
|
|
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
|
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
|
height: 100%;
|
|
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
|
padding-left: ${({ theme }) => theme.gridUnit * 4}px;
|
|
padding-right: ${({ theme }) => theme.gridUnit * 4}px;
|
|
margin-right: ${({ theme }) => theme.gridUnit * 6}px;
|
|
font-size: ${({ theme }) => theme.gridUnit * 4}px;
|
|
float: left;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
|
|
span {
|
|
max-width: ${({ theme }) => theme.gridUnit * 58}px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
@media (max-width: 1127px) {
|
|
display: none;
|
|
}
|
|
}
|
|
.main-nav .ant-menu-submenu-title > svg {
|
|
top: ${({ theme }) => theme.gridUnit * 5.25}px;
|
|
}
|
|
@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;
|
|
}
|
|
@media (max-width: 767px) {
|
|
.ant-menu-item {
|
|
padding: 0 ${({ theme }) => theme.gridUnit * 6}px 0
|
|
${({ theme }) => theme.gridUnit * 3}px !important;
|
|
}
|
|
.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
.ant-menu-item a {
|
|
&:hover {
|
|
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
|
background-color: ${({ theme }) => theme.colors.primary.light5};
|
|
border-bottom: none;
|
|
margin: 0;
|
|
&:after {
|
|
opacity: 1;
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const { SubMenu } = DropdownMenu;
|
|
|
|
const { useBreakpoint } = Grid;
|
|
|
|
export function Menu({
|
|
data: { menu, brand, navbar_right: navbarRight, settings },
|
|
isFrontendRoute = () => false,
|
|
}: MenuProps) {
|
|
const [showMenu, setMenu] = useState<MenuMode>('horizontal');
|
|
const screens = useBreakpoint();
|
|
const uiConig = useUiConfig();
|
|
|
|
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);
|
|
}, []);
|
|
|
|
const standalone = getUrlParam(URL_PARAMS.standalone);
|
|
if (standalone || uiConig.hideNav) 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={showMenu === 'inline' ? <></> : <Icons.TriangleDown />}
|
|
>
|
|
{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" role="navigation">
|
|
<Global
|
|
styles={css`
|
|
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light.ant-menu-submenu-placement-bottomLeft {
|
|
border-radius: 0px;
|
|
}
|
|
.ant-menu-submenu.ant-menu-submenu-popup.ant-menu.ant-menu-light {
|
|
border-radius: 0px;
|
|
}
|
|
`}
|
|
/>
|
|
<Row>
|
|
<Col md={16} xs={24}>
|
|
<Tooltip
|
|
id="brand-tooltip"
|
|
placement="bottomLeft"
|
|
title={brand.tooltip}
|
|
arrowPointAtCenter
|
|
>
|
|
<a className="navbar-brand" href={brand.path}>
|
|
<img width={brand.width} src={brand.icon} alt={brand.alt} />
|
|
</a>
|
|
</Tooltip>
|
|
{brand.text && (
|
|
<div className="navbar-brand-text">
|
|
<span>{brand.text}</span>
|
|
</div>
|
|
)}
|
|
<DropdownMenu
|
|
mode={showMenu}
|
|
data-test="navbar-top"
|
|
className="main-nav"
|
|
>
|
|
{menu.map(item => {
|
|
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 renderSubMenu(props);
|
|
})}
|
|
</DropdownMenu>
|
|
</Col>
|
|
<Col md={8} xs={24}>
|
|
<RightMenu
|
|
align={screens.md ? 'flex-end' : 'flex-start'}
|
|
settings={settings}
|
|
navbarRight={navbarRight}
|
|
isFrontendRoute={isFrontendRoute}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</StyledHeader>
|
|
);
|
|
}
|
|
|
|
// transform the menu data to reorganize components
|
|
export default function MenuWrapper({ data, ...rest }: MenuProps) {
|
|
const newMenuData = {
|
|
...data,
|
|
};
|
|
// Menu items that should go into settings dropdown
|
|
const settingsMenus = {
|
|
Security: true,
|
|
Manage: true,
|
|
};
|
|
|
|
// Cycle through menu.menu to build out cleanedMenu and settings
|
|
const cleanedMenu: MenuObjectProps[] = [];
|
|
const settings: MenuObjectProps[] = [];
|
|
newMenuData.menu.forEach((item: any) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
const children: (MenuObjectProps | string)[] = [];
|
|
const newItem = {
|
|
...item,
|
|
};
|
|
|
|
// Filter childs
|
|
if (item.childs) {
|
|
item.childs.forEach((child: MenuObjectChildProps | string) => {
|
|
if (typeof child === 'string') {
|
|
children.push(child);
|
|
} else if ((child as MenuObjectChildProps).label) {
|
|
children.push(child);
|
|
}
|
|
});
|
|
|
|
newItem.childs = children;
|
|
}
|
|
|
|
if (!settingsMenus.hasOwnProperty(item.name)) {
|
|
cleanedMenu.push(newItem);
|
|
} else {
|
|
settings.push(newItem);
|
|
}
|
|
});
|
|
|
|
newMenuData.menu = cleanedMenu;
|
|
newMenuData.settings = settings;
|
|
|
|
return <Menu data={newMenuData} {...rest} />;
|
|
}
|