diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts index 619010eedaf..c4c5ed47665 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts @@ -56,15 +56,21 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => { cy.get('.ant-dropdown:not(.ant-dropdown-hidden)') .first() - .find("[role='menu'] [role='menuitem'] [title='Drill by']") + .should('be.visible') + .find("[role='menu'] [role='menuitem']") + .contains(/^Drill by$/) .trigger('mouseover', { force: true }); + cy.get( - '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-hidden) [data-test="drill-by-submenu"]', + '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]', ) + .should('be.visible') .find('[role="menuitem"]') - .contains(new RegExp(`^${targetDrillByColumn}$`)) - .first() - .click({ force: true }); + .then($el => { + cy.wrap($el) + .contains(new RegExp(`^${targetDrillByColumn}$`)) + .trigger('keydown', { keyCode: 13, which: 13, force: true }); + }); if (isLegacy) { return cy.wait('@legacyData'); diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts index 81ba3a37970..f11aac44544 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts @@ -34,8 +34,8 @@ function openModalFromMenu(chartType: string) { cy.get( `[data-test-viz-type='${chartType}'] [aria-label='More Options']`, ).click(); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') + cy.get('.antd5-dropdown') + .not('.antd5-dropdown-hidden') .find("[role='menu'] [role='menuitem']") .eq(5) .should('contain', 'Drill to detail') @@ -43,37 +43,46 @@ function openModalFromMenu(chartType: string) { cy.wait('@samples'); } -function openModalFromChartContext(targetMenuItem: string) { +function drillToDetail(targetMenuItem: string) { interceptSamples(); - if (targetMenuItem.startsWith('Drill to detail by')) { - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .should('be.visible') - .first() - .find("[role='menu'] [role='menuitem'] [title='Drill to detail by']") - .trigger('mouseover'); - cy.get('[data-test="drill-to-detail-by-submenu"]') - .should('be.visible') - .not('.ant-dropdown-menu-hidden [data-test="drill-to-detail-by-submenu"]') - .should('be.visible') - .find('[role="menuitem"]') - .contains(new RegExp(`^${targetMenuItem}$`)) - .first() - .click(); - } else { - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .first() - .find("[role='menu'] [role='menuitem']") - .contains(new RegExp(`^${targetMenuItem}$`)) - .first() - .click(); - } + cy.get('.antd5-dropdown') + .not('.antd5-dropdown-hidden') + .first() + .find("[role='menu'] [role='menuitem']") + .contains(new RegExp(`^${targetMenuItem}$`)) + .first() + .trigger('keydown', { keyCode: 13, which: 13, force: true }); + cy.getBySel('metadata-bar').should('be.visible'); cy.wait('@samples'); } +const drillToDetailBy = (targetDrill: string) => { + interceptSamples(); + + cy.get('.ant-dropdown:not(.ant-dropdown-hidden)') + .first() + .should('be.visible') + .find("[role='menu'] [role='menuitem']") + .contains(/^Drill to detail by$/) + .trigger('mouseover', { force: true }); + + cy.get( + '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]', + ) + .should('be.visible') + .find('[role="menuitem"]') + .then($el => { + cy.wrap($el) + .contains(new RegExp(`^${targetDrill}$`)) + .trigger('keydown', { keyCode: 13, which: 13, force: true }); + }); + + cy.getBySel('metadata-bar').should('be.visible'); + return cy.wait('@samples'); +}; + function closeModal() { cy.get('body').then($body => { if ($body.find('[data-test="close-drilltodetail-modal"]').length) { @@ -90,7 +99,7 @@ function testTimeChart(vizType: string) { cy.wrap($canvas).trigger('mousemove', 70, 93); cy.wrap($canvas).rightclick(70, 93); - openModalFromChartContext('Drill to detail by 1965'); + drillToDetailBy('Drill to detail by 1965'); cy.getBySel('filter-val').should('contain', '1965'); closeModal(); @@ -98,7 +107,7 @@ function testTimeChart(vizType: string) { cy.wrap($canvas).trigger('mousemove', 70, 93); cy.wrap($canvas).rightclick(70, 93); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); @@ -106,24 +115,10 @@ function testTimeChart(vizType: string) { cy.wrap($canvas).trigger('mousemove', 70, 93); cy.wrap($canvas).rightclick(70, 93); - openModalFromChartContext('Drill to detail by all'); + drillToDetailBy('Drill to detail by all'); cy.getBySel('filter-val').first().should('contain', '1965'); cy.getBySel('filter-val').eq(1).should('contain', 'boy'); closeModal(); - - cy.wrap($canvas).scrollIntoView(); - cy.wrap($canvas).trigger('mousemove', 70, 145); - cy.wrap($canvas).rightclick(70, 145); - openModalFromChartContext('Drill to detail by girl'); - cy.getBySel('filter-val').should('contain', 'girl'); - closeModal(); - - cy.wrap($canvas).scrollIntoView(); - cy.wrap($canvas).trigger('mousemove', 70, 145); - cy.wrap($canvas).rightclick(70, 145); - openModalFromChartContext('Drill to detail by all'); - cy.getBySel('filter-val').first().should('contain', '1965'); - cy.getBySel('filter-val').eq(1).should('contain', 'girl'); }); } @@ -208,7 +203,7 @@ describe('Drill to detail modal', () => { "[data-test-viz-type='big_number_total'] .header-line", ).rightclick(); - openModalFromChartContext('Drill to detail'); + drillToDetail('Drill to detail'); cy.getBySel('filter-val').should('not.exist'); }); @@ -224,7 +219,7 @@ describe('Drill to detail modal', () => { ).scrollIntoView(); cy.get("[data-test-viz-type='big_number'] .header-line").rightclick(); - openModalFromChartContext('Drill to detail'); + drillToDetail('Drill to detail'); cy.getBySel('filter-val').should('not.exist'); @@ -236,7 +231,7 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).trigger('mousemove', 1, 14); cy.wrap($canvas).rightclick(1, 14); - openModalFromChartContext('Drill to detail by 1965'); + drillToDetailBy('Drill to detail by 1965'); // checking the filter cy.getBySel('filter-val').should('contain', '1965'); @@ -255,7 +250,7 @@ describe('Drill to detail modal', () => { cy.get("[data-test-viz-type='table']").contains('boy').scrollIntoView(); cy.get("[data-test-viz-type='table']").contains('boy').rightclick(); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); @@ -267,7 +262,7 @@ describe('Drill to detail modal', () => { cy.get("[data-test-viz-type='table']").scrollIntoView(); cy.get("[data-test-viz-type='table']").contains('girl').rightclick(); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); @@ -283,7 +278,7 @@ describe('Drill to detail modal', () => { .first() .rightclick(); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); @@ -294,7 +289,7 @@ describe('Drill to detail modal', () => { .first() .rightclick(); - openModalFromChartContext('Drill to detail by CA'); + drillToDetailBy('Drill to detail by CA'); cy.getBySel('filter-val').should('contain', 'CA'); closeModal(); @@ -305,7 +300,7 @@ describe('Drill to detail modal', () => { .eq(3) .rightclick(); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); closeModal(); @@ -316,7 +311,7 @@ describe('Drill to detail modal', () => { .eq(3) .rightclick(); - openModalFromChartContext('Drill to detail by FL'); + drillToDetailBy('Drill to detail by FL'); cy.getBySel('filter-val').should('contain', 'FL'); closeModal(); @@ -327,7 +322,7 @@ describe('Drill to detail modal', () => { .eq(3) .rightclick(); - openModalFromChartContext('Drill to detail by all'); + drillToDetailBy('Drill to detail by all'); cy.getBySel('filter-val').first().should('contain', 'girl'); cy.getBySel('filter-val').eq(1).should('contain', 'FL'); @@ -349,21 +344,21 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(70, 100); - openModalFromChartContext('Drill to detail by 1965'); + drillToDetailBy('Drill to detail by 1965'); cy.getBySel('filter-val').should('contain', '1965'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(70, 100); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(70, 100); - openModalFromChartContext('Drill to detail by all'); + drillToDetailBy('Drill to detail by all'); cy.getBySel('filter-val').first().should('contain', '1965'); cy.getBySel('filter-val').eq(1).should('contain', 'boy'); closeModal(); @@ -371,7 +366,7 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(72, 200); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }, ); @@ -399,14 +394,14 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(130, 150); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(230, 190); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); }); }); @@ -419,14 +414,14 @@ describe('Drill to detail modal', () => { cy.get("[data-test-viz-type='world_map'] svg").then($canvas => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(70, 150); - openModalFromChartContext('Drill to detail by USA'); + drillToDetailBy('Drill to detail by USA'); cy.getBySel('filter-val').should('contain', 'USA'); closeModal(); }); cy.get("[data-test-viz-type='world_map'] svg").then($canvas => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(200, 140); - openModalFromChartContext('Drill to detail by SRB'); + drillToDetailBy('Drill to detail by SRB'); cy.getBySel('filter-val').should('contain', 'SRB'); }); }); @@ -456,7 +451,7 @@ describe('Drill to detail modal', () => { force: true, }); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); // checking the filter cy.getBySel('filter-val').should('contain', 'boy'); @@ -492,7 +487,7 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).trigger('mousemove', 135, 275); cy.wrap($canvas).rightclick(135, 275); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); @@ -500,7 +495,7 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).trigger('mousemove', 270, 280); cy.wrap($canvas).rightclick(270, 280); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); }); @@ -532,14 +527,14 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(170, 90); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(190, 250); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); }); @@ -553,14 +548,14 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(135, 95); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(95, 135); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); }); @@ -580,14 +575,14 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(180, 45); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(180, 85); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); }); @@ -601,14 +596,14 @@ describe('Drill to detail modal', () => { cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(100, 30); - openModalFromChartContext('Drill to detail by boy'); + drillToDetailBy('Drill to detail by boy'); cy.getBySel('filter-val').should('contain', 'boy'); closeModal(); cy.wrap($canvas).scrollIntoView(); cy.wrap($canvas).rightclick(150, 250); - openModalFromChartContext('Drill to detail by girl'); + drillToDetailBy('Drill to detail by girl'); cy.getBySel('filter-val').should('contain', 'girl'); }); }); diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts index 5d7dc5f9b93..aaa047f601b 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts @@ -219,12 +219,13 @@ function openExploreWithDashboardContext(chartName: string) { cy.get( `[data-test-chart-name='${chartName}'] [aria-label='More Options']`, ).click(); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .find("[role='menu'] [role='menuitem']") - .eq(2) - .should('contain', 'Edit chart') - .click(); + cy.get(`[data-test-edit-chart-name='${chartName}']`) + .should('be.visible') + .trigger('keydown', { + keyCode: 13, + which: 13, + force: true, + }); cy.wait('@v1Data'); cy.get('.chart-container').should('exist'); } diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts index 3cc1a2de666..f1bfa9617e1 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts @@ -57,16 +57,16 @@ function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') { .trigger('mouseover'); if (orientation === 'vertical') { - cy.get('.ant-dropdown-menu-item-selected') + cy.get('.antd5-menu-item-selected') .contains('Horizontal (Top)') .should('exist'); - cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click(); + cy.get('.antd5-menu-item').contains('Vertical (Left)').click(); cy.getBySel('dashboard-filters-panel').should('exist'); } else { - cy.get('.ant-dropdown-menu-item-selected') + cy.get('.antd5-menu-item-selected') .contains('Vertical (Left)') .should('exist'); - cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click(); + cy.get('.antd5-menu-item').contains('Horizontal (Top)').click(); cy.getBySel('loading-indicator').should('exist'); cy.getBySel('filter-bar').should('exist'); cy.getBySel('dashboard-filters-panel').should('not.exist'); diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts b/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts index b6c6a020f82..e768e5a8c9c 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts @@ -52,11 +52,10 @@ describe('Test explore links', () => { cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[aria-label="Menu actions trigger"]').click(); - cy.get('div[title="Share"]').trigger('mouseover'); - // need to use [id= syntax, otherwise error gets triggered because of special character in id - cy.get('[id="share_submenu$Menu"]').within(() => { - cy.contains('Embed code').parent().click(); + cy.get('div[role="menuitem"]').within(() => { + cy.contains('Share').parent().click(); }); + cy.getBySel('embed-code-button').click(); cy.get('#embed-code-popover').within(() => { cy.get('textarea[name=embedCode]').contains('iframe'); }); diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts index c4e90228dd9..3445cf5f262 100644 --- a/superset-frontend/cypress-base/cypress/support/directories.ts +++ b/superset-frontend/cypress-base/cypress/support/directories.ts @@ -25,16 +25,16 @@ export function dataTestChartName(chartName: string): string { export const pageHeader = { logo: '.navbar-brand > img', - headerNavigationItem: '.ant-menu-submenu-title', + headerNavigationItem: '.antd5-menu-submenu-title', headerNavigationDropdown: "[aria-label='triangle-down']", - headerNavigationItemMenu: '.ant-menu-item-group-list', - plusIcon: ':nth-child(2) > .ant-menu-submenu-title', + headerNavigationItemMenu: '.antd5-menu-item-group-list', + plusIcon: ':nth-child(2) > .antd5-menu-submenu-title', plusIconMenuOptions: { sqlQueryOption: dataTestLocator('menu-item-SQL query'), chartOption: dataTestLocator('menu-item-Chart'), dashboardOption: dataTestLocator('menu-item-Dashboard'), }, - plusMenu: '.ant-menu-submenu-popup', + plusMenu: '.antd5-menu-submenu-popup', barButtons: '[role="presentation"]', sqlLabMenu: '[id="item_3$Menu"]', dataMenu: '[id="item_4$Menu"]', diff --git a/superset-frontend/src/GlobalStyles.tsx b/superset-frontend/src/GlobalStyles.tsx index 19f2c2d10c0..c305fb214a0 100644 --- a/superset-frontend/src/GlobalStyles.tsx +++ b/superset-frontend/src/GlobalStyles.tsx @@ -99,6 +99,13 @@ export const GlobalStyles = () => ( margin-right: 0; } } + .ant-dropdown-menu-sub .antd5-menu.antd5-menu-vertical { + box-shadow: none; + } + .ant-dropdown-menu-submenu-title, + .ant-dropdown-menu-item { + line-height: 1.5em !important; + } `} /> ); diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index b3b3afbb81e..9b3752152ea 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -29,6 +29,7 @@ import ReactDOM from 'react-dom'; import { useDispatch, useSelector } from 'react-redux'; import { Behavior, + BinaryQueryObjectFilterClause, ContextMenuFilters, ensureIsArray, FeatureFlag, @@ -47,6 +48,7 @@ import { DrillDetailMenuItems } from '../DrillDetail'; import { getMenuAdjustedY } from '../utils'; import { MenuItemTooltip } from '../DisabledMenuItemTooltip'; import { DrillByMenuItems } from '../DrillBy/DrillByMenuItems'; +import DrillDetailModal from '../DrillDetail/DrillDetailModal'; export enum ContextMenuItem { CrossFilter, @@ -95,6 +97,12 @@ const ChartContextMenu = ( ); const [openKeys, setOpenKeys] = useState([]); + const [modalFilters, setFilters] = useState( + [], + ); + + const [visible, setVisible] = useState(false); + const isDisplayed = (item: ContextMenuItem) => displayedItems === ContextMenuItem.All || ensureIsArray(displayedItems).includes(item); @@ -216,14 +224,13 @@ const ChartContextMenu = ( if (showDrillToDetail) { menuItems.push( , @@ -279,37 +286,58 @@ const ChartContextMenu = ( ); return ReactDOM.createPortal( - { - setOpenKeys(openKeys); - }} - > - {menuItems.length ? ( - menuItems - ) : ( - No actions - )} - - } - trigger={['click']} - onVisibleChange={value => !value && onClose()} - > - + { + setVisible(false); + onClose(); + }} + > + {menuItems.length ? ( + menuItems + ) : ( + {t('No actions')} + )} + + } + trigger={['click']} + onVisibleChange={value => { + setVisible(value); + if (!value) { + setOpenKeys([]); + } }} - /> - , + visible={visible} + > + + + {showDrillToDetail && ( + { + setDrillModalIsOpen(false); + }} + /> + )} + , document.body, ); }; diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx index 2e60c08586f..c874da68540 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx @@ -70,7 +70,7 @@ const renderMenu = ({ ...rest }: Partial) => render( - + { within(drillByMenuItem).queryByTestId('tooltip-trigger'); expect(tooltipTrigger).not.toBeInTheDocument(); - userEvent.hover( - within(drillByMenuItem).getByRole('button', { name: 'Drill by' }), - ); - expect(await screen.findByTestId('drill-by-submenu')).toBeInTheDocument(); + userEvent.hover(within(drillByMenuItem).getByText('Drill by')); + const drillBySubmenus = await screen.findAllByTestId('drill-by-submenu'); + expect(drillBySubmenus[0]).toBeInTheDocument(); }; getChartMetadataRegistry().registerValue( @@ -176,7 +175,7 @@ test('render menu item with submenu and searchbox', async () => { expect(screen.getByText(column.column_name)).toBeInTheDocument(); }); - const searchbox = screen.getByRole('textbox'); + const searchbox = screen.getAllByPlaceholderText('Search columns')[1]; expect(searchbox).toBeInTheDocument(); userEvent.type(searchbox, 'col1'); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx index 0b06d95b55a..002ceb67adb 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx @@ -18,7 +18,7 @@ */ import { useState } from 'react'; import userEvent from '@testing-library/user-event'; -import { render, screen, within } from 'spec/helpers/testing-library'; +import { cleanup, render, screen, within } from 'spec/helpers/testing-library'; import setupPlugins from 'src/setup/setupPlugins'; import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore'; import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries'; @@ -27,6 +27,7 @@ import { Menu } from 'src/components/Menu'; import DrillDetailMenuItems, { DrillDetailMenuItemsProps, } from './DrillDetailMenuItems'; +import DrillDetailModal from './DrillDetailModal'; /* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */ @@ -74,20 +75,35 @@ const MockRenderChart = ({ formData, isContextMenu, filters, -}: Partial) => { +}: Partial & { chartId?: number }) => { const [showMenu, setShowMenu] = useState(false); + const [modalFilters, setFilters] = useState< + BinaryQueryObjectFilterClause[] | undefined + >(filters); + return ( - - + + + + + { + setShowMenu(false); + }} /> - + ); }; @@ -96,7 +112,7 @@ const renderMenu = ({ formData, isContextMenu, filters, -}: Partial) => { +}: Partial & { chartId?: number }) => { const store = getMockStoreWithNativeFilters(); return render( { + cleanup(); + renderMenu({ + chartId: defaultChartId, + formData: defaultFormData, + isContextMenu: true, + filters, + }); +}; + /** * Drill to Detail modal should appear with correct initial filters */ @@ -124,9 +150,8 @@ const expectDrillToDetailModal = async ( }); expect(modal).toBeInTheDocument(); - expect(screen.getByTestId('modal-filters')).toHaveTextContent( - JSON.stringify(filters), - ); + const modalFilters = await screen.findByTestId('modal-filters'); + expect(modalFilters).toHaveTextContent(JSON.stringify(filters)); }; /** @@ -204,13 +229,11 @@ const expectDrillToDetailByEnabled = async () => { }); await expectMenuItemEnabled(drillToDetailBy); - userEvent.hover( - within(drillToDetailBy).getByRole('button', { name: 'Drill to detail by' }), - ); + userEvent.hover(drillToDetailBy); - expect( - await screen.findByTestId('drill-to-detail-by-submenu'), - ).toBeInTheDocument(); + const submenus = await screen.findAllByTestId('drill-to-detail-by-submenu'); + + expect(submenus.length).toEqual(2); }; /** @@ -230,18 +253,17 @@ const expectDrillToDetailByDisabled = async (tooltipContent?: string) => { const expectDrillToDetailByDimension = async ( filter: BinaryQueryObjectFilterClause, ) => { - userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' })); - const drillToDetailBySubMenu = await screen.findByTestId( + userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' })); + const drillToDetailBySubMenus = await screen.findAllByTestId( 'drill-to-detail-by-submenu', ); const menuItemName = `Drill to detail by ${filter.formattedVal}`; - const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole( - 'menuitem', - { name: menuItemName }, - ); + const drillToDetailBySubmenuItems = await within( + drillToDetailBySubMenus[1], + ).findAllByRole('menuitem'); - await expectMenuItemEnabled(drillToDetailBySubmenuItem); + await expectMenuItemEnabled(drillToDetailBySubmenuItems[0]); await expectDrillToDetailModal(menuItemName, [filter]); }; @@ -251,16 +273,15 @@ const expectDrillToDetailByDimension = async ( const expectDrillToDetailByAll = async ( filters: BinaryQueryObjectFilterClause[], ) => { - userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' })); - const drillToDetailBySubMenu = await screen.findByTestId( + userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' })); + const drillToDetailBySubMenus = await screen.findAllByTestId( 'drill-to-detail-by-submenu', ); const menuItemName = 'Drill to detail by all'; - const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole( - 'menuitem', - { name: menuItemName }, - ); + const drillToDetailBySubmenuItem = await within( + drillToDetailBySubMenus[1], + ).findByRole('menuitem', { name: menuItemName }); await expectMenuItemEnabled(drillToDetailBySubmenuItem); await expectDrillToDetailModal(menuItemName, filters); @@ -353,22 +374,26 @@ test('context menu for supported chart, dimensions, 1 filter', async () => { filters, }); - await expectDrillToDetailEnabled(); await expectDrillToDetailByEnabled(); await expectDrillToDetailByDimension(filterA); }); -test('context menu for supported chart, dimensions, 2 filters', async () => { +test('context menu for supported chart, dimensions, filter A', async () => { const filters = [filterA, filterB]; - renderMenu({ - formData: defaultFormData, - isContextMenu: true, - filters, - }); - - await expectDrillToDetailEnabled(); + setupMenu(filters); await expectDrillToDetailByEnabled(); await expectDrillToDetailByDimension(filterA); +}); + +test('context menu for supported chart, dimensions, filter B', async () => { + const filters = [filterA, filterB]; + setupMenu(filters); + await expectDrillToDetailByEnabled(); await expectDrillToDetailByDimension(filterB); +}); + +test('context menu for supported chart, dimensions, all filters', async () => { + const filters = [filterA, filterB]; + setupMenu(filters); await expectDrillToDetailByAll(filters); }); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx index 45ea7de96f3..e20bc0290f7 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx @@ -17,7 +17,13 @@ * under the License. */ -import { ReactNode, RefObject, useCallback, useMemo, useState } from 'react'; +import { + Dispatch, + ReactNode, + SetStateAction, + useCallback, + useMemo, +} from 'react'; import { isEmpty } from 'lodash'; import { Behavior, @@ -33,7 +39,6 @@ import { import { useSelector } from 'react-redux'; import { Menu } from 'src/components/Menu'; import { RootState } from 'src/dashboard/types'; -import DrillDetailModal from './DrillDetailModal'; import { getSubmenuYOffset } from '../utils'; import { MenuItemTooltip } from '../DisabledMenuItemTooltip'; import { MenuItemWithTruncation } from '../MenuItemWithTruncation'; @@ -90,21 +95,20 @@ const StyledFilter = styled(Filter)` `; export type DrillDetailMenuItemsProps = { - chartId: number; formData: QueryFormData; filters?: BinaryQueryObjectFilterClause[]; + setFilters: Dispatch>; isContextMenu?: boolean; contextMenuY?: number; onSelection?: () => void; onClick?: (event: MouseEvent) => void; submenuIndex?: number; - showModal: boolean; setShowModal: (show: boolean) => void; - drillToDetailMenuRef?: RefObject; + key?: string; + forceSubmenuRender?: boolean; }; const DrillDetailMenuItems = ({ - chartId, formData, filters = [], isContextMenu = false, @@ -112,9 +116,9 @@ const DrillDetailMenuItems = ({ onSelection = () => null, onClick = () => null, submenuIndex = 0, - showModal, + setFilters, setShowModal, - drillToDetailMenuRef, + key, ...props }: DrillDetailMenuItemsProps) => { const drillToDetailDisabled = useSelector( @@ -122,10 +126,6 @@ const DrillDetailMenuItems = ({ datasources[formData.datasource]?.database?.disable_drill_to_detail, ); - const [modalFilters, setFilters] = useState( - [], - ); - const openModal = useCallback( (filters, event) => { onClick(event); @@ -136,10 +136,6 @@ const DrillDetailMenuItems = ({ [onClick, onSelection], ); - const closeModal = useCallback(() => { - setShowModal(false); - }, []); - // Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu` // event for dimensions. If it doesn't, tell the user that drill to detail by // dimension is not supported. If it does, and the `contextmenu` handler didn't @@ -196,7 +192,6 @@ const DrillDetailMenuItems = ({ {...props} key="drill-to-detail" onClick={openModal.bind(null, [])} - ref={drillToDetailMenuRef} > {DRILL_TO_DETAIL} @@ -213,6 +208,7 @@ const DrillDetailMenuItems = ({ popupOffset={[0, submenuYOffset]} popupClassName="chart-context-submenu" title={DRILL_TO_DETAIL_BY} + key={key} >
{filters.map((filter, i) => ( @@ -246,13 +242,6 @@ const DrillDetailMenuItems = ({ <> {drillToDetailMenuItem} {isContextMenu && drillToDetailByMenuItem} - ); }; diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx index 1b517154f07..fa8f22262cb 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx @@ -98,7 +98,7 @@ export default function DrillDetailModal({ const dashboardPageId = useContext(DashboardPageIdContext); const { slice_name: chartName } = useSelector( (state: { sliceEntities: { slices: Record } }) => - state.sliceEntities.slices[chartId], + state.sliceEntities?.slices?.[chartId] || {}, ); const canExplore = useSelector((state: RootState) => findPermission('can_explore', 'Superset', state.user?.roles), diff --git a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx index 1ab3daf4855..9e8802f1284 100644 --- a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx +++ b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx @@ -21,12 +21,12 @@ import { ReactNode, CSSProperties } from 'react'; import { css, truncationCSS, useCSSTextTruncation } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; import { Tooltip } from 'src/components/Tooltip'; -import type { MenuProps } from 'antd/lib/menu'; +import { MenuItemProps } from 'antd-v5'; export type MenuItemWithTruncationProps = { tooltipText: ReactNode; children: ReactNode; - onClick?: MenuProps['onClick']; + onClick?: MenuItemProps['onClick']; style?: CSSProperties; }; @@ -41,6 +41,7 @@ export const MenuItemWithTruncation = ({ diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx index dc56d73b326..ef81bd42ccd 100644 --- a/superset-frontend/src/components/Dropdown/index.tsx +++ b/superset-frontend/src/components/Dropdown/index.tsx @@ -17,7 +17,6 @@ * under the License. */ import { - RefObject, ReactElement, ReactNode, FocusEvent, @@ -26,6 +25,11 @@ import { } from 'react'; import { AntdDropdown } from 'src/components'; +// TODO: @geido - Remove these after dropdown is fully migrated to Antd v5 +import { + Dropdown as Antd5Dropdown, + DropDownProps as Antd5DropdownProps, +} from 'antd-v5'; import { DropDownProps } from 'antd/lib/dropdown'; import { styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -108,11 +112,7 @@ export const Dropdown = ({ ); -interface ExtendedDropDownProps extends DropDownProps { - ref?: RefObject; -} - -export interface NoAnimationDropdownProps extends ExtendedDropDownProps { +export interface NoAnimationDropdownProps extends Antd5DropdownProps { children: ReactNode; onBlur?: (e: FocusEvent) => void; onKeyDown?: (e: KeyboardEvent) => void; @@ -126,8 +126,8 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => { }); return ( - + {childrenWithProps} - + ); }; diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index 12d23dc2423..8d791929d37 100644 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { styled, useTheme } from '@superset-ui/core'; -import { FC, RefObject, useMemo, ReactNode } from 'react'; +import { addAlpha, styled, useTheme } from '@superset-ui/core'; +import { FC, RefObject, useMemo, ReactNode, useState } from 'react'; import Icons from 'src/components/Icons'; import { DropdownButton } from 'src/components/DropdownButton'; import { DropdownButtonProps } from 'antd/lib/dropdown'; @@ -63,6 +63,12 @@ const StyledDropdownButton = styled(DropdownButton as FC)` const StyledMenu = styled(Menu)` ${({ theme }) => ` + box-shadow: + 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)}, + 0 6px 16px 0 + ${addAlpha(theme.colors.grayscale.dark2, 0.08)}, + 0 9px 28px 8px + ${addAlpha(theme.colors.grayscale.dark2, 0.05)}; .info { font-size: ${theme.typography.sizes.s}px; color: ${theme.colors.grayscale.base}; @@ -98,7 +104,17 @@ const StyleSubmenuItem = styled.div` export default (props: DropDownSelectableProps) => { const theme = useTheme(); + const [visible, setVisible] = useState(false); const { icon, info, menuItems, selectedKeys, onSelect } = props; + + const handleVisibleChange = setVisible; + + const handleMenuSelect: MenuProps['onSelect'] = info => { + if (onSelect) { + onSelect(info); + } + setVisible(false); + }; const menuItem = useMemo( () => (label: string | ReactNode, key: string, divider?: boolean) => ( @@ -119,28 +135,34 @@ export default (props: DropDownSelectableProps) => { const overlayMenu = useMemo( () => ( - + <> {info && (
{info}
)} - {menuItems.map(m => - m.children?.length ? ( - - {m.children.map(s => menuItem(s.label, s.key))} - - ) : ( - menuItem(m.label, m.key, m.divider) - ), - )} -
+ + {menuItems.map(m => + m.children?.length ? ( + + {m.children.map(s => menuItem(s.label, s.key))} + + ) : ( + menuItem(m.label, m.key, m.divider) + ), + )} + + ), - [selectedKeys, onSelect, info, menuItems, menuItem], + [selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect], ); return ( @@ -148,6 +170,8 @@ export default (props: DropDownSelectableProps) => { overlay={overlayMenu} trigger={['click']} icon={icon} + visible={visible} + onVisibleChange={handleVisibleChange} /> ); }; diff --git a/superset-frontend/src/components/Menu/Menu.stories.tsx b/superset-frontend/src/components/Menu/Menu.stories.tsx new file mode 100644 index 00000000000..1ba01091672 --- /dev/null +++ b/superset-frontend/src/components/Menu/Menu.stories.tsx @@ -0,0 +1,63 @@ +/** + * 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 { Menu, MainNav } from '.'; + +export default { + title: 'Menu', + component: Menu as React.FC, +}; + +export const MainNavigation = (args: any) => ( + + + Dashboards + + + Charts + + + Datasets + + +); + +export const InteractiveMenu = (args: any) => ( + + Dashboards + Charts + Datasets + +); + +InteractiveMenu.args = { + defaultSelectedKeys: ['1'], + inlineCollapsed: false, + mode: 'horizontal', + multiple: false, + selectable: true, +}; + +InteractiveMenu.argTypes = { + mode: { + control: { + type: 'select', + }, + options: ['horizontal', 'vertical', 'inline'], + }, +}; diff --git a/superset-frontend/src/components/Menu/index.tsx b/superset-frontend/src/components/Menu/index.tsx index 35251834ba0..ff2c1c0b6ec 100644 --- a/superset-frontend/src/components/Menu/index.tsx +++ b/superset-frontend/src/components/Menu/index.tsx @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { styled } from '@superset-ui/core'; +import { addAlpha, styled } from '@superset-ui/core'; import { ReactElement } from 'react'; -import { Menu as AntdMenu } from 'antd'; -import { MenuProps as AntdMenuProps } from 'antd/lib/menu'; +import { Menu as AntdMenu } from 'antd-v5'; +import { MenuProps as AntdMenuProps } from 'antd-v5/es/menu'; export type MenuProps = AntdMenuProps; @@ -29,7 +29,9 @@ export enum MenuItemKeyEnum { SubMenuItem = 'submenu-item', } -export type AntdMenuTypeRef = { current: { props: { parentMenu: AntdMenu } } }; +export type AntdMenuTypeRef = { + current: { props: { parentMenu: typeof AntdMenu } }; +}; export type AntdMenuItemType = ReactElement & { ref: AntdMenuTypeRef; @@ -38,35 +40,22 @@ export type AntdMenuItemType = ReactElement & { export type MenuItemChildType = AntdMenuItemType; -export const isAntdMenuItemRef = ( - ref: AntdMenuTypeRef, -): ref is AntdMenuTypeRef => - (ref as AntdMenuTypeRef)?.current?.props?.parentMenu !== undefined; - -export const isAntdMenuItem = (child: MenuItemChildType) => - child?.type?.displayName === 'Styled(MenuItem)'; - -export const isAntdMenuSubmenu = (child: MenuItemChildType) => - child?.type?.isSubMenu === 1; - -export const isSubMenuOrItemType = (type: string) => - type === MenuItemKeyEnum.SubMenu || type === MenuItemKeyEnum.SubMenuItem; - -const MenuItem = styled(AntdMenu.Item)` - > a { +const StyledMenuItem = styled(AntdMenu.Item)` + a { text-decoration: none; } - - &.ant-menu-item { - height: ${({ theme }) => theme.gridUnit * 8}px; - line-height: ${({ theme }) => theme.gridUnit * 8}px; + &.antd5-menu-item { + div { + display: flex; + align-items: center; + justify-content: space-between; + } a { - border-bottom: none; transition: background-color ${({ theme }) => theme.transitionTiming}s; &:after { content: ''; position: absolute; - bottom: -3px; + bottom: -2px; left: 50%; width: 0; height: 3px; @@ -76,74 +65,74 @@ const MenuItem = styled(AntdMenu.Item)` 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, - &.ant-dropdown-menu-item { - span[role='button'] { - display: inline-block; - width: 100%; +// TODO: @geido - Move this to theme after fully migrating dropdown to Antd5 +const StyledMenu = styled(AntdMenu)` + ${({ theme }) => ` + &.antd5-menu-horizontal { + background-color: inherit; + border-bottom: 1px solid transparent; } - transition-duration: 0s; - } + &.antd5-menu-vertical, + &.ant-dropdown-menu { + box-shadow: + 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)}, + 0 6px 16px 0 + ${addAlpha(theme.colors.grayscale.dark2, 0.08)}, + 0 9px 28px 8px + ${addAlpha(theme.colors.grayscale.dark2, 0.05)}; + } + `} `; const StyledNav = styled(AntdMenu)` - line-height: 51px; - border: none; - - & > .ant-menu-item, - & > .ant-menu-submenu { - vertical-align: inherit; + display: flex; + align-items: center; + height: 100%; + gap: 0; + &.antd5-menu-horizontal > .antd5-menu-item { + height: 100%; + display: flex; + align-items: center; + margin: 0; + border-bottom: 2px solid transparent; + padding: ${({ theme }) => theme.gridUnit * 2}px + ${({ theme }) => theme.gridUnit * 4}px; &:hover { - color: ${({ theme }) => theme.colors.grayscale.dark1}; + background-color: ${({ theme }) => theme.colors.primary.light5}; + border-bottom: 2px solid transparent; + & a:after { + opacity: 1; + width: 100%; + } } } - - &:not(.ant-menu-dark) > .ant-menu-submenu, - &:not(.ant-menu-dark) > .ant-menu-item { - &:hover { - border-bottom: none; - } - } - - &:not(.ant-menu-dark) > .ant-menu-submenu, - &:not(.ant-menu-dark) > .ant-menu-item { - margin: 0px; - } - - & > .ant-menu-item > a { - padding: ${({ theme }) => theme.gridUnit * 4}px; + &.antd5-menu-horizontal > .antd5-menu-item-selected { + box-sizing: border-box; + border-bottom: 2px solid ${({ theme }) => theme.colors.primary.base}; } `; const StyledSubMenu = styled(AntdMenu.SubMenu)` - color: ${({ theme }) => theme.colors.grayscale.dark1}; - border-bottom: none; - .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; + .antd5-menu-submenu-open, + .antd5-menu-submenu-active { + .antd5-menu-submenu-title { &:after { opacity: 1; width: calc(100% - 1); } } } - .ant-menu-submenu-title { - position: relative; - top: ${({ theme }) => -theme.gridUnit - 3}px; + .antd5-menu-submenu-title { + display: flex; + flex-direction: row-reverse; &:after { content: ''; position: absolute; @@ -154,47 +143,27 @@ const StyledSubMenu = styled(AntdMenu.SubMenu)` opacity: 0; transform: translateX(-50%); transition: all ${({ theme }) => theme.transitionTiming}s; - background-color: ${({ theme }) => theme.colors.primary.base}; } } - .ant-menu-submenu-arrow { - top: 67%; - } - & > .ant-menu-submenu-title { - padding: 0 ${({ theme }) => theme.gridUnit * 6}px 0 - ${({ theme }) => theme.gridUnit * 3}px !important; - span[role='img'] { - position: absolute; - right: ${({ theme }) => -theme.gridUnit + -2}px; - top: ${({ theme }) => theme.gridUnit * 5.25}px; - svg { - font-size: ${({ theme }) => theme.gridUnit * 6}px; - color: ${({ theme }) => theme.colors.grayscale.base}; - } - } - & > span { - position: relative; - top: 7px; - } - &:hover { - color: ${({ theme }) => theme.colors.primary.base}; - } + + .ant-dropdown-menu-submenu-arrow:before, + .ant-dropdown-menu-submenu-arrow:after { + content: none !important; } `; -export declare type MenuMode = - | 'vertical' - | 'vertical-left' - | 'vertical-right' - | 'horizontal' - | 'inline'; +export type MenuMode = AntdMenuProps['mode']; +export type MenuItem = Required['items'][number]; -export const Menu = Object.assign(AntdMenu, { - Item: MenuItem, -}); - -export const MainNav = Object.assign(StyledNav, { - Item: MenuItem, +export const Menu = Object.assign(StyledMenu, { + Item: StyledMenuItem, + SubMenu: StyledSubMenu, + Divider: AntdMenu.Divider, + ItemGroup: AntdMenu.ItemGroup, +}); + +export const MainNav = Object.assign(StyledNav, { + Item: StyledMenuItem, SubMenu: StyledSubMenu, Divider: AntdMenu.Divider, ItemGroup: AntdMenu.ItemGroup, diff --git a/superset-frontend/src/components/ModalTrigger/index.tsx b/superset-frontend/src/components/ModalTrigger/index.tsx index 556291c4db2..58c19347e62 100644 --- a/superset-frontend/src/components/ModalTrigger/index.tsx +++ b/superset-frontend/src/components/ModalTrigger/index.tsx @@ -102,9 +102,9 @@ const ModalTrigger = forwardRef( )} {!isButton && ( - +
{triggerNode} - +
)} { test('should render the Download dropdown button when not in edit mode', () => { const mockedProps = createProps(); setup(mockedProps); - expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument(); + expect( + screen.getByRole('menuitem', { name: 'Download' }), + ).toBeInTheDocument(); }); test('should render the menu items', async () => { @@ -194,7 +196,7 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', isLoading: true, }; setup(loadingProps); - expect(screen.getByText('Refresh dashboard')).toHaveClass( + expect(screen.getByText('Refresh dashboard').parentElement).toHaveClass( 'ant-menu-item-disabled', ); }); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx index d5ae7aaee2a..6bc712f6b84 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx @@ -195,7 +195,7 @@ export class HeaderActionsDropdown extends PureComponent< {editMode && ( {t('Edit CSS')}
} + triggerNode={
{t('Edit CSS')}
} initialCss={this.state.css} onChange={this.changeCss} addDangerToast={addDangerToast} @@ -222,7 +222,7 @@ export class HeaderActionsDropdown extends PureComponent< colorScheme={colorScheme} onSave={onSave} triggerNode={ - {t('Save as')} +
{t('Save as')}
} canOverwrite={userCanEdit} /> @@ -242,24 +242,20 @@ export class HeaderActionsDropdown extends PureComponent< /> {userCanShare && ( - - - + url={url} + copyMenuItemTitle={t('Copy permalink to clipboard')} + emailMenuItemTitle={t('Share permalink by email')} + emailSubject={emailSubject} + emailBody={emailBody} + addSuccessToast={addSuccessToast} + addDangerToast={addDangerToast} + dashboardId={dashboardId} + dashboardComponentId={dashboardComponentId} + /> )} {!editMode && userCanCurate && ( {t('Set filter mapping')} +
{t('Set filter mapping')}
} />
@@ -316,7 +312,7 @@ export class HeaderActionsDropdown extends PureComponent< onChange={this.changeRefreshInterval} editMode={editMode} refreshIntervalOptions={refreshIntervalOptions} - triggerNode={{t('Set auto-refresh interval')}} + triggerNode={
{t('Set auto-refresh interval')}
} />
diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx index c8b4e00a187..a683dfa2b29 100644 --- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx +++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx @@ -23,6 +23,9 @@ import fetchMock from 'fetch-mock'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; import { HeaderActionsDropdown } from 'src/dashboard/components/Header/HeaderActionsDropdown'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; const createProps = () => ({ addSuccessToast: jest.fn(), @@ -81,10 +84,15 @@ const editModeOnProps = { editMode: true, }; +const mockStore = configureStore([thunk]); +const store = mockStore({}); + const setup = (overrides?: any) => ( -
- -
+ +
+ +
+
); fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index edeb1b54172..e1f20b5505e 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -17,28 +17,13 @@ * under the License. */ -import { KeyboardEvent, ReactElement } from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from 'spec/helpers/testing-library'; import { FeatureFlag, VizType } from '@superset-ui/core'; import mockState from 'spec/fixtures/mockState'; -import { Menu } from 'src/components/Menu'; -import SliceHeaderControls from '.'; -import { SliceHeaderControlsProps } from './types'; -import { handleDropdownNavigation } from './utils'; +import SliceHeaderControls, { SliceHeaderControlsProps } from '.'; -jest.mock('src/components/Dropdown', () => { - const original = jest.requireActual('src/components/Dropdown'); - return { - ...original, - NoAnimationDropdown: (props: any) => ( -
- {props.overlay} - {props.children} -
- ), - }; -}); +const SLICE_ID = 371; const createProps = (viz_type = VizType.Sunburst) => ({ @@ -54,7 +39,7 @@ const createProps = (viz_type = VizType.Sunburst) => toggleExpandSlice: jest.fn(), logEvent: jest.fn(), slice: { - slice_id: 371, + slice_id: SLICE_ID, slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%20371%7D', slice_name: 'Vaccine Candidates per Country & Stage', slice_description: 'Table of vaccine candidates for 100 countries', @@ -73,7 +58,7 @@ const createProps = (viz_type = VizType.Sunburst) => secondary_metric: 'metrics', }, row_limit: 10000, - slice_id: 371, + slice_id: SLICE_ID, time_range: 'No filter', url_params: {}, viz_type, @@ -104,6 +89,7 @@ const createProps = (viz_type = VizType.Sunburst) => viz_type: VizType.Sunburst, }, exploreUrl: '/explore', + defaultOpen: true, }) as SliceHeaderControlsProps; const renderWrapper = ( @@ -131,7 +117,7 @@ test('Should render', () => { expect( screen.getByRole('button', { name: 'More Options' }), ).toBeInTheDocument(); - expect(screen.getByTestId('NoAnimationDropdown')).toBeInTheDocument(); + expect(screen.getByTestId(`slice_${SLICE_ID}-menu`)).toBeInTheDocument(); }); test('Should render default props', () => { @@ -157,27 +143,17 @@ test('Should render default props', () => { delete props.isExpanded; renderWrapper(props); - expect( - screen.getByRole('menuitem', { name: 'Enter fullscreen' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: /Force refresh/ }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Show chart description' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Edit chart' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Download' }), - ).toBeInTheDocument(); - expect(screen.getByRole('menuitem', { name: 'Share' })).toBeInTheDocument(); + expect(screen.getByText('Enter fullscreen')).toBeInTheDocument(); + expect(screen.getByText('Force refresh')).toBeInTheDocument(); + expect(screen.getByText('Show chart description')).toBeInTheDocument(); + expect(screen.getByText('Edit chart')).toBeInTheDocument(); + expect(screen.getByText('Download')).toBeInTheDocument(); + expect(screen.getByText('Share')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'More Options' }), ).toBeInTheDocument(); - expect(screen.getByTestId('NoAnimationDropdown')).toBeInTheDocument(); + expect(screen.getByTestId(`slice_${SLICE_ID}-menu`)).toBeInTheDocument(); }); test('Should "export to CSV"', async () => { @@ -449,168 +425,3 @@ test('Should not show the "Edit chart" button', () => { }); expect(screen.queryByText('Edit chart')).not.toBeInTheDocument(); }); - -describe('handleDropdownNavigation', () => { - const mockToggleDropdown = jest.fn(); - const mockSetSelectedKeys = jest.fn(); - const mockSetOpenKeys = jest.fn(); - - const menu = ( - - Item 1 - Item 2 - Item 3 - - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('should continue with system tab navigation if dropdown is closed and tab key is pressed', () => { - const event = { - key: 'Tab', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - false, -
, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockToggleDropdown).not.toHaveBeenCalled(); - expect(mockSetSelectedKeys).not.toHaveBeenCalled(); - }); - - test(`should prevent default behavior and toggle dropdown if dropdown - is closed and action key is pressed`, () => { - const event = { - key: 'Enter', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - false, -
, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockToggleDropdown).toHaveBeenCalled(); - expect(mockSetSelectedKeys).not.toHaveBeenCalled(); - }); - - test(`should trigger menu item click, - clear selected keys, close dropdown, and focus on menu trigger - if action key is pressed and menu item is selected`, () => { - const event = { - key: 'Enter', - preventDefault: jest.fn(), - currentTarget: { focus: jest.fn() }, - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - true, - menu, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockToggleDropdown).toHaveBeenCalled(); - expect(mockSetSelectedKeys).toHaveBeenCalledWith([]); - expect(event.currentTarget.focus).toHaveBeenCalled(); - }); - - test('should select the next menu item if down arrow key is pressed', () => { - const event = { - key: 'ArrowDown', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - true, - menu, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockSetSelectedKeys).toHaveBeenCalledWith(['item2']); - }); - - test('should select the previous menu item if up arrow key is pressed', () => { - const event = { - key: 'ArrowUp', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - true, - menu, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockSetSelectedKeys).toHaveBeenCalledWith(['item1']); - }); - - test('should close dropdown menu if escape key is pressed', () => { - const event = { - key: 'Escape', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - true, -
, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockToggleDropdown).toHaveBeenCalled(); - expect(mockSetSelectedKeys).not.toHaveBeenCalled(); - }); - - test('should do nothing if an unsupported key is pressed', () => { - const event = { - key: 'Shift', - preventDefault: jest.fn(), - } as unknown as KeyboardEvent; - - handleDropdownNavigation( - event, - true, -
, - mockToggleDropdown, - mockSetSelectedKeys, - mockSetOpenKeys, - ); - expect(mockToggleDropdown).not.toHaveBeenCalled(); - expect(mockSetSelectedKeys).not.toHaveBeenCalled(); - }); - - test('should find a child element with a key', () => { - const item = { - props: { - children: [ -
Child 1
, -
Child 2
, -
Child 3
, - ], - }, - }; - - const childWithKey = item?.props?.children?.find( - (child: ReactElement) => child?.key, - ); - - expect(childWithKey).toBeDefined(); - }); -}); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/ViewResultsModalTrigger.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/ViewResultsModalTrigger.tsx index 4d6b9e74e52..b27d3be2d7d 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/ViewResultsModalTrigger.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/ViewResultsModalTrigger.tsx @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { ReactChild, useCallback } from 'react'; +import { ReactChild, RefObject, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { css, t, useTheme } from '@superset-ui/core'; -import Modal from 'src/components/Modal'; import Button from 'src/components/Button'; +import ModalTrigger from 'src/components/ModalTrigger'; export const ViewResultsModalTrigger = ({ canExplore, @@ -28,90 +28,66 @@ export const ViewResultsModalTrigger = ({ triggerNode, modalTitle, modalBody, - showModal = false, - setShowModal, + modalRef, }: { canExplore?: boolean; exploreUrl: string; triggerNode: ReactChild; - modalTitle: ReactChild; + modalTitle: string; modalBody: ReactChild; - showModal: boolean; - setShowModal: (showModal: boolean) => void; + modalRef?: RefObject; }) => { const history = useHistory(); const exploreChart = () => history.push(exploreUrl); const theme = useTheme(); - const openModal = useCallback(() => setShowModal(true), [setShowModal]); - const closeModal = useCallback(() => setShowModal(false), [setShowModal]); - + const handleCloseModal = useCallback(() => { + modalRef?.current?.close(); + }, [modalRef]); return ( - <> - - {triggerNode} - - {(() => ( - + - - - } - responsive - resizable - resizableConfig={{ - minHeight: theme.gridUnit * 128, - minWidth: theme.gridUnit * 128, - defaultSize: { - width: 'auto', - height: '75vh', - }, - }} - draggable - destroyOnClose - > - {modalBody} - - ))()} - + > + {t('Edit chart')} + + + + } + /> ); }; diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index af8cbd739b7..25750dcca11 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -16,9 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { MouseEvent, Key, useState, useRef, RefObject } from 'react'; +import { + MouseEvent, + Key, + KeyboardEvent, + useState, + useRef, + RefObject, +} from 'react'; -import { useHistory } from 'react-router-dom'; +import { RouteComponentProps, useHistory } from 'react-router-dom'; import { extendedDayjs } from 'src/utils/dates'; import { Behavior, @@ -29,6 +36,8 @@ import { styled, t, VizType, + BinaryQueryObjectFilterClause, + QueryFormData, } from '@superset-ui/core'; import { useSelector } from 'react-redux'; import { Menu } from 'src/components/Menu'; @@ -44,11 +53,10 @@ import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane'; import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail'; import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; +import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal'; import { usePermissions } from 'src/hooks/usePermissions'; import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal'; -import { handleDropdownNavigation } from './utils'; import { ViewResultsModalTrigger } from './ViewResultsModalTrigger'; -import { SliceHeaderControlsProps } from './types'; // TODO: replace 3 dots with an icon const VerticalDotsContainer = styled.div` @@ -93,6 +101,52 @@ const VerticalDotsTrigger = () => ( ); +export interface SliceHeaderControlsProps { + slice: { + description: string; + viz_type: string; + slice_name: string; + slice_id: number; + slice_description: string; + datasource: string; + }; + + defaultOpen?: boolean; + componentId: string; + dashboardId: number; + chartStatus: string; + isCached: boolean[]; + cachedDttm: string[] | null; + isExpanded?: boolean; + updatedDttm: number | null; + isFullSize?: boolean; + isDescriptionExpanded?: boolean; + formData: QueryFormData; + exploreUrl: string; + + forceRefresh: (sliceId: number, dashboardId: number) => void; + logExploreChart?: (sliceId: number) => void; + logEvent?: (eventName: string, eventData?: object) => void; + toggleExpandSlice?: (sliceId: number) => void; + exportCSV?: (sliceId: number) => void; + exportPivotCSV?: (sliceId: number) => void; + exportFullCSV?: (sliceId: number) => void; + exportXLSX?: (sliceId: number) => void; + exportFullXLSX?: (sliceId: number) => void; + handleToggleFullSize: () => void; + + addDangerToast: (message: string) => void; + addSuccessToast: (message: string) => void; + + supersetCanExplore?: boolean; + supersetCanShare?: boolean; + supersetCanCSV?: boolean; + + crossFiltersEnabled?: boolean; +} +type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps & + RouteComponentProps; + const dropdownIconsStyles = css` &&.anticon > .anticon:first-child { margin-right: 0; @@ -100,9 +154,9 @@ const dropdownIconsStyles = css` } `; -const SliceHeaderControls = (props: SliceHeaderControlsProps) => { - const [dropdownIsOpen, setDropdownIsOpen] = useState(false); - const [tableModalIsOpen, setTableModalIsOpen] = useState(false); +const SliceHeaderControls = ( + props: SliceHeaderControlsPropsWithRouter | SliceHeaderControlsProps, +) => { const [drillModalIsOpen, setDrillModalIsOpen] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); // setting openKeys undefined falls back to uncontrolled behaviour @@ -113,18 +167,11 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { const history = useHistory(); const queryMenuRef: RefObject = useRef(null); - const menuRef: RefObject = useRef(null); - const copyLinkMenuRef: RefObject = useRef(null); - const shareByEmailMenuRef: RefObject = useRef(null); - const drillToDetailMenuRef: RefObject = useRef(null); + const resultsMenuRef: RefObject = useRef(null); - const toggleDropdown = ({ close }: { close?: boolean } = {}) => { - setDropdownIsOpen(!(close || dropdownIsOpen)); - // clear selected keys - setSelectedKeys([]); - // clear out/deselect submenus - // setOpenKeys([]); - }; + const [modalFilters, setFilters] = useState( + [], + ); const canEditCrossFilters = useSelector( @@ -147,10 +194,8 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { domEvent, }: { key: Key; - domEvent: MouseEvent; + domEvent: MouseEvent | KeyboardEvent; }) => { - // close menu - toggleDropdown({ close: true }); switch (key) { case MenuKeys.ForceRefresh: refreshChart(); @@ -222,8 +267,8 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { break; } case MenuKeys.ViewResults: { - if (!tableModalIsOpen) { - setTableModalIsOpen(true); + if (resultsMenuRef.current && !resultsMenuRef.current.showModal) { + resultsMenuRef.current.open(domEvent); } break; } @@ -302,8 +347,9 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { selectable={false} data-test={`slice_${slice.slice_id}-menu`} selectedKeys={selectedKeys} + onSelect={({ selectedKeys: keys }) => setSelectedKeys(keys)} + openKeys={openKeys} id={`slice_${slice.slice_id}-menu`} - ref={menuRef} // submenus must be rendered for handleDropdownNavigation forceSubMenuRender {...openKeysProps} @@ -333,7 +379,10 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { )} {canExplore && ( - + {t('Edit chart')} @@ -352,7 +401,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { {t('View query')} +
{t('View query')}
} modalTitle={t('View query')} modalBody={} @@ -370,10 +419,9 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { canExplore={props.supersetCanExplore} exploreUrl={props.exploreUrl} triggerNode={ - {t('View as table')} +
{t('View as table')}
} - setShowModal={setTableModalIsOpen} - showModal={tableModalIsOpen} + modalRef={resultsMenuRef} modalTitle={t('Chart Data: %s', slice.slice_name)} modalBody={ { {isFeatureEnabled(FeatureFlag.DrillToDetail) && canDrillToDetail && ( )} {(slice.description || canExplore) && } {supersetCanShare && ( - setOpenKeys(undefined)} - > - key === MenuKeys.CopyLink || key === MenuKeys.ShareByEmail, - )} - /> - + /> )} {props.supersetCanCSV && ( - setOpenKeys(undefined)} - > + } @@ -495,22 +529,12 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { /> )} menu} overlayStyle={dropdownOverlayStyle} trigger={['click']} placement="bottomRight" - visible={dropdownIsOpen} - onVisibleChange={status => toggleDropdown({ close: !status })} - onKeyDown={e => - handleDropdownNavigation( - e, - dropdownIsOpen, - menu, - toggleDropdown, - setSelectedKeys, - setOpenKeys, - ) - } + autoFocus + forceRender > css` @@ -526,6 +550,16 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { + { + setDrillModalIsOpen(false); + }} + chartId={slice.slice_id} + showModal={drillModalIsOpen} + /> + {canEditCrossFilters && scopingModal} ); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/utils.ts b/superset-frontend/src/dashboard/components/SliceHeaderControls/utils.ts deleted file mode 100644 index ab2d45076bc..00000000000 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/utils.ts +++ /dev/null @@ -1,293 +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 { - isAntdMenuItem, - isAntdMenuItemRef, - isAntdMenuSubmenu, - isSubMenuOrItemType, - MenuItemChildType, - MenuItemKeyEnum, -} from 'src/components/Menu'; -import { KeyboardEvent, ReactElement, RefObject } from 'react'; -import { ensureIsArray } from '@superset-ui/core'; - -const ACTION_KEYS = { - enter: 'Enter', - spacebar: 'Spacebar', - space: ' ', -}; - -const NAV_KEYS = { - tab: 'Tab', - escape: 'Escape', - up: 'ArrowUp', - down: 'ArrowDown', -}; - -/** - * A MenuItem can be recognized in the tree by the presence of a ref - * - * @param children - * @param currentKeys - * @returns an array of keys - */ -const extractMenuItemRefs = (child: MenuItemChildType): RefObject[] => { - // check that child has props - const childProps: Record = child?.props; - // loop through each prop - if (childProps) { - const arrayProps = Object.values(childProps); - // check if any is of type ref MenuItem - return arrayProps.filter(ref => isAntdMenuItemRef(ref)); - } - return []; -}; - -/** - * Recursively extracts keys from menu items - * - * @param children - * @param currentKeys - * @returns an array of keys and their refs - * - */ -const extractMenuItemsKeys = ( - children: MenuItemChildType[], - currentKeys?: { key: string; ref?: RefObject }[], -): { key: string; ref?: RefObject }[] => { - const allKeys = currentKeys || []; - const arrayChildren = ensureIsArray(children); - - arrayChildren.forEach((child: MenuItemChildType) => { - const isMenuItem = isAntdMenuItem(child); - const refs = extractMenuItemRefs(child); - // key is immediately available in a standard MenuItem - if (isMenuItem) { - const { key } = child; - if (key) { - allKeys.push({ - key, - }); - } - } - // one or more menu items refs are available - if (refs.length) { - allKeys.push( - ...refs.map(ref => ({ key: ref.current.props.eventKey, ref })), - ); - } - - // continue to extract keys from nested children - if (child?.props?.children) { - const childKeys = extractMenuItemsKeys(child.props.children, allKeys); - allKeys.push(...childKeys); - } - }); - - return allKeys; -}; - -/** - * Generates a map of keys and their types for a MenuItem - * Individual refs can be given to extract keys from nested items - * Refs can be used to control the event handlers of the menu items - * - * @param itemChildren - * @param type - * @returns a map of keys and their types - */ -const extractMenuItemsKeyMap = ( - children: MenuItemChildType, -): Record => { - const keysMap: Record = {}; - const childrenArray = ensureIsArray(children); - - childrenArray.forEach((child: MenuItemChildType) => { - const isMenuItem = isAntdMenuItem(child); - const isSubmenu = isAntdMenuSubmenu(child); - const menuItemsRefs = extractMenuItemRefs(child); - - // key is immediately available in MenuItem or SubMenu - if (isMenuItem || isSubmenu) { - const directKey = child?.key; - if (directKey) { - keysMap[directKey] = {}; - keysMap[directKey].type = isSubmenu - ? MenuItemKeyEnum.SubMenu - : MenuItemKeyEnum.MenuItem; - } - } - - // one or more menu items refs are available - if (menuItemsRefs.length) { - menuItemsRefs.forEach(ref => { - const key = ref.current.props.eventKey; - keysMap[key] = {}; - keysMap[key].type = isSubmenu - ? MenuItemKeyEnum.SubMenu - : MenuItemKeyEnum.MenuItem; - keysMap[key].parent = child.key; - keysMap[key].ref = ref; - }); - } - - // if it has children must check for the presence of menu items - if (child?.props?.children) { - const theChildren = child?.props?.children; - const childKeys = extractMenuItemsKeys(theChildren); - childKeys.forEach(keyMap => { - const k = keyMap.key; - keysMap[k] = {}; - keysMap[k].type = MenuItemKeyEnum.SubMenuItem; - keysMap[k].parent = child.key; - if (keyMap.ref) { - keysMap[k].ref = keyMap.ref; - } - }); - } - }); - - return keysMap; -}; - -/** - * - * Determines the next key to select based on the current key and direction - * - * @param keys - * @param keysMap - * @param currentKeyIndex - * @param direction - * @returns the selected key and the open key - */ -const getNavigationKeys = ( - keys: string[], - keysMap: Record, - currentKeyIndex: number, - direction = 'up', -) => { - const step = direction === 'up' ? -1 : 1; - const skipStep = direction === 'up' ? -2 : 2; - const keysLen = direction === 'up' ? 0 : keys.length; - const mathFn = direction === 'up' ? Math.max : Math.min; - let openKey: string | undefined; - let selectedKey = keys[mathFn(currentKeyIndex + step, keysLen)]; - - // go to first key if current key is the last - if (!selectedKey) { - return { selectedKey: keys[0], openKey: undefined }; - } - - const isSubMenu = keysMap[selectedKey]?.type === MenuItemKeyEnum.SubMenu; - if (isSubMenu) { - // this is a submenu, skip to first submenu item - selectedKey = keys[mathFn(currentKeyIndex + skipStep, keysLen)]; - } - // re-evaulate if current selected key is a submenu or submenu item - if (!isSubMenuOrItemType(keysMap[selectedKey].type)) { - openKey = undefined; - } else { - const parentKey = keysMap[selectedKey].parent; - if (parentKey) { - openKey = parentKey; - } - } - return { selectedKey, openKey }; -}; - -export const handleDropdownNavigation = ( - e: KeyboardEvent, - dropdownIsOpen: boolean, - menu: ReactElement, - toggleDropdown: () => void, - setSelectedKeys: (keys: string[]) => void, - setOpenKeys: (keys: string[]) => void, -) => { - if (e.key === NAV_KEYS.tab && !dropdownIsOpen) { - return; // if tab, continue with system tab navigation - } - const menuProps = menu.props || {}; - const keysMap = extractMenuItemsKeyMap(menuProps.children); - const keys = Object.keys(keysMap); - const { selectedKeys = [] } = menuProps; - const currentKeyIndex = keys.indexOf(selectedKeys[0]); - - switch (e.key) { - // toggle the dropdown on keypress - case ACTION_KEYS.enter: - case ACTION_KEYS.spacebar: - case ACTION_KEYS.space: - if (selectedKeys.length) { - const currentKey = selectedKeys[0]; - const currentKeyConf = keysMap[selectedKeys]; - // when a menu item is selected, then trigger - // the menu item's onClick handler - menuProps.onClick?.({ key: currentKey, domEvent: e }); - // trigger click handle on ref - if (currentKeyConf?.ref) { - const refMenuItemProps = currentKeyConf.ref.current.props; - refMenuItemProps.onClick?.({ - key: currentKey, - domEvent: e, - }); - } - // clear out/deselect keys - setSelectedKeys([]); - // close submenus - setOpenKeys([]); - // put focus back on menu trigger - e.currentTarget.focus(); - } - // if nothing was selected, or after selecting new menu item, - toggleDropdown(); - break; - // select the menu items going down - case NAV_KEYS.down: - case NAV_KEYS.tab && !e.shiftKey: { - const { selectedKey, openKey } = getNavigationKeys( - keys, - keysMap, - currentKeyIndex, - 'down', - ); - setSelectedKeys([selectedKey]); - setOpenKeys(openKey ? [openKey] : []); - break; - } - // select the menu items going up - case NAV_KEYS.up: - case NAV_KEYS.tab && e.shiftKey: { - const { selectedKey, openKey } = getNavigationKeys( - keys, - keysMap, - currentKeyIndex, - 'up', - ); - setSelectedKeys([selectedKey]); - setOpenKeys(openKey ? [openKey] : []); - break; - } - case NAV_KEYS.escape: - // close dropdown menu - toggleDropdown(); - break; - default: - break; - } -}; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx index 223b98d578d..35be0144af7 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx @@ -159,7 +159,7 @@ test('should call exportChart when exportCSV is clicked', async () => { }, ); fireEvent.click(getByRole('button', { name: 'More Options' })); - fireEvent.mouseOver(getByRole('button', { name: 'Download right' })); + fireEvent.mouseOver(getByRole('menuitem', { name: 'Download right' })); const exportAction = await findByText('Export to .CSV'); fireEvent.click(exportAction); expect(stubbedExportCSV).toHaveBeenCalledTimes(1); @@ -187,7 +187,7 @@ test('should call exportChart with row_limit props.maxRows when exportFullCSV is }, ); fireEvent.click(getByRole('button', { name: 'More Options' })); - fireEvent.mouseOver(getByRole('button', { name: 'Download right' })); + fireEvent.mouseOver(getByRole('menuitem', { name: 'Download right' })); const exportAction = await findByText('Export to full .CSV'); fireEvent.click(exportAction); expect(stubbedExportCSV).toHaveBeenCalledTimes(1); @@ -214,7 +214,7 @@ test('should call exportChart when exportXLSX is clicked', async () => { }, ); fireEvent.click(getByRole('button', { name: 'More Options' })); - fireEvent.mouseOver(getByRole('button', { name: 'Download right' })); + fireEvent.mouseOver(getByRole('menuitem', { name: 'Download right' })); const exportAction = await findByText('Export to Excel'); fireEvent.click(exportAction); expect(stubbedExportXLSX).toHaveBeenCalledTimes(1); @@ -241,7 +241,7 @@ test('should call exportChart with row_limit props.maxRows when exportFullXLSX i }, ); fireEvent.click(getByRole('button', { name: 'More Options' })); - fireEvent.mouseOver(getByRole('button', { name: 'Download right' })); + fireEvent.mouseOver(getByRole('menuitem', { name: 'Download right' })); const exportAction = await findByText('Export to full Excel'); fireEvent.click(exportAction); expect(stubbedExportXLSX).toHaveBeenCalledTimes(1); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx index cbee1765c9a..0f3049d8477 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx @@ -17,6 +17,7 @@ * under the License. */ import { render, screen } from 'spec/helpers/testing-library'; +import { Menu } from 'src/components/Menu'; import DownloadMenuItems from '.'; const createProps = () => ({ @@ -28,9 +29,14 @@ const createProps = () => ({ }); const renderComponent = () => { - render(, { - useRedux: true, - }); + render( + + + , + { + useRedux: true, + }, + ); }; test('Should render menu items', () => { diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx index 875537fb8e1..d9ffaaaedf5 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { Menu } from 'src/components/Menu'; import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; import DownloadScreenshot from './DownloadScreenshot'; import { DownloadScreenshotFormat } from './types'; @@ -44,42 +43,38 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => { isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) && isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot); - return ( - - {isWebDriverScreenshotEnabled ? ( - <> - - - - ) : ( - <> - - - - )} - + return isWebDriverScreenshotEnabled ? ( + <> + + + + ) : ( + <> + + + ); }; diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx index 8420847acab..16bbcff9ecc 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx @@ -36,6 +36,7 @@ const createProps = () => ({ emailSubject: 'Superset dashboard COVID Vaccine Dashboard', emailBody: 'Check out this dashboard: ', dashboardId: DASHBOARD_ID, + title: 'Test Dashboard', }); const { location } = window; @@ -66,30 +67,30 @@ afterAll((): void => { test('Should render menu items', () => { const props = createProps(); render( - + , { useRedux: true }, ); - expect( - screen.getByRole('menuitem', { name: 'Copy dashboard URL' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Share dashboard by email' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Copy dashboard URL' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Share dashboard by email' }), - ).toBeInTheDocument(); + expect(screen.getByText('Copy dashboard URL')).toBeInTheDocument(); + expect(screen.getByText('Share dashboard by email')).toBeInTheDocument(); }); test('Click on "Copy dashboard URL" and succeed', async () => { spy.mockResolvedValue(undefined); const props = createProps(); render( - + , { useRedux: true }, @@ -101,7 +102,7 @@ test('Click on "Copy dashboard URL" and succeed', async () => { expect(props.addDangerToast).toHaveBeenCalledTimes(0); }); - userEvent.click(screen.getByRole('button', { name: 'Copy dashboard URL' })); + userEvent.click(screen.getByText('Copy dashboard URL')); await waitFor(async () => { expect(spy).toHaveBeenCalledTimes(1); @@ -117,7 +118,12 @@ test('Click on "Copy dashboard URL" and fail', async () => { spy.mockRejectedValue(undefined); const props = createProps(); render( - + , { useRedux: true }, @@ -129,7 +135,7 @@ test('Click on "Copy dashboard URL" and fail', async () => { expect(props.addDangerToast).toHaveBeenCalledTimes(0); }); - userEvent.click(screen.getByRole('button', { name: 'Copy dashboard URL' })); + userEvent.click(screen.getByText('Copy dashboard URL')); await waitFor(async () => { expect(spy).toHaveBeenCalledTimes(1); @@ -146,7 +152,12 @@ test('Click on "Copy dashboard URL" and fail', async () => { test('Click on "Share dashboard by email" and succeed', async () => { const props = createProps(); render( - + , { useRedux: true }, @@ -157,9 +168,7 @@ test('Click on "Share dashboard by email" and succeed', async () => { expect(window.location.href).toBe(''); }); - userEvent.click( - screen.getByRole('button', { name: 'Share dashboard by email' }), - ); + userEvent.click(screen.getByText('Share dashboard by email')); await waitFor(() => { expect(props.addDangerToast).toHaveBeenCalledTimes(0); @@ -177,7 +186,12 @@ test('Click on "Share dashboard by email" and fail', async () => { ); const props = createProps(); render( - + , { useRedux: true }, @@ -188,9 +202,7 @@ test('Click on "Share dashboard by email" and fail', async () => { expect(window.location.href).toBe(''); }); - userEvent.click( - screen.getByRole('button', { name: 'Share dashboard by email' }), - ); + userEvent.click(screen.getByText('Share dashboard by email')); await waitFor(() => { expect(window.location.href).toBe(''); diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx index 0d7211ba8eb..6c5468da242 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx @@ -37,6 +37,9 @@ interface ShareMenuItemProps { copyMenuItemRef?: RefObject; shareByEmailMenuItemRef?: RefObject; selectedKeys?: string[]; + setOpenKeys?: Function; + key?: string; + title: string; } const ShareMenuItems = (props: ShareMenuItemProps) => { @@ -49,10 +52,8 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { addSuccessToast, dashboardId, dashboardComponentId, - copyMenuItemRef, - shareByEmailMenuItemRef, - selectedKeys, - ...rest + key, + title, } = props; const { dataMask, activeTabs } = useSelector( (state: RootState) => ({ @@ -95,28 +96,14 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { } return ( - - e.key === MenuKeys.CopyLink ? onCopyLink() : onShareByEmail() - } - > - -
- {copyMenuItemTitle} -
+ + onCopyLink()}> + {copyMenuItemTitle} - -
- {emailMenuItemTitle} -
+ onShareByEmail()}> + {emailMenuItemTitle} -
+ ); }; export default ShareMenuItems; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx index e3a80d21e9f..2c6196962ec 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx @@ -211,9 +211,6 @@ test('On selection change, send request and update checked value', async () => { expect( within(screen.getAllByRole('menuitem')[2]).getByLabelText('check'), ).toBeInTheDocument(); - expect( - within(screen.getAllByRole('menuitem')[3]).queryByLabelText('check'), - ).not.toBeInTheDocument(); userEvent.click(screen.getByText('Horizontal (Top)')); @@ -221,10 +218,6 @@ test('On selection change, send request and update checked value', async () => { expect( await within(screen.getAllByRole('menuitem')[3]).findByLabelText('check'), ).toBeInTheDocument(); - expect( - within(screen.getAllByRole('menuitem')[2]).queryByLabelText('check'), - ).not.toBeInTheDocument(); - // successful query await waitFor(() => expect(fetchMock.lastCall()?.[1]?.body).toEqual( @@ -236,6 +229,10 @@ test('On selection change, send request and update checked value', async () => { }), ), ); + await waitFor(() => { + const menuitems = screen.getAllByRole('menuitem'); + expect(menuitems.length).toBeGreaterThanOrEqual(3); + }); // 2nd check - checkmark stays after successful query expect( @@ -285,6 +282,11 @@ test('On failed request, restore previous selection', async () => { expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); + await waitFor(() => { + const menuitems = screen.getAllByRole('menuitem'); + expect(menuitems.length).toBeGreaterThanOrEqual(3); + }); + // checkmark gets rolled back to the original selection after successful query expect( await within(screen.getAllByRole('menuitem')[2]).findByLabelText('check'), diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts index 7881dbda181..0aae5aa4ffe 100644 --- a/superset-frontend/src/dashboard/styles.ts +++ b/superset-frontend/src/dashboard/styles.ts @@ -127,7 +127,7 @@ export const focusStyle = (theme: SupersetTheme) => css` } &:not( .superset-button, - .ant-menu-item, + .antd5-menu-item, a, .fave-unfave-icon, .ant-tabs-tabpane, diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 90f8b5d99e4..d4ed74a0194 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -343,7 +343,7 @@ class DatasourceControl extends PureComponent { {t('Query preview')} +
{t('Query preview')}
} modalTitle={t('Query preview')} modalBody={ diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx index e297b66885a..c6e103702a9 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx @@ -21,57 +21,58 @@ import userEvent from '@testing-library/user-event'; import { Menu } from 'src/components/Menu'; import DashboardItems from './DashboardsSubMenu'; -const asyncRender = (numberOfItems: number) => - waitFor(() => { - const dashboards = []; - for (let i = 1; i <= numberOfItems; i += 1) { - dashboards.push({ id: i, dashboard_title: `Dashboard ${i}` }); - } - render( - - - - - , - { - useRouter: true, - }, - ); - }); +const asyncRender = (numberOfItems: number) => { + const dashboards = []; + for (let i = 1; i <= numberOfItems; i += 1) { + dashboards.push({ id: i, dashboard_title: `Dashboard ${i}` }); + } + render( + + + + + , + { + useRouter: true, + }, + ); +}; test('renders a submenu', async () => { - await asyncRender(3); - expect(screen.getByText('Dashboard 1')).toBeInTheDocument(); - expect(screen.getByText('Dashboard 2')).toBeInTheDocument(); - expect(screen.getByText('Dashboard 3')).toBeInTheDocument(); + asyncRender(3); + await waitFor(() => { + expect(screen.getByText('Dashboard 1')).toBeInTheDocument(); + expect(screen.getByText('Dashboard 2')).toBeInTheDocument(); + expect(screen.getByText('Dashboard 3')).toBeInTheDocument(); + }); }); test('renders a submenu with search', async () => { - await asyncRender(20); - expect(screen.getByPlaceholderText('Search')).toBeInTheDocument(); + asyncRender(20); + expect(await screen.findByPlaceholderText('Search')).toBeInTheDocument(); }); test('displays a searched value', async () => { - await asyncRender(20); + asyncRender(20); userEvent.type(screen.getByPlaceholderText('Search'), '2'); - expect(screen.getByText('Dashboard 2')).toBeInTheDocument(); - expect(screen.getByText('Dashboard 20')).toBeInTheDocument(); + expect(await screen.findByText('Dashboard 2')).toBeInTheDocument(); + expect(await screen.findByText('Dashboard 20')).toBeInTheDocument(); }); test('renders a "No results found" message when searching', async () => { - await asyncRender(20); + asyncRender(20); userEvent.type(screen.getByPlaceholderText('Search'), 'unknown'); - expect(screen.getByText('No results found')).toBeInTheDocument(); + expect(await screen.findByText('No results found')).toBeInTheDocument(); }); test('renders a submenu with no dashboards', async () => { - await asyncRender(0); - expect(screen.getByText('None')).toBeInTheDocument(); + asyncRender(0); + expect(await screen.findByText('None')).toBeInTheDocument(); }); test('shows link icon when hovering', async () => { - await asyncRender(3); + asyncRender(3); expect(screen.queryByRole('img', { name: 'full' })).not.toBeInTheDocument(); - userEvent.hover(screen.getByText('Dashboard 1')); - expect(screen.getByRole('img', { name: 'full' })).toBeInTheDocument(); + userEvent.hover(await screen.findByText('Dashboard 1')); + expect(await screen.findByRole('img', { name: 'full' })).toBeInTheDocument(); }); diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index 5a3cf1ccf27..21cf69cec41 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -384,7 +384,7 @@ export const useExploreAdditionalActionsMenu = ( {t('Embed code')} +
{t('Embed code')}
} modalTitle={t('Embed code')} modalBody={ @@ -429,7 +429,7 @@ export const useExploreAdditionalActionsMenu = ( {t('View query')} +
{t('View query')}
} modalTitle={t('View query')} modalBody={ diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index 0673bf5fd05..6c9750037e6 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -597,6 +597,7 @@ test('renders all notification fields', async () => { expect(recipients).toBeInTheDocument(); expect(addNotificationMethod).toBeInTheDocument(); }); + test('adds another notification method section after clicking add notification method', async () => { render(, { useRedux: true, diff --git a/superset-frontend/src/features/home/DashboardTable.test.tsx b/superset-frontend/src/features/home/DashboardTable.test.tsx index 970dbc138a4..9fe6726fcb8 100644 --- a/superset-frontend/src/features/home/DashboardTable.test.tsx +++ b/superset-frontend/src/features/home/DashboardTable.test.tsx @@ -69,7 +69,7 @@ describe('DashboardTable', () => { }); it('render a submenu with clickable tabs and buttons', async () => { - expect(wrapper.find('SubMenu')).toExist(); + expect(wrapper.find('Menu')).toExist(); expect(wrapper.find('[role="tab"]')).toHaveLength(2); expect(wrapper.find('Button')).toHaveLength(6); act(() => { diff --git a/superset-frontend/src/features/home/LanguagePicker.stories.tsx b/superset-frontend/src/features/home/LanguagePicker.stories.tsx new file mode 100644 index 00000000000..b73d23f0235 --- /dev/null +++ b/superset-frontend/src/features/home/LanguagePicker.stories.tsx @@ -0,0 +1,58 @@ +/** + * 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 { MainNav as Menu } from 'src/components/Menu'; // Ensure correct import path +import LanguagePicker from './LanguagePicker'; // Ensure correct import path + +export default { + title: 'Components/LanguagePicker', + component: LanguagePicker, + parameters: { + docs: { + description: { + component: + 'The LanguagePicker component allows users to select a language from a dropdown.', + }, + }, + }, +}; + +const mockedProps = { + locale: 'en', + languages: { + en: { + flag: 'us', + name: 'English', + url: '/lang/en', + }, + it: { + flag: 'it', + name: 'Italian', + url: '/lang/it', + }, + }, +}; + +const Template = (args: any) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = mockedProps; diff --git a/superset-frontend/src/features/home/LanguagePicker.test.tsx b/superset-frontend/src/features/home/LanguagePicker.test.tsx index ad466eafc58..f846314a8c1 100644 --- a/superset-frontend/src/features/home/LanguagePicker.test.tsx +++ b/superset-frontend/src/features/home/LanguagePicker.test.tsx @@ -43,7 +43,7 @@ test('should render', async () => {
, ); - expect(await screen.findByRole('button')).toBeInTheDocument(); + expect(await screen.findByRole('menu')).toBeInTheDocument(); expect(container).toBeInTheDocument(); }); @@ -62,7 +62,7 @@ test('should render the items', async () => {
, ); - userEvent.hover(screen.getByRole('button')); + userEvent.hover(screen.getByRole('menuitem')); expect(await screen.findByText('English')).toBeInTheDocument(); expect(await screen.findByText('Italian')).toBeInTheDocument(); }); diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx index 123d12632f9..4a095472df1 100644 --- a/superset-frontend/src/features/home/Menu.test.tsx +++ b/superset-frontend/src/features/home/Menu.test.tsx @@ -327,8 +327,9 @@ test('should render the top navbar child menu items', async () => { useQueryParams: true, useRouter: true, }); - const sources = screen.getByText('Sources'); + const sources = await screen.findByText('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 }; @@ -477,13 +478,13 @@ test('should render the About section and version_string, sha or build_number wh }); 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}`); - const build = await screen.findByText(`Build: ${build_number}`); + const version = await screen.findAllByText(`Version: ${version_string}`); + const sha = await screen.findAllByText(`SHA: ${version_sha}`); + const build = await screen.findAllByText(`Build: ${build_number}`); expect(about).toBeInTheDocument(); - expect(version).toBeInTheDocument(); - expect(sha).toBeInTheDocument(); - expect(build).toBeInTheDocument(); + expect(version[0]).toBeInTheDocument(); + expect(sha[0]).toBeInTheDocument(); + expect(build[0]).toBeInTheDocument(); }); test('should render the Documentation link when available', async () => { @@ -578,9 +579,15 @@ test('should render an extension component if one is supplied', async () => { setupExtensions(); - render(, { useRouter: true, useQueryParams: true }); + render(, { + useRouter: true, + useQueryParams: true, + useRedux: true, + }); - expect( - await screen.findByText('navbar.right extension component'), - ).toBeInTheDocument(); + const extension = await screen.findAllByText( + 'navbar.right extension component', + ); + + expect(extension[0]).toBeInTheDocument(); }); diff --git a/superset-frontend/src/features/home/Menu.tsx b/superset-frontend/src/features/home/Menu.tsx index b91f71b20bf..ebcff4a023c 100644 --- a/superset-frontend/src/features/home/Menu.tsx +++ b/superset-frontend/src/features/home/Menu.tsx @@ -17,12 +17,11 @@ * under the License. */ import { useState, useEffect } from 'react'; -import { styled, css, useTheme, SupersetTheme } from '@superset-ui/core'; +import { styled } from '@superset-ui/core'; import { debounce } from 'lodash'; -import { Global } from '@emotion/react'; import { getUrlParam } from 'src/utils/urlUtils'; import { Row, Col, Grid } from 'src/components'; -import { MainNav as DropdownMenu, MenuMode } from 'src/components/Menu'; +import { MainNav, MenuMode } from 'src/components/Menu'; import { Tooltip } from 'src/components/Tooltip'; import { NavLink, useLocation } from 'react-router-dom'; import { GenericLink } from 'src/components/GenericLink/GenericLink'; @@ -99,92 +98,34 @@ const StyledHeader = styled.header` display: none; } } - .main-nav .ant-menu-submenu-title > svg { - top: ${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.gridUnit * 4}px; - } @media (max-width: 767px) { - .ant-menu-item { + .antd5-menu-item { padding: 0 ${theme.gridUnit * 6}px 0 ${theme.gridUnit * 3}px !important; } - .ant-menu > .ant-menu-item > a { + .antd5-menu > .antd5-menu-item > span > a { padding: 0px; } - .main-nav .ant-menu-submenu-title > svg:nth-of-type(1) { + .main-nav .antd5-menu-submenu-title > svg:nth-of-type(1) { display: none; } - .ant-menu-item-active > a { - &:hover { - color: ${theme.colors.primary.base} !important; - background-color: transparent !important; - } - } - } - .ant-menu-item a { - &:hover { - color: ${theme.colors.grayscale.dark1}; - background-color: ${theme.colors.primary.light5}; - border-bottom: none; - margin: 0; - &:after { - opacity: 1; - width: 100%; - } - } } `} `; -const globalStyles = (theme: SupersetTheme) => 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; - } - .ant-menu-vertical > .ant-menu-submenu.data-menu > .ant-menu-submenu-title { - height: 28px; - i { - padding-right: ${theme.gridUnit * 2}px; - margin-left: ${theme.gridUnit * 1.75}px; - } - } - .ant-menu-item-selected { - background-color: transparent; - &:not(.ant-menu-item-active) { - color: inherit; - border-bottom-color: transparent; - & > a { - color: inherit; - } - } - } - .ant-menu-horizontal > .ant-menu-item:has(> .is-active) { - color: ${theme.colors.primary.base}; - border-bottom-color: ${theme.colors.primary.base}; - & > a { - color: ${theme.colors.primary.base}; - } - } - .ant-menu-vertical > .ant-menu-item:has(> .is-active) { - background-color: ${theme.colors.primary.light5}; - & > a { - color: ${theme.colors.primary.base}; +const { SubMenu } = MainNav; + +const StyledSubMenu = styled(SubMenu)` + &.antd5-menu-submenu-active { + .antd5-menu-title-content { + color: ${({ theme }) => theme.colors.primary.base}; } } `; -const { SubMenu } = DropdownMenu; const { useBreakpoint } = Grid; @@ -201,7 +142,6 @@ export function Menu({ const [showMenu, setMenu] = useState('horizontal'); const screens = useBreakpoint(); const uiConfig = useUiConfig(); - const theme = useTheme(); useEffect(() => { function handleResize() { @@ -254,33 +194,33 @@ export function Menu({ }: MenuObjectProps) => { if (url && isFrontendRoute) { return ( - + {label} - + ); } if (url) { return ( - + {label} - + ); } return ( - : } > {childs?.map((child: MenuObjectChildProps | string, index1: number) => { if (typeof child === 'string' && child === '-' && label !== 'Data') { - return ; + return ; } if (typeof child !== 'string') { return ( - + {child.isFrontendRoute ? ( {child.label} )} - + ); } return null; })} - + ); }; return ( - {brand.text}
)} - {menu.map((item, index) => { const props = { @@ -351,7 +291,7 @@ export function Menu({ return renderSubMenu(props); })} - + , { useRedux: true, useQueryParams: true, diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index e5c34fdd9e4..0c53c3abd65 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -33,7 +33,7 @@ import { getExtensionsRegistry, useTheme, } from '@superset-ui/core'; -import { MainNav as Menu } from 'src/components/Menu'; +import { Menu } from 'src/components/Menu'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; import Label from 'src/components/Label'; @@ -71,21 +71,15 @@ const StyledI = styled.div` const styledDisabled = (theme: SupersetTheme) => css` color: ${theme.colors.grayscale.light1}; - .ant-menu-item-active { - color: ${theme.colors.grayscale.light1}; - cursor: default; - } `; const StyledDiv = styled.div<{ align: string }>` display: flex; + height: 100%; flex-direction: row; justify-content: ${({ align }) => align}; align-items: center; margin-right: ${({ theme }) => theme.gridUnit}px; - .ant-menu-submenu-title > svg { - top: ${({ theme }) => theme.gridUnit * 5.25}px; - } `; const StyledMenuItemWithIcon = styled.div` @@ -113,6 +107,14 @@ const styledChildMenu = (theme: SupersetTheme) => css` const { SubMenu } = Menu; +const StyledSubMenu = styled(SubMenu)` + &.antd5-menu-submenu-active { + .antd5-menu-title-content { + color: ${({ theme }) => theme.colors.primary.base}; + } + } +`; + const RightMenu = ({ align, settings, @@ -280,11 +282,8 @@ const RightMenu = ({ } }, [canDatabase, canDataset]); - const menuIconAndLabel = (menu: MenuObjectProps) => ( - <> - - {menu.label} - + const menuIcon = (menu: MenuObjectProps) => ( + ); const handleMenuSelection = (itemChose: any) => { @@ -407,10 +406,11 @@ const RightMenu = ({ mode="horizontal" onClick={handleMenuSelection} onOpenChange={onMenuOpen} + disabledOverflow > {RightMenuExtension && } {!navbarRight.user_is_anonymous && showActionDropdown && ( - @@ -424,10 +424,11 @@ const RightMenu = ({ if (menu.childs) { if (canShowChild) { return ( - {menu?.childs?.map?.((item, idx) => typeof item !== 'string' && item.name && item.perm ? ( @@ -437,7 +438,7 @@ const RightMenu = ({ ) : null, )} - + ); } if (!menu.url) { @@ -472,9 +473,9 @@ const RightMenu = ({ ) ); })} - + )} - } > @@ -548,7 +549,7 @@ const RightMenu = ({
, ]} - + {navbarRight.show_language_picker && ( { ]; setup({ buttons }); const testButton = screen.getByText(buttons[0].name); - expect(await screen.findAllByRole('button')).toHaveLength(3); + expect(await screen.findAllByRole('button')).toHaveLength(2); userEvent.click(testButton); expect(mockFunc).toHaveBeenCalled(); }); diff --git a/superset-frontend/src/features/home/SubMenu.tsx b/superset-frontend/src/features/home/SubMenu.tsx index 55b5f08f9b1..af1df5a59a3 100644 --- a/superset-frontend/src/features/home/SubMenu.tsx +++ b/superset-frontend/src/features/home/SubMenu.tsx @@ -24,7 +24,7 @@ import cx from 'classnames'; import { Tooltip } from 'src/components/Tooltip'; import { debounce } from 'lodash'; import { Row } from 'src/components'; -import { Menu, MenuMode, MainNav as DropdownMenu } from 'src/components/Menu'; +import { Menu, MenuMode, MainNav } from 'src/components/Menu'; import Button, { OnClickHandler } from 'src/components/Button'; import Icons from 'src/components/Icons'; import { MenuObjectProps } from 'src/types/bootstrapTypes'; @@ -48,7 +48,7 @@ const StyledHeader = styled.div` float: right; position: absolute; right: 0; - ul.ant-menu-root { + ul.antd5-menu-root { padding: 0px; } li[role='menuitem'] { @@ -69,78 +69,28 @@ const StyledHeader = styled.div` } .menu { background-color: ${({ theme }) => theme.colors.grayscale.light5}; - .ant-menu-horizontal { - line-height: inherit; - .ant-menu-item { - border-bottom: none; - &:hover { - border-bottom: none; - text-decoration: 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 > .antd5-menu { + padding: ${({ theme }) => theme.gridUnit * 5}px + ${({ theme }) => theme.gridUnit * 8}px; - .menu .ant-menu-item { - li, - div { - a, - div { - font-size: ${({ theme }) => theme.typography.sizes.s}px; - color: ${({ theme }) => theme.colors.secondary.dark1}; - - a { - margin: 0; - padding: ${({ theme }) => theme.gridUnit * 2}px - ${({ theme }) => theme.gridUnit * 4}px; - line-height: ${({ theme }) => theme.gridUnit * 5}px; - - &:hover { - text-decoration: none; - } - } - } - - &.no-router a { - padding: ${({ theme }) => theme.gridUnit * 2}px - ${({ theme }) => theme.gridUnit * 4}px; - } - - &.active a { - background: ${({ theme }) => theme.colors.secondary.light4}; - border-radius: ${({ theme }) => theme.borderRadius}px; - } - } - - li.active > a, - li.active > div, - div.active > div, - li > a:hover, - li > a:focus, - li > div:hover, - div > div:hover, - div > a:hover { - background: ${({ theme }) => theme.colors.secondary.light4}; - border-bottom: none; + .antd5-menu-item { border-radius: ${({ theme }) => theme.borderRadius}px; - margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; - text-decoration: none; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + padding: ${({ theme }) => theme.gridUnit}px + ${({ theme }) => theme.gridUnit * 4}px; + margin-right: ${({ theme }) => theme.gridUnit}px; + } + .antd5-menu-item:hover, + .antd5-menu-item:has(> span > .active) { + background-color: ${({ theme }) => theme.colors.secondary.light4}; } } .btn-link { padding: 10px 0; } - .ant-menu-horizontal { - border: none; - } @media (max-width: 767px) { .header, .nav-right { @@ -148,17 +98,6 @@ const StyledHeader = styled.div` margin-left: ${({ theme }) => theme.gridUnit * 2}px; } } - .ant-menu-submenu { - span[role='img'] { - position: absolute; - right: ${({ theme }) => -theme.gridUnit + -2}px; - top: ${({ theme }) => theme.gridUnit + 1}px !important; - } - } - .dropdown-menu-links > div.ant-menu-submenu-title, - .ant-menu-submenu-open.ant-menu-submenu-active > div.ant-menu-submenu-title { - color: ${({ theme }) => theme.colors.primary.dark1}; - } `; const styledDisabled = (theme: SupersetTheme) => css` @@ -169,7 +108,7 @@ const styledDisabled = (theme: SupersetTheme) => css` color: ${theme.colors.grayscale.light1}; } - .ant-menu-item-selected { + .antd5-menu-item-selected { background-color: ${theme.colors.grayscale.light1}; } `; @@ -210,7 +149,7 @@ export interface SubMenuProps { dropDownLinks?: Array; } -const { SubMenu } = DropdownMenu; +const { SubMenu } = MainNav; const SubMenuComponent: FunctionComponent = props => { const [showMenu, setMenu] = useState('horizontal'); @@ -255,7 +194,7 @@ const SubMenuComponent: FunctionComponent = props => { {props.name &&
{props.name}
} - + {props.tabs?.map(tab => { if ((props.usesRouter || hasHistory) && !!tab.usesRouter) { return ( @@ -290,7 +229,7 @@ const SubMenuComponent: FunctionComponent = props => { })}
- + {props.dropDownLinks?.map((link, i) => ( = props => { {link.childs?.map(item => { if (typeof item === 'object') { return item.disable ? ( - = props => { > {item.label} - + ) : ( - + {item.label} - + ); } return null; diff --git a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx index a2790f47403..18d0d620ec0 100644 --- a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx +++ b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx @@ -26,7 +26,7 @@ let isFeatureEnabledMock: jest.MockInstance; const createProps = () => ({ dashboardId: 1, useTextMenu: false, - isDropdownVisible: false, + isDropdownVisible: true, setIsDropdownVisible: jest.fn, setShowReportSubMenu: jest.fn, }); diff --git a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx index aedf4774428..66a8195f1f1 100644 --- a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx +++ b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx @@ -56,7 +56,7 @@ const deleteColor = (theme: SupersetTheme) => css` `; const onMenuHover = (theme: SupersetTheme) => css` - & .ant-menu-item { + & .antd5-menu-item { padding: 5px 12px; margin-top: 0px; margin-bottom: 4px; @@ -235,25 +235,23 @@ export default function HeaderReportDropDown({ ) : ( - isDropdownVisible && ( - - toggleActiveKey(report, !isReportActive)} - > - - - {t('Email reports active')} - - - - {t('Edit email report')} - - - {t('Delete email report')} - - - ) + + toggleActiveKey(report, !isReportActive)} + > + + + {t('Email reports active')} + + + + {t('Edit email report')} + + + {t('Delete email report')} + + ); const menu = () => ( @@ -326,7 +324,7 @@ export default function HeaderReportDropDown({ dashboardId ? CreationMethod.Dashboards : CreationMethod.Charts } /> - {useTextMenu ? textMenu() : iconMenu()} + {isDropdownVisible ? (useTextMenu ? textMenu() : iconMenu()) : null} {currentReportDeleting && ( theme.colors.grayscale.light1}; - .ant-menu-item:hover { - color: ${({ theme }) => theme.colors.grayscale.light1}; + .antd5-menu-item:hover { cursor: default; } &::after { diff --git a/superset-frontend/src/pages/Home/index.tsx b/superset-frontend/src/pages/Home/index.tsx index df382843a70..90466fe96e7 100644 --- a/superset-frontend/src/pages/Home/index.tsx +++ b/superset-frontend/src/pages/Home/index.tsx @@ -94,9 +94,6 @@ const WelcomeContainer = styled.div` margin: 0px 2px; } } - .ant-menu.ant-menu-light.ant-menu-root.ant-menu-horizontal { - padding-left: ${({ theme }) => theme.gridUnit * 8}px; - } button { padding: 3px 21px; } diff --git a/superset-frontend/src/theme/index.ts b/superset-frontend/src/theme/index.ts index 7b61bfdeca0..9cc2653c805 100644 --- a/superset-frontend/src/theme/index.ts +++ b/superset-frontend/src/theme/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import { addAlpha } from '@superset-ui/core'; import { type ThemeConfig } from 'antd-v5'; import { theme as supersetTheme } from 'src/preamble'; import { mix } from 'polished'; @@ -125,6 +126,10 @@ const baseConfig: ThemeConfig = { Divider: { colorSplit: supersetTheme.colors.grayscale.light3, }, + Dropdown: { + colorBgElevated: supersetTheme.colors.grayscale.light5, + zIndexPopup: supersetTheme.zIndex.max, + }, Input: { colorBorder: supersetTheme.colors.secondary.light3, colorBgContainer: supersetTheme.colors.grayscale.light5, @@ -145,6 +150,19 @@ const baseConfig: ThemeConfig = { colorSplit: supersetTheme.colors.grayscale.light3, colorText: supersetTheme.colors.grayscale.dark1, }, + Menu: { + itemHeight: 32, + colorBgContainer: supersetTheme.colors.grayscale.light5, + subMenuItemBg: supersetTheme.colors.grayscale.light5, + colorBgElevated: supersetTheme.colors.grayscale.light5, + boxShadowSecondary: `0 3px 6px -4px ${addAlpha(supersetTheme.colors.grayscale.dark2, 0.12)}, 0 6px 16px 0 ${addAlpha(supersetTheme.colors.grayscale.dark2, 0.08)}, 0 9px 28px 8px ${addAlpha(supersetTheme.colors.grayscale.dark2, 0.05)}`, + activeBarHeight: 0, + itemHoverBg: supersetTheme.colors.secondary.light5, + padding: supersetTheme.gridUnit * 2, + subMenuItemBorderRadius: 0, + horizontalLineHeight: 1.4, + zIndexPopup: supersetTheme.zIndex.max, + }, Modal: { colorBgMask: `${supersetTheme.colors.grayscale.dark2}73`, contentBg: supersetTheme.colors.grayscale.light5, diff --git a/superset-frontend/src/views/menu.tsx b/superset-frontend/src/views/menu.tsx index 4d7d40356c7..128fa9ca876 100644 --- a/superset-frontend/src/views/menu.tsx +++ b/superset-frontend/src/views/menu.tsx @@ -28,6 +28,7 @@ import createCache from '@emotion/cache'; import { ThemeProvider } from '@superset-ui/core'; import Menu from 'src/features/home/Menu'; import { theme } from 'src/preamble'; +import { AntdThemeProvider } from 'src/components/AntdThemeProvider'; import getBootstrapData from 'src/utils/getBootstrapData'; import { setupStore } from './store'; @@ -45,16 +46,18 @@ const app = ( // @ts-ignore: emotion types defs are incompatible between core and cache - - - - - - - + + + + + + + + + );