diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index c1ce9c2296d..1706d11befe 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -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; diff --git a/superset-frontend/src/components/Menu/Menu.test.tsx b/superset-frontend/src/components/Menu/Menu.test.tsx index b3a2273eb1b..add4344b4d1 100644 --- a/superset-frontend/src/components/Menu/Menu.test.tsx +++ b/superset-frontend/src/components/Menu/Menu.test.tsx @@ -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.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(); - 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(); - const settings = screen.getByText('Settings'); - expect(settings).toHaveAttribute('href', '#'); +test('should render the dropdown items', async () => { + render(); + 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(); - 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(); + 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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', () => { diff --git a/superset-frontend/src/components/Menu/Menu.tsx b/superset-frontend/src/components/Menu/Menu.tsx index 0503f9f6020..d9385230996 100644 --- a/superset-frontend/src/components/Menu/Menu.tsx +++ b/superset-frontend/src/components/Menu/Menu.tsx @@ -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('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 ( + + + {label} + + + ); + } + if (url) { + return ( + + {label} + + ); + } + return ( + }> + {childs?.map((child: MenuObjectChildProps | string, index1: number) => { + if (typeof child === 'string' && child === '-') { + return ; + } + if (typeof child !== 'string') { + return ( + + {child.isFrontendRoute ? ( + {child.label} + ) : ( + {child.label} + )} + + ); + } + return null; + })} + + ); + }; return ( - - - - - - {brand.alt} - - - - - - - + return { + ...c, + isFrontendRoute: isFrontendRoute(c.url), + }; + }), + }; + + return renderSubMenu(props); + })} + + + + + + ); } diff --git a/superset-frontend/src/components/Menu/MenuObject.tsx b/superset-frontend/src/components/Menu/MenuObject.tsx deleted file mode 100644 index 012172d8910..00000000000 --- a/superset-frontend/src/components/Menu/MenuObject.tsx +++ /dev/null @@ -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 ( -
  • - - {label} - -
  • - ); - } - if (url) { - return ( - - {label} - - ); - } - - return ( - setDropdownOpen(true)} - onMouseLeave={() => setDropdownOpen(false)} - onToggle={value => setDropdownOpen(value)} - open={dropdownOpen} - > - - {childs?.map((child: MenuObjectChildProps | string, index1: number) => { - if (typeof child === 'string' && child === '-') { - return ; - } - if (typeof child !== 'string') { - return ( - - {child.isFrontendRoute ? ( - {child.label} - ) : ( - {child.label} - )} - - ); - } - return null; - })} - - - ); -} diff --git a/superset-frontend/src/components/Menu/MenuRight.tsx b/superset-frontend/src/components/Menu/MenuRight.tsx new file mode 100644 index 00000000000..26985d6d71c --- /dev/null +++ b/superset-frontend/src/components/Menu/MenuRight.tsx @@ -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) => ( + + {!navbarRight.user_is_anonymous && ( + } + icon={} + > + {dropdownItems.map((menu, i) => ( + + + {' '} + {menu.label} + + + ))} + + )} + }> + {settings.map((section, index) => [ + + {section.childs?.map(child => { + if (typeof child !== 'string') { + return ( + + {isFrontendRoute(child.url) ? ( + {child.label} + ) : ( + {child.label} + )} + + ); + } + return null; + })} + , + index < settings.length - 1 && , + ])} + + {!navbarRight.user_is_anonymous && [ + , + + {navbarRight.user_profile_url && ( + + {t('Profile')} + + )} + + {t('Info')} + + + {t('Logout')} + + , + ]} + {(navbarRight.version_string || navbarRight.version_sha) && [ + , + +
    + {navbarRight.version_string && ( +
  • + Version: {navbarRight.version_string} +
  • + )} + {navbarRight.version_sha && ( +
  • SHA: {navbarRight.version_sha}
  • + )} +
    +
    , + ]} +
    + {navbarRight.documentation_url && ( + + + +   + + + )} + {navbarRight.bug_report_url && ( + + + + +   + + )} + {navbarRight.show_language_picker && ( + + + + )} + {navbarRight.user_is_anonymous && ( + + + + {t('Login')} + + + )} +
    +); + +export default RightMenu; diff --git a/superset-frontend/src/components/Menu/NewMenu.test.tsx b/superset-frontend/src/components/Menu/NewMenu.test.tsx deleted file mode 100644 index b325370cec3..00000000000 --- a/superset-frontend/src/components/Menu/NewMenu.test.tsx +++ /dev/null @@ -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(); - expect(container).toBeInTheDocument(); -}); - -test('should render the dropdown items', () => { - render(); - dropdownItems.forEach(item => { - expect(screen.getByText(item.label)).toHaveAttribute('href', item.url); - expect(screen.getByTestId(`menu-item-${item.label}`)).toBeInTheDocument(); - }); -}); diff --git a/superset-frontend/src/components/Menu/NewMenu.tsx b/superset-frontend/src/components/Menu/NewMenu.tsx deleted file mode 100644 index 8ff46d29538..00000000000 --- a/superset-frontend/src/components/Menu/NewMenu.tsx +++ /dev/null @@ -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 ( - } - onMouseEnter={() => setDropdownOpen(true)} - onMouseLeave={() => setDropdownOpen(false)} - onToggle={value => setDropdownOpen(value)} - open={dropdownOpen} - > - - {dropdownItems.map((menu, i) => ( - - - {' '} - {menu.label} - - - ))} - - - ); -} diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx index f8f9e3a9d34..27f4b86d894 100644 --- a/superset-frontend/src/components/Menu/SubMenu.tsx +++ b/superset-frontend/src/components/Menu/SubMenu.tsx @@ -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 ; * otherwise, a 'You should not use outside a ' error will be thrown */ usesRouter?: boolean; + color?: string; + headerSize?: number; } -const SubMenu: React.FunctionComponent = props => { +const SubMenuComponent: React.FunctionComponent = props => { + const [showMenu, setMenu] = useState('horizontal'); + const [navRightStyle, setNavRightStyle] = useState('nav-right'); + const [navRightCol, setNavRightCol] = useState(8); + + const { headerSize = 2 } = props; let hasHistory = true; // If no parent component exists, useHistory throws an error try { @@ -130,64 +167,101 @@ const SubMenu: React.FunctionComponent = props => { // If error is thrown, we know not to use 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 ( - - - {props.name} - - -
    + + +
    + {props.buttons?.map((btn, i) => ( - - ))} - - + ))} +
    + + {props.children} ); }; -export default SubMenu; +export default SubMenuComponent; diff --git a/superset-frontend/src/dashboard/stylesheets/dashboard.less b/superset-frontend/src/dashboard/stylesheets/dashboard.less index 432dd4b71b9..a3409c4d48b 100644 --- a/superset-frontend/src/dashboard/stylesheets/dashboard.less +++ b/superset-frontend/src/dashboard/stylesheets/dashboard.less @@ -18,7 +18,7 @@ */ /* header has mysterious extra margin */ header.top { - margin-bottom: -20px; + margin-bottom: 2px; } body { diff --git a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx index 55f7da5a4c3..f3dc3a873d2 100644 --- a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx +++ b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx @@ -344,7 +344,11 @@ function AnnotationLayersList({ return ( <> - + - + { 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: {}, diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index 7cea906aa7d..8df7f12489a 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -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) ? ( - {renderActivity()} - ) : ( - - )} - + {activityData[activeChild]?.length > 0 || + (activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? ( + {renderActivity()} + ) : ( + + )} ); } diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.test.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.test.tsx index 4146ab80f8f..078b14d0e21 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.test.tsx @@ -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'); diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.test.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.test.tsx index 8a57b0991f9..058e990887e 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.test.tsx @@ -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); diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index 83719c489e3..4f394c93ea9 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -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}; diff --git a/superset-frontend/stylesheets/superset.less b/superset-frontend/stylesheets/superset.less index ecfdd017f61..4c5cc224a35 100644 --- a/superset-frontend/stylesheets/superset.less +++ b/superset-frontend/stylesheets/superset.less @@ -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; +}