From bcc61bd9339543f43712b6966a5221bc37f1e802 Mon Sep 17 00:00:00 2001 From: Mehmet Salih Yavuz Date: Fri, 7 Feb 2025 20:38:04 +0300 Subject: [PATCH] refactor(Dropdown): Migrate Dropdown to Ant Design 5 (#31972) --- .../e2e/dashboard/_skip.controls.test.ts | 10 +- .../cypress/e2e/dashboard/drillby.test.ts | 5 +- .../e2e/dashboard/drilltodetail.test.ts | 5 +- .../e2e/dashboard/horizontalFilterBar.test.ts | 8 +- .../cypress/e2e/explore/chart.test.js | 22 +- .../visualizations/download_chart.test.js | 6 +- .../cypress/e2e/sqllab/tabs.test.ts | 4 +- .../cypress/support/directories.ts | 8 +- superset-frontend/src/GlobalStyles.tsx | 9 - .../components/QueryLimitSelect/index.tsx | 53 +-- .../SaveDatasetActionButton/index.tsx | 30 +- .../src/SqlLab/components/SqlEditor/index.tsx | 10 +- .../components/SqlEditorTabHeader/index.tsx | 6 +- .../SqlLab/components/TablePreview/index.tsx | 15 +- .../src/components/Button/index.tsx | 2 +- .../ChartContextMenu/ChartContextMenu.tsx | 42 ++- .../Chart/DrillBy/DrillByMenuItems.test.tsx | 1 - .../Chart/DrillBy/DrillByMenuItems.tsx | 35 +- .../DrillDetail/DrillDetailMenuItems.tsx | 27 +- .../Chart/MenuItemWithTruncation.tsx | 9 +- .../components/Dropdown/Dropdown.stories.tsx | 6 +- .../src/components/Dropdown/index.tsx | 27 +- .../src/components/DropdownButton/index.tsx | 97 ++--- .../DropdownSelectableIcon.stories.tsx | 56 --- .../DropdownSelectableIcon.test.tsx | 98 ----- .../DropdownSelectableIcon/index.tsx | 177 --------- .../ListViewCard/ListViewCard.stories.tsx | 10 +- .../src/components/Menu/index.tsx | 27 +- .../PageHeaderWithActions/index.tsx | 10 +- .../src/components/PopoverDropdown/index.tsx | 10 +- .../Table/cell-renderers/ActionCell/index.tsx | 4 +- .../src/components/Tooltip/index.tsx | 18 +- superset-frontend/src/components/index.ts | 2 - .../dashboard/components/CssEditor/index.tsx | 6 +- .../DashboardBuilder.test.tsx | 3 - .../HeaderActionsDropdown.test.tsx | 260 ------------- .../src/dashboard/components/Header/index.jsx | 180 +++++---- .../src/dashboard/components/Header/types.ts | 5 +- ...x.tsx => useHeaderActionsDropdownMenu.tsx} | 345 +++++++++--------- .../components/RefreshIntervalModal.test.tsx | 18 +- .../SliceHeaderControls.test.tsx | 31 +- .../components/SliceHeaderControls/index.tsx | 38 +- .../DownloadAsImage.test.tsx | 6 +- .../DownloadMenuItems/DownloadAsImage.tsx | 12 +- .../DownloadMenuItems/DownloadAsPdf.test.tsx | 6 +- .../menu/DownloadMenuItems/DownloadAsPdf.tsx | 12 +- .../DownloadMenuItems.test.tsx | 12 +- .../menu/DownloadMenuItems/index.tsx | 51 +-- .../ShareMenuItems/ShareMenuItems.test.tsx | 1 + .../components/menu/ShareMenuItems/index.tsx | 16 +- .../FilterBarSettings.test.tsx | 50 ++- .../FilterBar/FilterBarSettings/index.tsx | 76 ++-- .../FilterConfigurationLink.test.tsx | 15 +- .../FilterConfigurationLink/index.tsx | 65 +--- .../useFilterConfigModal.tsx | 82 +++++ .../nativeFilters/FilterBar/Header/index.tsx | 3 - .../nativeFilters/FilterCard/NameRow.tsx | 15 +- .../dashboard/hooks/useDownloadScreenshot.ts | 184 ++++++++++ superset-frontend/src/dashboard/styles.ts | 6 +- .../components/ExploreChartHeader/index.jsx | 51 ++- .../components/ExportToCSVDropdown/index.tsx | 10 +- .../DatasourceControl.test.jsx | 6 + .../controls/DatasourceControl/index.jsx | 14 +- .../useExploreAdditionalActionsMenu/index.jsx | 56 ++- .../src/features/charts/ChartCard.tsx | 6 +- .../src/features/dashboards/DashboardCard.tsx | 6 +- .../src/features/home/RightMenu.tsx | 2 + .../src/features/home/SavedQueries.tsx | 6 +- .../HeaderReportDropdown/index.test.tsx | 71 ++-- .../HeaderReportDropdown/index.tsx | 132 ++----- .../src/features/tags/TagCard.tsx | 6 +- 71 files changed, 1137 insertions(+), 1576 deletions(-) delete mode 100644 superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx delete mode 100644 superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx delete mode 100644 superset-frontend/src/components/DropdownSelectableIcon/index.tsx delete mode 100644 superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx rename superset-frontend/src/dashboard/components/Header/{HeaderActionsDropdown/index.tsx => useHeaderActionsDropdownMenu.tsx} (53%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx create mode 100644 superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.controls.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.controls.test.ts index 4a65d68cf..0e5c73313 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.controls.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.controls.test.ts @@ -47,12 +47,12 @@ describe.skip('Dashboard top-level controls', () => { // Solution: pause the network before clicking, assert, then unpause network. cy.get('[data-test="refresh-chart-menu-item"]').should( 'have.class', - 'ant-dropdown-menu-item-disabled', + 'antd5-dropdown-menu-item-disabled', ); waitForChartLoad(mapSpec); cy.get('[data-test="refresh-chart-menu-item"]').should( 'not.have.class', - 'ant-dropdown-menu-item-disabled', + 'antd5-dropdown-menu-item-disabled', ); }); }); @@ -65,7 +65,7 @@ describe.skip('Dashboard top-level controls', () => { cy.get('[aria-label="more-horiz"]').click(); cy.get('[data-test="refresh-dashboard-menu-item"]').should( 'not.have.class', - 'ant-dropdown-menu-item-disabled', + 'antd5-dropdown-menu-item-disabled', ); cy.get('[data-test="refresh-dashboard-menu-item"]').click({ @@ -73,7 +73,7 @@ describe.skip('Dashboard top-level controls', () => { }); cy.get('[data-test="refresh-dashboard-menu-item"]').should( 'have.class', - 'ant-dropdown-menu-item-disabled', + 'antd5-dropdown-menu-item-disabled', ); // wait all charts force refreshed. @@ -94,7 +94,7 @@ describe.skip('Dashboard top-level controls', () => { cy.get('[aria-label="more-horiz"]').click(); cy.get('[data-test="refresh-dashboard-menu-item"]').and( 'not.have.class', - 'ant-dropdown-menu-item-disabled', + 'antd5-dropdown-menu-item-disabled', ); }); }); 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 e471d1da8..2586fbd43 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts @@ -54,15 +54,14 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => { interceptV1ChartData(); } - cy.get('.ant-dropdown:not(.ant-dropdown-hidden)') - .first() + cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)') .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-submenu-hidden) [data-test="drill-by-submenu"]', + '.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]', ) .should('be.visible') .find('[role="menuitem"]') 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 4ebd64dd6..d2eb482a9 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts @@ -61,15 +61,14 @@ function drillToDetail(targetMenuItem: string) { const drillToDetailBy = (targetDrill: string) => { interceptSamples(); - cy.get('.ant-dropdown:not(.ant-dropdown-hidden)') - .first() + cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)') .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"]', + '.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]', ) .should('be.visible') .find('[role="menuitem"]') 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 bcacae8a3..ec20224fc 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('.antd5-menu-item-selected') + cy.get('.antd5-dropdown-menu-item-selected') .contains('Horizontal (Top)') .should('exist'); - cy.get('.antd5-menu-item').contains('Vertical (Left)').click(); + cy.get('.antd5-dropdown-menu-item').contains('Vertical (Left)').click(); cy.getBySel('dashboard-filters-panel').should('exist'); } else { - cy.get('.antd5-menu-item-selected') + cy.get('.antd5-dropdown-menu-item-selected') .contains('Vertical (Left)') .should('exist'); - cy.get('.antd5-menu-item').contains('Horizontal (Top)').click(); + cy.get('.antd5-dropdown-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/chart.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js index c7b81a3e1..abe5a0c51 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js +++ b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js @@ -31,35 +31,35 @@ const SAMPLE_DASHBOARDS_INDEXES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; function openDashboardsAddedTo() { cy.getBySel('actions-trigger').click(); - cy.get('.ant-dropdown-menu-submenu-title') + cy.get('.antd5-dropdown-menu-submenu-title') .contains('On dashboards') .trigger('mouseover', { force: true }); } function closeDashboardsAddedTo() { - cy.get('.ant-dropdown-menu-submenu-title') + cy.get('.antd5-dropdown-menu-submenu-title') .contains('On dashboards') .trigger('mouseout', { force: true }); cy.getBySel('actions-trigger').click(); } function verifyDashboardsSubmenuItem(dashboardName) { - cy.get('.ant-dropdown-menu-submenu-popup').contains(dashboardName); + cy.get('.antd5-dropdown-menu-submenu-popup').contains(dashboardName); closeDashboardsAddedTo(); } function verifyDashboardSearch() { openDashboardsAddedTo(); - cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover'); - cy.get('.ant-dropdown-menu-submenu-popup') + cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover'); + cy.get('.antd5-dropdown-menu-submenu-popup') .find('input[placeholder="Search"]') .type('1'); - cy.get('.ant-dropdown-menu-submenu-popup').contains('1 - Sample dashboard'); - cy.get('.ant-dropdown-menu-submenu-popup') + cy.get('.antd5-dropdown-menu-submenu-popup').contains('1 - Sample dashboard'); + cy.get('.antd5-dropdown-menu-submenu-popup') .find('input[placeholder="Search"]') .type('Blahblah'); - cy.get('.ant-dropdown-menu-submenu-popup').contains('No results found'); - cy.get('.ant-dropdown-menu-submenu-popup') + cy.get('.antd5-dropdown-menu-submenu-popup').contains('No results found'); + cy.get('.antd5-dropdown-menu-submenu-popup') .find('[aria-label="close-circle"]') .click(); closeDashboardsAddedTo(); @@ -68,8 +68,8 @@ function verifyDashboardSearch() { function verifyDashboardLink() { interceptDashboardGet(); openDashboardsAddedTo(); - cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover'); - cy.get('.ant-dropdown-menu-submenu-popup a') + cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover'); + cy.get('.antd5-dropdown-menu-submenu-popup a') .first() .invoke('removeAttr', 'target') .click(); diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js index ce7e52277..76653dada 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js +++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js @@ -36,10 +36,10 @@ describe('Download Chart > Bar chart', () => { }; cy.visitChartByParams(formData); - cy.get('.header-with-actions .ant-dropdown-trigger').click(); - cy.get(':nth-child(3) > .ant-dropdown-menu-submenu-title').click(); + cy.get('.header-with-actions .antd5-dropdown-trigger').click(); + cy.get(':nth-child(3) > .antd5-dropdown-menu-submenu-title').click(); cy.get( - '.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)', + '.antd5-dropdown-menu-submenu > .antd5-dropdown-menu li:nth-child(3)', ).click(); cy.verifyDownload('.jpg', { contains: true, diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts index 0deeabde8..ef97eb1c1 100644 --- a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts @@ -80,9 +80,9 @@ describe('SqlLab query tabs', () => { // configure some editor settings cy.get(editorInput).type('some random query string', { force: true }); cy.get(queryLimitSelector).parent().click({ force: true }); - cy.get('.ant-dropdown-menu') + cy.get('.antd5-dropdown-menu') .last() - .find('.ant-dropdown-menu-item') + .find('.antd5-dropdown-menu-item') .first() .click({ force: true }); diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts index 3445cf5f2..c2754e454 100644 --- a/superset-frontend/cypress-base/cypress/support/directories.ts +++ b/superset-frontend/cypress-base/cypress/support/directories.ts @@ -158,10 +158,10 @@ export const sqlLabView = { runButton: '.css-d3dxop', }, rowsLimit: { - dropdown: '.ant-dropdown-menu', - limitButton: '.ant-dropdown-menu-item', + dropdown: '.antd5-dropdown-menu', + limitButton: '.antd5-dropdown-menu-item', limitButtonText: '.css-151uxnz', - limitTextWithValue: '[class="ant-dropdown-trigger"]', + limitTextWithValue: '[class="antd5-dropdown-trigger"]', }, renderedTableHeader: '.ReactVirtualized__Table__headerRow', renderedTableRow: '.ReactVirtualized__Table__row', @@ -633,7 +633,7 @@ export const dashboardView = { refreshChart: dataTestLocator('refresh-chart-menu-item'), }, threeDotsMenuIcon: - '.header-with-actions .right-button-panel .ant-dropdown-trigger', + '.header-with-actions .right-button-panel .antd5-dropdown-trigger', threeDotsMenuDropdown: dataTestLocator('header-actions-menu'), refreshDashboard: dataTestLocator('refresh-dashboard-menu-item'), saveAsMenuOption: dataTestLocator('save-as-menu-item'), diff --git a/superset-frontend/src/GlobalStyles.tsx b/superset-frontend/src/GlobalStyles.tsx index 09a2792dd..d213faac8 100644 --- a/superset-frontend/src/GlobalStyles.tsx +++ b/superset-frontend/src/GlobalStyles.tsx @@ -43,8 +43,6 @@ export const GlobalStyles = () => ( // TODO: Remove z-indexes when Ant Design is fully upgraded to v5 // Prefer vanilla Ant Design z-indexes that should work out of the box .ant-popover, - .antd5-dropdown, - .ant-dropdown, .ant-select-dropdown, .antd5-modal-wrap, .antd5-modal-mask, @@ -105,13 +103,6 @@ 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/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx index df35351e5..ce9cb2b06 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx @@ -17,12 +17,13 @@ * under the License. */ import { useDispatch } from 'react-redux'; -import { styled, useTheme, t } from '@superset-ui/core'; -import { AntdDropdown } from 'src/components'; +import { useTheme, t } from '@superset-ui/core'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import Button from 'src/components/Button'; export interface QueryLimitSelectProps { queryEditorId: string; @@ -34,28 +35,6 @@ export function convertToNumWithSpaces(num: number) { return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 '); } -const LimitSelectStyled = styled.span` - ${({ theme }) => ` - .ant-dropdown-trigger { - align-items: center; - color: ${theme.colors.grayscale.dark2}; - display: flex; - font-size: 12px; - margin-right: ${theme.gridUnit * 2}px; - text-decoration: none; - border: 0; - background: transparent; - span { - display: inline-block; - margin-right: ${theme.gridUnit * 2}px; - &:last-of-type: { - margin-right: ${theme.gridUnit * 4}px; - } - } - } - `} -`; - function renderQueryLimit( maxRow: number, setQueryLimit: (limit: number) => void, @@ -94,20 +73,18 @@ const QueryLimitSelect = ({ dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit)); return ( - - - - - + renderQueryLimit(maxRow, setQueryLimit)} + trigger={['click']} + > + + ); }; diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx index 2acd7665b..6cb17bc2b 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx @@ -16,12 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { FC } from 'react'; -import { t, useTheme, styled } from '@superset-ui/core'; +import { t, useTheme } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import { DropdownButton } from 'src/components/DropdownButton'; import Button from 'src/components/Button'; -import { DropdownButtonProps } from 'antd/lib/dropdown'; interface SaveDatasetActionButtonProps { setShowSave: (arg0: boolean) => void; @@ -34,34 +32,14 @@ const SaveDatasetActionButton = ({ }: SaveDatasetActionButtonProps) => { const theme = useTheme(); - const StyledDropdownButton = styled( - DropdownButton as FC, - )` - &.ant-dropdown-button button.ant-btn.ant-btn-default { - font-weight: ${theme.gridUnit * 150}; - background-color: ${theme.colors.primary.light4}; - color: ${theme.colors.primary.dark1}; - &:nth-of-type(2) { - &:before, - &:hover:before { - border-left: 2px solid ${theme.colors.primary.dark2}; - } - } - } - span[name='caret-down'] { - margin-left: ${theme.gridUnit * 1}px; - color: ${theme.colors.primary.dark2}; - } - `; - return !overlayMenu ? ( ) : ( - setShowSave(true)} - overlay={overlayMenu} + dropdownRender={() => overlayMenu} icon={ {t('Save')} - + ); }; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 527c22752..957de4fbc 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -56,7 +56,8 @@ import Mousetrap from 'mousetrap'; import Button from 'src/components/Button'; import Timer from 'src/components/Timer'; import ResizableSidebar from 'src/components/ResizableSidebar'; -import { AntdDropdown, Skeleton } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; +import { Skeleton } from 'src/components'; import { Switch } from 'src/components/Switch'; import { Input } from 'src/components/Input'; import { Menu } from 'src/components/Menu'; @@ -868,9 +869,12 @@ const SqlEditor: FC = ({ - + renderDropdown()} + trigger={['click']} + > - + )} diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx index d8f4c8e4e..76c8b74b1 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx @@ -20,7 +20,7 @@ import { useMemo, FC } from 'react'; import { bindActionCreators } from 'redux'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import { Dropdown } from 'src/components/Dropdown'; +import { MenuDotsDropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import { styled, t, QueryState } from '@superset-ui/core'; import { @@ -88,10 +88,10 @@ const SqlEditorTabHeader: FC = ({ queryEditor }) => { return ( - + = ({ dbId, catalog, schema, tableName }) => { <Icons.Table iconSize="l" /> {tableName} - <AntdDropdown - overlay={ + <Dropdown + dropdownRender={() => ( <Menu onClick={({ key }) => { if (key === 'refresh-table') { @@ -324,7 +321,7 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => { }} items={dropdownMenu} /> - } + )} trigger={['click']} > <Icons.DownSquareOutlined @@ -332,7 +329,7 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => { style={{ marginTop: 2, marginLeft: 4 }} aria-label={t('Table actions')} /> - </AntdDropdown> + </Dropdown> {isMetadataRefreshing ? ( diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx index c38fbc15c..aa447e13b 100644 --- a/superset-frontend/src/components/Button/index.tsx +++ b/superset-frontend/src/components/Button/index.tsx @@ -67,7 +67,7 @@ const decideType = (buttonStyle: ButtonStyle) => { success: 'primary', secondary: 'default', default: 'default', - tertiary: 'dashed', + tertiary: 'default', dashed: 'dashed', link: 'link', }; diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index d619dae48..efea8cc23 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -30,6 +30,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Behavior, BinaryQueryObjectFilterClause, + Column, ContextMenuFilters, ensureIsArray, FeatureFlag, @@ -42,8 +43,11 @@ import { import { RootState } from 'src/dashboard/types'; import { Menu } from 'src/components/Menu'; import { usePermissions } from 'src/hooks/usePermissions'; -import { AntdDropdown as Dropdown } from 'src/components/index'; +import { Dropdown } from 'src/components/Dropdown'; import { updateDataMask } from 'src/dataMask/actions'; +import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal'; +import { useVerboseMap } from 'src/hooks/apiResources/datasets'; +import { Dataset } from 'src/components/Chart/types'; import { DrillDetailMenuItems } from '../DrillDetail'; import { getMenuAdjustedY } from '../utils'; import { MenuItemTooltip } from '../DisabledMenuItemTooltip'; @@ -114,8 +118,22 @@ const ChartContextMenu = ( }>({ clientX: 0, clientY: 0 }); const [drillModalIsOpen, setDrillModalIsOpen] = useState(false); + const [drillByColumn, setDrillByColumn] = useState(); + const [showDrillByModal, setShowDrillByModal] = useState(false); + const [dataset, setDataset] = useState(); + const verboseMap = useVerboseMap(dataset); - const menuItems = []; + const handleDrillBy = useCallback((column: Column, dataset: Dataset) => { + setDrillByColumn(column); + setDataset(dataset); // Save dataset when drilling + setShowDrillByModal(true); + }, []); + + const handleCloseDrillByModal = useCallback(() => { + setShowDrillByModal(false); + }, []); + + const menuItems: React.JSX.Element[] = []; const showDrillToDetail = isFeatureEnabled(FeatureFlag.DrillToDetail) && @@ -249,9 +267,9 @@ const ChartContextMenu = ( formData={formData} contextMenuY={clientY} submenuIndex={submenuIndex} - canDownload={canDownload} open={openKeys.includes('drill-by-submenu')} key="drill-by-submenu" + onDrillBy={handleDrillBy} {...(additionalConfig?.drillBy || {})} />, ); @@ -286,7 +304,7 @@ const ChartContextMenu = ( return ReactDOM.createPortal( <> ( {t('No actions')} )} - } + )} trigger={['click']} - onVisibleChange={value => { + onOpenChange={value => { setVisible(value); if (!value) { setOpenKeys([]); } }} - visible={visible} + open={visible} > )} + {showDrillByModal && drillByColumn && dataset && filters?.drillBy && ( + + )} , 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 c874da685..c494ce52a 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx @@ -74,7 +74,6 @@ const renderMenu = ({ diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx index 47666db78..b054ff02d 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx @@ -53,10 +53,8 @@ import { cachedSupersetGet, supersetGetCache, } from 'src/utils/cachedSupersetGet'; -import { useVerboseMap } from 'src/hooks/apiResources/datasets'; import { InputRef } from 'antd-v5'; import { MenuItemTooltip } from '../DisabledMenuItemTooltip'; -import DrillByModal from './DrillByModal'; import { getSubmenuYOffset } from '../utils'; import { MenuItemWithTruncation } from '../MenuItemWithTruncation'; import { Dataset } from '../types'; @@ -74,8 +72,8 @@ export interface DrillByMenuItemsProps { onClick?: (event: MouseEvent) => void; openNewModal?: boolean; excludedColumns?: Column[]; - canDownload: boolean; open: boolean; + onDrillBy?: (column: Column, dataset: Dataset) => void; } const loadDrillByOptions = getExtensionsRegistry().get('load.drillby.options'); @@ -106,8 +104,8 @@ export const DrillByMenuItems = ({ onClick = () => {}, excludedColumns, openNewModal = true, - canDownload, open, + onDrillBy, ...rest }: DrillByMenuItemsProps) => { const theme = useTheme(); @@ -117,25 +115,20 @@ export const DrillByMenuItems = ({ const [debouncedSearchInput, setDebouncedSearchInput] = useState(''); const [dataset, setDataset] = useState(); const [columns, setColumns] = useState([]); - const [showModal, setShowModal] = useState(false); - const [currentColumn, setCurrentColumn] = useState(); const ref = useRef(null); const showSearch = loadDrillByOptions || columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD; + const handleSelection = useCallback( (event, column) => { onClick(event); onSelection(column, drillByConfig); - setCurrentColumn(column); - if (openNewModal) { - setShowModal(true); + if (openNewModal && onDrillBy && dataset) { + onDrillBy(column, dataset); } }, - [drillByConfig, onClick, onSelection, openNewModal], + [drillByConfig, onClick, onSelection, openNewModal, onDrillBy, dataset], ); - const closeModal = useCallback(() => { - setShowModal(false); - }, []); useEffect(() => { if (open) { @@ -156,7 +149,6 @@ export const DrillByMenuItems = ({ ?.behaviors.find(behavior => behavior === Behavior.DrillBy), [formData.viz_type], ); - const verboseMap = useVerboseMap(dataset); useEffect(() => { async function loadOptions() { @@ -275,11 +267,11 @@ export const DrillByMenuItems = ({ const column = columns[index]; return ( handleSelection(e, column)} style={style} + {...rest} > {column.verbose_name || column.column_name} @@ -289,6 +281,7 @@ export const DrillByMenuItems = ({ return ( <> - {showModal && ( - - )} ); }; diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx index e20bc0290..aae82d830 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx @@ -60,8 +60,15 @@ const DISABLED_REASONS = { ), }; -const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => ( - +const DisabledMenuItem = ({ + children, + menuKey, + ...rest +}: { + children: ReactNode; + menuKey: string; +}) => ( +
+ {DRILL_TO_DETAIL} ) : ( - + {DRILL_TO_DETAIL} ); const drillToDetailByMenuItem = drillByDisabled ? ( - + {DRILL_TO_DETAIL_BY} ) : (
{filters.map((filter, i) => ( {`${DRILL_TO_DETAIL_BY} `} @@ -224,7 +226,6 @@ const DrillDetailMenuItems = ({ ))} {filters.length > 1 && ( diff --git a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx index 9e8802f12..cf77427cc 100644 --- a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx +++ b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx @@ -28,12 +28,15 @@ export type MenuItemWithTruncationProps = { children: ReactNode; onClick?: MenuItemProps['onClick']; style?: CSSProperties; + menuKey?: string; }; export const MenuItemWithTruncation = ({ tooltipText, children, - ...props + onClick, + style, + menuKey, }: MenuItemWithTruncationProps) => { const [itemRef, itemIsTruncated] = useCSSTextTruncation(); @@ -43,7 +46,9 @@ export const MenuItemWithTruncation = ({ display: flex; line-height: 1.5em; `} - {...props} + eventKey={menuKey} + onClick={onClick} + style={style} >
( - ( + diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx index ef81bd42c..1fdca2e75 100644 --- a/superset-frontend/src/components/Dropdown/index.tsx +++ b/superset-frontend/src/components/Dropdown/index.tsx @@ -24,13 +24,10 @@ import { cloneElement, } 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, + Dropdown as AntdDropdown, + DropdownProps as AntdDropdownProps, } from 'antd-v5'; -import { DropDownProps } from 'antd/lib/dropdown'; import { styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -83,7 +80,8 @@ export enum IconOrientation { Vertical = 'vertical', Horizontal = 'horizontal', } -export interface DropdownProps extends DropDownProps { + +export interface MenuDotsDropdownProps extends AntdDropdownProps { overlay: ReactElement; iconOrientation?: IconOrientation; } @@ -100,19 +98,19 @@ const RenderIcon = ( return component; }; -export const Dropdown = ({ +export const MenuDotsDropdown = ({ overlay, iconOrientation = IconOrientation.Vertical, ...rest -}: DropdownProps) => ( - +}: MenuDotsDropdownProps) => ( + overlay} {...rest}> {RenderIcon(iconOrientation)} ); -export interface NoAnimationDropdownProps extends Antd5DropdownProps { +export interface NoAnimationDropdownProps extends AntdDropdownProps { children: ReactNode; onBlur?: (e: FocusEvent) => void; onKeyDown?: (e: KeyboardEvent) => void; @@ -126,8 +124,13 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => { }); return ( - + {childrenWithProps} - + ); }; + +export type DropdownProps = AntdDropdownProps; +export const Dropdown = (props: DropdownProps) => ( + +); diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx index 32a7739e3..84ffd5fe2 100644 --- a/superset-frontend/src/components/DropdownButton/index.tsx +++ b/superset-frontend/src/components/DropdownButton/index.tsx @@ -16,90 +16,39 @@ * specific language governing permissions and limitations * under the License. */ -import { ReactNode, ReactElement } from 'react'; +import { type ComponentProps } from 'react'; -import { AntdDropdown, AntdTooltip } from 'src/components'; -import { styled } from '@superset-ui/core'; +import { Dropdown } from 'antd-v5'; +import { Tooltip, TooltipPlacement } from 'src/components/Tooltip'; import { kebabCase } from 'lodash'; -const StyledDropdownButton = styled.div` - .ant-btn-group { - button.ant-btn { - background-color: ${({ theme }) => theme.colors.primary.dark1}; - border-color: transparent; - color: ${({ theme }) => theme.colors.grayscale.light5}; - font-size: 12px; - line-height: 13px; - outline: none; - &:first-of-type { - border-radius: ${({ theme }) => - `${theme.gridUnit}px 0 0 ${theme.gridUnit}px`}; - margin: 0; - } - - &:disabled { - background-color: ${({ theme }) => theme.colors.grayscale.light2}; - color: ${({ theme }) => theme.colors.grayscale.base}; - } - &:nth-of-type(2) { - margin: 0; - border-radius: ${({ theme }) => - `0 ${theme.gridUnit}px ${theme.gridUnit}px 0`}; - width: ${({ theme }) => theme.gridUnit * 9}px; - &:before, - &:hover:before { - border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5}; - content: ''; - display: block; - height: ${({ theme }) => theme.gridUnit * 8}px; - margin: 0; - position: absolute; - width: ${({ theme }) => theme.gridUnit * 0.25}px; - } - - &:disabled:before { - border-left: 1px solid ${({ theme }) => theme.colors.grayscale.base}; - } - } - } - } -`; - -export interface DropdownButtonProps { - overlay: ReactElement; +export type DropdownButtonProps = ComponentProps & { tooltip?: string; - placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; - buttonsRender?: ((buttons: ReactNode[]) => ReactNode[]) | undefined; -} + tooltipPlacement?: TooltipPlacement; +}; export const DropdownButton = ({ - overlay, + dropdownRender, tooltip, - placement, + tooltipPlacement, + children, ...rest }: DropdownButtonProps) => { - const buildButton = ( - props: { - buttonsRender?: DropdownButtonProps['buttonsRender']; - } = {}, - ) => ( - - - + const button = ( + + {children} + ); if (tooltip) { - return buildButton({ - buttonsRender: ([leftButton, rightButton]) => [ - - {leftButton} - , - rightButton, - ], - }); + return ( + + {button} + + ); } - return buildButton(); + return button; }; diff --git a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx b/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx deleted file mode 100644 index cc12a8ba5..000000000 --- a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx +++ /dev/null @@ -1,56 +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 Icons from 'src/components/Icons'; -import DropdownSelectableIcon, { DropDownSelectableProps } from '.'; - -export default { - title: 'DropdownSelectableIcon', - component: DropdownSelectableIcon, -}; - -export const Component = (props: DropDownSelectableProps) => ( - } - /> -); - -Component.args = { - info: 'Info go here', - selectedKeys: ['vertical'], - menuItems: [ - { - key: 'vertical', - label: 'Vertical', - }, - { - key: 'horizontal', - label: 'Horizontal', - }, - ], -}; - -Component.argTypes = { - onSelect: { - action: 'onSelect', - table: { - disable: true, - }, - }, -}; diff --git a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx b/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx deleted file mode 100644 index cfff77d5e..000000000 --- a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx +++ /dev/null @@ -1,98 +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 { render, screen, waitFor } from 'spec/helpers/testing-library'; -import Icons from 'src/components/Icons'; -import userEvent from '@testing-library/user-event'; -import DropdownSelectableIcon, { DropDownSelectableProps } from '.'; - -const mockedProps = { - menuItems: [ - { - key: 'vertical', - label: 'vertical', - }, - { - key: 'horizontal', - label: 'horizontal', - }, - ], - selectedKeys: [], - icon: , -}; - -const asyncRender = (props: DropDownSelectableProps) => - waitFor(() => render()); - -const openMenu = () => { - userEvent.click(screen.getByRole('img', { name: 'gear' })); -}; - -test('should render', async () => { - const { container } = await asyncRender(mockedProps); - expect(container).toBeInTheDocument(); -}); - -test('should render the icon', async () => { - await asyncRender(mockedProps); - expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument(); -}); - -test('should not render the info', async () => { - await asyncRender(mockedProps); - openMenu(); - expect( - screen.queryByTestId('dropdown-selectable-info'), - ).not.toBeInTheDocument(); -}); - -test('should render the info', async () => { - const infoProps = { - ...mockedProps, - info: 'Test', - }; - await asyncRender(infoProps); - openMenu(); - expect(screen.getByTestId('dropdown-selectable-info')).toBeInTheDocument(); - expect(screen.getByText('Test')).toBeInTheDocument(); -}); - -test('should render the menu items', async () => { - await asyncRender(mockedProps); - openMenu(); - expect(screen.getAllByRole('menuitem')).toHaveLength(2); - expect(screen.getByText('vertical')).toBeInTheDocument(); - expect(screen.getByText('horizontal')).toBeInTheDocument(); -}); - -test('should not render any selected menu item', async () => { - await asyncRender(mockedProps); - openMenu(); - expect(screen.getAllByRole('menuitem')).toHaveLength(2); - expect(screen.queryByRole('img', { name: 'check' })).not.toBeInTheDocument(); -}); - -test('should render the selected menu items', async () => { - const selectedProps = { - ...mockedProps, - selectedKeys: ['vertical'], - }; - await asyncRender(selectedProps); - openMenu(); - expect(screen.getByRole('img', { name: 'check' })).toBeInTheDocument(); -}); diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx deleted file mode 100644 index 8d791929d..000000000 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ /dev/null @@ -1,177 +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 { 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'; -import { Menu, MenuProps } from 'src/components/Menu'; - -const { SubMenu } = Menu; - -type SubMenuItemProps = { key: string; label: string | ReactNode }; - -export interface DropDownSelectableProps extends Pick { - ref?: RefObject; - icon: ReactNode; - info?: string; - menuItems: { - key: string; - label: string | ReactNode; - children?: SubMenuItemProps[]; - divider?: boolean; - }[]; - selectedKeys?: string[]; -} - -const StyledDropdownButton = styled(DropdownButton as FC)` - button.ant-btn:first-of-type { - display: none; - } - > button.ant-btn:nth-of-type(2) { - display: inline-flex; - background-color: transparent !important; - height: unset; - padding: 0; - border: none; - width: auto !important; - - .anticon { - line-height: 0; - } - &:after { - box-shadow: none !important; - } - } -`; - -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}; - padding: ${theme.gridUnit}px ${theme.gridUnit * 3}px ${ - theme.gridUnit - }px ${theme.gridUnit * 3}px; - } - .ant-dropdown-menu-item-selected { - color: ${theme.colors.grayscale.dark1}; - background-color: ${theme.colors.primary.light5}; - } - `} -`; - -const StyleMenuItem = styled(Menu.Item)<{ divider?: boolean }>` - display: flex; - justify-content: space-between; - > span { - width: 100%; - } - border-bottom: ${({ divider, theme }) => - divider ? `1px solid ${theme.colors.grayscale.light3};` : 'none;'}; -`; - -const StyleSubmenuItem = styled.div` - display: flex; - justify-content: space-between; - width: 100%; - > div { - flex-grow: 1; - } -`; - -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) => ( - - - {label} - {selectedKeys?.includes(key) && ( - - )} - - - ), - [selectedKeys, theme.colors.primary.base], - ); - - 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) - ), - )} - - - ), - [selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect], - ); - - return ( - - ); -}; diff --git a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx index 38fc7cbe9..61d2c70a3 100644 --- a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx +++ b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx @@ -17,7 +17,7 @@ * under the License. */ import { action } from '@storybook/addon-actions'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; import FaveStar from 'src/components/FaveStar'; @@ -70,8 +70,8 @@ export const SupersetListViewCard = ({ saveFaveStar={action('saveFaveStar')} isStarred={isStarred} /> - ( Delete @@ -80,10 +80,10 @@ export const SupersetListViewCard = ({ Edit - } + )} > - +
} /> diff --git a/superset-frontend/src/components/Menu/index.tsx b/superset-frontend/src/components/Menu/index.tsx index ff2c1c0b6..5d0f6e0dc 100644 --- a/superset-frontend/src/components/Menu/index.tsx +++ b/superset-frontend/src/components/Menu/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { addAlpha, styled } from '@superset-ui/core'; +import { styled } from '@superset-ui/core'; import { ReactElement } from 'react'; import { Menu as AntdMenu } from 'antd-v5'; import { MenuProps as AntdMenuProps } from 'antd-v5/es/menu'; @@ -73,23 +73,11 @@ const StyledMenuItem = styled(AntdMenu.Item)` } `; -// 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; - } - &.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)}; - } - `} + &.antd5-menu-horizontal { + background-color: inherit; + border-bottom: 1px solid transparent; + } `; const StyledNav = styled(AntdMenu)` @@ -145,11 +133,6 @@ const StyledSubMenu = styled(AntdMenu.SubMenu)` transition: all ${({ theme }) => theme.transitionTiming}s; } } - - .ant-dropdown-menu-submenu-arrow:before, - .ant-dropdown-menu-submenu-arrow:after { - content: none !important; - } `; export type MenuMode = AntdMenuProps['mode']; diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx index b84e67b31..68c33d78c 100644 --- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx @@ -18,7 +18,7 @@ */ import { ReactNode, ReactElement } from 'react'; import { css, SupersetTheme, t, useTheme } from '@superset-ui/core'; -import { AntdDropdown, AntdDropdownProps } from 'src/components'; +import { Dropdown, DropdownProps } from 'src/components/Dropdown'; import { TooltipPlacement } from 'src/components/Tooltip'; import { DynamicEditableTitle, @@ -116,7 +116,7 @@ export type PageHeaderWithActionsProps = { titlePanelAdditionalItems: ReactNode; rightPanelAdditionalItems: ReactNode; additionalActionsMenu: ReactElement; - menuDropdownProps: Omit; + menuDropdownProps: Omit; tooltipProps?: { text?: string; placement?: TooltipPlacement; @@ -155,9 +155,9 @@ export const PageHeaderWithActions = ({ {rightPanelAdditionalItems}
{showMenuDropdown && ( - additionalActionsMenu} {...menuDropdownProps} > - + )}
diff --git a/superset-frontend/src/components/PopoverDropdown/index.tsx b/superset-frontend/src/components/PopoverDropdown/index.tsx index 9ad507d5f..41812ea13 100644 --- a/superset-frontend/src/components/PopoverDropdown/index.tsx +++ b/superset-frontend/src/components/PopoverDropdown/index.tsx @@ -19,7 +19,7 @@ import { Key } from 'react'; import cx from 'classnames'; import { styled, useTheme } from '@superset-ui/core'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; @@ -89,10 +89,10 @@ const PopoverDropdown = (props: PopoverDropdownProps) => { const theme = useTheme(); const selected = options.find(opt => opt.value === value); return ( - ( onChange(key)}> {options.map(option => ( { ))} - } + )} >
{selected && renderButton(selected)} @@ -115,7 +115,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => { css={{ marginTop: theme.gridUnit * 0.5 }} />
-
+ ); }; diff --git a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx index 1c3127504..5b404ccbd 100644 --- a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx +++ b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx @@ -18,7 +18,7 @@ */ import { useState, useEffect } from 'react'; import { styled } from '@superset-ui/core'; -import { Dropdown, IconOrientation } from 'src/components/Dropdown'; +import { MenuDotsDropdown, IconOrientation } from 'src/components/Dropdown'; import { Menu, MenuProps } from 'src/components/Menu'; /** @@ -126,7 +126,7 @@ export function ActionCell(props: ActionCellProps) { setVisible(flag); }; return ( - ( - <> - - + ); diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts index cb949ef5c..9d9ad65eb 100644 --- a/superset-frontend/src/components/index.ts +++ b/superset-frontend/src/components/index.ts @@ -56,7 +56,6 @@ export { Card as AntdCard, Checkbox as AntdCheckbox, Collapse as AntdCollapse, - Dropdown as AntdDropdown, Form as AntdForm, Input as AntdInput, Select as AntdSelect, @@ -67,5 +66,4 @@ export { // Exported types export type { FormInstance } from 'antd/lib/form'; -export type { DropDownProps as AntdDropdownProps } from 'antd/lib/dropdown'; export type { RadioChangeEvent } from 'antd/lib/radio'; diff --git a/superset-frontend/src/dashboard/components/CssEditor/index.tsx b/superset-frontend/src/dashboard/components/CssEditor/index.tsx index e2748c8b0..2b9b99070 100644 --- a/superset-frontend/src/dashboard/components/CssEditor/index.tsx +++ b/superset-frontend/src/dashboard/components/CssEditor/index.tsx @@ -17,8 +17,8 @@ * under the License. */ import { Key, ReactNode, PureComponent } from 'react'; +import { Dropdown } from 'src/components/Dropdown'; import rison from 'rison'; -import { AntdDropdown } from 'src/components'; import { Menu } from 'src/components/Menu'; import Button from 'src/components/Button'; import { t, styled, SupersetClient } from '@superset-ui/core'; @@ -115,9 +115,9 @@ class CssEditor extends PureComponent {
); return ( - + menu} placement="bottomRight"> - +
); } return null; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx index 1a0b73165..877d9203a 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx @@ -53,9 +53,6 @@ jest.mock('src/components/Select/Select', () => () => ( jest.mock('src/components/Select/AsyncSelect', () => () => (
)); -jest.mock('src/dashboard/components/Header/HeaderActionsDropdown', () => () => ( -
-)); jest.mock('src/components/PageHeaderWithActions', () => ({ PageHeaderWithActions: () => (
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx deleted file mode 100644 index 090f12a2b..000000000 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ /dev/null @@ -1,260 +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 { shallow } from 'enzyme'; -import sinon from 'sinon'; -import { render, screen } from 'spec/helpers/testing-library'; -import userEvent from '@testing-library/user-event'; -import fetchMock from 'fetch-mock'; -import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; -import injectCustomCss from 'src/dashboard/util/injectCustomCss'; -import { HeaderActionsDropdown } from '.'; - -const createProps = (): HeaderDropdownProps => ({ - addSuccessToast: jest.fn(), - addDangerToast: jest.fn(), - customCss: '.ant-menu {margin-left: 100px;}', - dashboardId: 1, - dashboardInfo: { - id: 1, - dash_edit_perm: true, - dash_save_perm: true, - userId: '1', - metadata: {}, - common: { - conf: { - DASHBOARD_AUTO_REFRESH_INTERVALS: [ - [0, "Don't refresh"], - [10, '10 seconds'], - ], - }, - }, - }, - dashboardTitle: 'Title', - editMode: false, - expandedSlices: {}, - forceRefreshAllCharts: jest.fn(), - hasUnsavedChanges: false, - isLoading: false, - layout: {}, - onChange: jest.fn(), - onSave: jest.fn(), - refreshFrequency: 200, - setRefreshFrequency: jest.fn(), - shouldPersistRefreshFrequency: false, - showPropertiesModal: jest.fn(), - startPeriodicRender: jest.fn(), - updateCss: jest.fn(), - userCanEdit: false, - userCanSave: false, - userCanShare: false, - userCanCurate: false, - lastModifiedTime: 0, - isDropdownVisible: true, - setIsDropdownVisible: jest.fn(), - directPathToChild: [], - manageEmbedded: jest.fn(), - dataMask: {}, - logEvent: jest.fn(), - refreshLimit: 0, - refreshWarning: '', -}); - -const editModeOnProps = { - ...createProps(), - editMode: true, -}; - -const editModeOnWithFilterScopesProps = { - ...editModeOnProps, - dashboardInfo: { - ...editModeOnProps.dashboardInfo, - metadata: { - filter_scopes: { - '1': { scopes: ['ROOT_ID'], immune: [] }, - }, - }, - }, -}; - -const guestUserProps = { - ...createProps(), - dashboardInfo: { - ...createProps().dashboardInfo, - userId: undefined, - }, -}; - -function setup(props: HeaderDropdownProps) { - return render( -
- -
, - { useRedux: true }, - ); -} - -fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); - -test('should render', () => { - const mockedProps = createProps(); - const { container } = setup(mockedProps); - expect(container).toBeInTheDocument(); -}); - -test('should render the Download dropdown button when not in edit mode', () => { - const mockedProps = createProps(); - setup(mockedProps); - expect( - screen.getByRole('menuitem', { name: 'Download' }), - ).toBeInTheDocument(); -}); - -test('should render the menu items', async () => { - const mockedProps = createProps(); - setup(mockedProps); - expect(screen.getAllByRole('menuitem')).toHaveLength(4); - expect(screen.getByText('Refresh dashboard')).toBeInTheDocument(); - expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); - expect(screen.getByText('Enter fullscreen')).toBeInTheDocument(); - expect(screen.getByText('Download')).toBeInTheDocument(); -}); - -test('should render the menu items in edit mode', async () => { - setup(editModeOnProps); - expect(screen.getAllByRole('menuitem')).toHaveLength(4); - expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); - expect(screen.getByText('Edit properties')).toBeInTheDocument(); - expect(screen.getByText('Edit CSS')).toBeInTheDocument(); - expect(screen.getByText('Download')).toBeInTheDocument(); -}); - -test('should render the menu items in Embedded mode', async () => { - setup(guestUserProps); - expect(screen.getAllByRole('menuitem')).toHaveLength(3); - expect(screen.getByText('Refresh dashboard')).toBeInTheDocument(); - expect(screen.getByText('Download')).toBeInTheDocument(); - expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); -}); - -test('should not render filter mapping in edit mode if explicit filter scopes undefined', async () => { - setup(editModeOnProps); - expect(screen.queryByText('Set filter mapping')).not.toBeInTheDocument(); -}); - -test('should render filter mapping in edit mode if explicit filter scopes defined', async () => { - setup(editModeOnWithFilterScopesProps); - expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); -}); - -test('should show the share actions', async () => { - const mockedProps = createProps(); - const canShareProps = { - ...mockedProps, - userCanShare: true, - }; - setup(canShareProps); - - expect(screen.getByText('Share')).toBeInTheDocument(); -}); - -test('should render the "Save as" menu item when user can save', async () => { - const mockedProps = createProps(); - const canSaveProps = { - ...mockedProps, - userCanSave: true, - }; - setup(canSaveProps); - expect(screen.getByText('Save as')).toBeInTheDocument(); -}); - -test('should NOT render the "Save as" menu item when user cannot save', async () => { - const mockedProps = createProps(); - setup(mockedProps); - expect(screen.queryByText('Save as')).not.toBeInTheDocument(); -}); - -test('should render the "Refresh dashboard" menu item as disabled when loading', async () => { - const mockedProps = createProps(); - const loadingProps = { - ...mockedProps, - isLoading: true, - }; - setup(loadingProps); - expect(screen.getByText('Refresh dashboard').parentElement).toHaveClass( - 'ant-menu-item-disabled', - ); -}); - -test('should NOT render the "Refresh dashboard" menu item as disabled', async () => { - const mockedProps = createProps(); - setup(mockedProps); - expect(screen.getByText('Refresh dashboard')).not.toHaveClass( - 'ant-menu-item-disabled', - ); -}); - -test('should render with custom css', () => { - const mockedProps = createProps(); - const { customCss } = mockedProps; - setup(mockedProps); - injectCustomCss(customCss); - expect(screen.getByTestId('header-actions-menu')).toHaveStyle( - 'margin-left: 100px', - ); -}); - -test('should refresh the charts', async () => { - const mockedProps = createProps(); - setup(mockedProps); - userEvent.click(screen.getByText('Refresh dashboard')); - expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1); - expect(mockedProps.addSuccessToast).toHaveBeenCalledTimes(1); -}); - -test('should show the properties modal', async () => { - setup(editModeOnProps); - userEvent.click(screen.getByText('Edit properties')); - expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1); -}); - -describe('UNSAFE_componentWillReceiveProps', () => { - let wrapper: any; - - const mockedProps = createProps(); - const props = { ...mockedProps, customCss: '' }; - beforeEach(() => { - wrapper = shallow(); - wrapper.setState({ css: props.customCss }); - sinon.spy(wrapper.instance(), 'setState'); - }); - - afterEach(() => { - wrapper.instance().setState.restore(); - }); - - it('css should update state and inject custom css', () => { - wrapper.instance().UNSAFE_componentWillReceiveProps({ - ...props, - customCss: mockedProps.customCss, - }); - expect(wrapper.instance().setState.calledOnce).toBe(true); - const stateKeys = Object.keys(wrapper.instance().setState.lastCall.args[0]); - expect(stateKeys).toContain('css'); - }); -}); diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 5ce86cc52..1324d785c 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -40,7 +40,6 @@ import { Button } from 'src/components/'; import { findPermission } from 'src/utils/findPermission'; import { Tooltip } from 'src/components/Tooltip'; import { safeStringify } from 'src/utils/safeStringify'; -import ConnectedHeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown'; import PublishedStatus from 'src/dashboard/components/PublishedStatus'; import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; @@ -53,6 +52,9 @@ import { import setPeriodicRunner, { stopPeriodicRender, } from 'src/dashboard/util/setPeriodicRunner'; +import ReportModal from 'src/features/reports/ReportModal'; +import DeleteModal from 'src/components/DeleteModal'; +import { deleteActiveReport } from 'src/features/reports/ReportModal/actions'; import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions'; import DashboardEmbedModal from '../EmbeddedModal'; import OverwriteConfirm from '../OverwriteConfirm'; @@ -88,6 +90,7 @@ import { dashboardInfoChanged } from '../../actions/dashboardInfo'; import isDashboardLoading from '../../util/isDashboardLoading'; import { useChartIds } from '../../util/charts/useChartIds'; import { useDashboardMetadataBar } from './useDashboardMetadataBar'; +import { useHeaderActionsMenu } from './useHeaderActionsDropdownMenu'; const extensionsRegistry = getExtensionsRegistry(); @@ -160,8 +163,9 @@ const Header = () => { const [emphasizeUndo, setEmphasizeUndo] = useState(false); const [emphasizeRedo, setEmphasizeRedo] = useState(false); const [showingPropertiesModal, setShowingPropertiesModal] = useState(false); - const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [showingEmbedModal, setShowingEmbedModal] = useState(false); + const [showingReportModal, setShowingReportModal] = useState(false); + const [currentReportDeleting, setCurrentReportDeleting] = useState(null); const dashboardInfo = useSelector(state => state.dashboardInfo); const layout = useSelector(state => state.dashboardLayout.present); const undoLength = useSelector(state => state.dashboardLayout.past.length); @@ -348,10 +352,6 @@ const Header = () => { [boundActionCreators, dashboardTitle], ); - const setDropdownVisible = useCallback(visible => { - setIsDropdownVisible(visible); - }, []); - const handleCtrlY = useCallback(() => { boundActionCreators.onRedo(); setEmphasizeRedo(true); @@ -475,6 +475,14 @@ const Header = () => { setShowingEmbedModal(false); }, []); + const showReportModal = useCallback(() => { + setShowingReportModal(true); + }, []); + + const hideReportModal = useCallback(() => { + setShowingReportModal(false); + }, []); + const metadataBar = useDashboardMetadataBar(dashboardInfo); const userCanEdit = @@ -689,92 +697,47 @@ const Header = () => { ], ); - const menuDropdownProps = useMemo( - () => ({ - getPopupContainer: triggerNode => - triggerNode.closest('.header-with-actions'), - visible: isDropdownVisible, - onVisibleChange: setDropdownVisible, - }), - [isDropdownVisible, setDropdownVisible], - ); - - const additionalActionsMenu = useMemo( - () => ( - - ), - [ - actualLastModifiedTime, - boundActionCreators.addDangerToast, - boundActionCreators.addSuccessToast, - boundActionCreators.logEvent, - boundActionCreators.onChange, - boundActionCreators.onSave, - boundActionCreators.setRefreshFrequency, - boundActionCreators.updateCss, - colorNamespace, - colorScheme, - customCss, - dashboardInfo, - dashboardTitle, - dataMask, - editMode, - expandedSlices, - forceRefresh, - hasUnsavedChanges, - isDropdownVisible, - isLoading, - layout, - refreshFrequency, - refreshLimit, - refreshWarning, - setDropdownVisible, - shouldPersistRefreshFrequency, - showEmbedModal, - showPropertiesModal, - startPeriodicRender, - userCanCurate, - userCanEdit, - userCanSaveAs, - userCanShare, - ], - ); + const handleReportDelete = async report => { + await dispatch(deleteActiveReport(report)); + setCurrentReportDeleting(null); + }; + const [menu, isDropdownVisible, setIsDropdownVisible] = useHeaderActionsMenu({ + addSuccessToast: boundActionCreators.addSuccessToast, + addDangerToast: boundActionCreators.addDangerToast, + dashboardInfo, + dashboardId: dashboardInfo.id, + dashboardTitle, + dataMask, + layout, + expandedSlices, + customCss, + colorNamespace, + colorScheme, + onSave: boundActionCreators.onSave, + onChange: boundActionCreators.onChange, + forceRefreshAllCharts: forceRefresh, + startPeriodicRender, + refreshFrequency, + shouldPersistRefreshFrequency, + setRefreshFrequency: boundActionCreators.setRefreshFrequency, + updateCss: boundActionCreators.updateCss, + editMode, + hasUnsavedChanges, + userCanEdit, + userCanShare, + userCanSave: userCanSaveAs, + userCanCurate, + isLoading, + showReportModal, + showPropertiesModal, + setCurrentReportDeleting, + manageEmbedded: showEmbedModal, + refreshLimit, + refreshWarning, + lastModifiedTime: actualLastModifiedTime, + logEvent: boundActionCreators.logEvent, + }); return (
{ faveStarProps={faveStarProps} titlePanelAdditionalItems={titlePanelAdditionalItems} rightPanelAdditionalItems={rightPanelAdditionalItems} - menuDropdownProps={menuDropdownProps} - additionalActionsMenu={additionalActionsMenu} + menuDropdownProps={{ + open: isDropdownVisible, + onOpenChange: setIsDropdownVisible, + }} + additionalActionsMenu={menu} showFaveStar={user?.userId && dashboardInfo?.id} showTitlePanelItems /> @@ -806,6 +772,32 @@ const Header = () => { /> )} + + + {currentReportDeleting && ( + { + if (currentReportDeleting) { + handleReportDelete(currentReportDeleting); + } + }} + onHide={() => setCurrentReportDeleting(null)} + open + title={t('Delete Report?')} + /> + )} + {userCanCurate && ( @@ -817,7 +809,7 @@ const Header = () => { )} void; - setIsDropdownVisible: (visible: boolean) => void; - isDropdownVisible: boolean; refreshLimit: number; refreshWarning: string; directPathToChild: string[]; + showReportModal: () => void; + setCurrentReportDeleting: (alert: AlertObject | null) => void; } export interface HeaderProps { diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx b/superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx similarity index 53% rename from superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx rename to superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx index 6bc712f6b..420e91e55 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx +++ b/superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { PureComponent } from 'react'; -import { isEmpty } from 'lodash'; -import { connect } from 'react-redux'; -import { t } from '@superset-ui/core'; +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { Menu } from 'src/components/Menu'; +import { t } from '@superset-ui/core'; +import { isEmpty } from 'lodash'; import { URL_PARAMS } from 'src/constants'; import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; import DownloadMenuItems from 'src/dashboard/components/menu/DownloadMenuItems'; @@ -37,158 +37,156 @@ import { getUrlParam } from 'src/utils/urlUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; -const mapStateToProps = (state: RootState) => ({ - directPathToChild: state.dashboardState.directPathToChild, -}); - -interface HeaderActionsDropdownState { - css: string; - showReportSubMenu: boolean | null; -} - -export class HeaderActionsDropdown extends PureComponent< - HeaderDropdownProps, - HeaderActionsDropdownState -> { - static defaultProps = { - colorNamespace: undefined, - colorScheme: undefined, - refreshLimit: 0, - refreshWarning: null, - }; - - constructor(props: HeaderDropdownProps) { - super(props); - this.state = { - css: props.customCss || '', - showReportSubMenu: null, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: HeaderDropdownProps) { - if (this.props.customCss !== nextProps.customCss) { - this.setState({ css: nextProps.customCss }, () => { - injectCustomCss(nextProps.customCss); - }); +export const useHeaderActionsMenu = ({ + customCss, + dashboardId, + dashboardInfo, + refreshFrequency, + shouldPersistRefreshFrequency, + editMode, + colorNamespace, + colorScheme, + layout, + expandedSlices, + onSave, + userCanEdit, + userCanShare, + userCanSave, + userCanCurate, + isLoading, + refreshLimit, + refreshWarning, + lastModifiedTime, + addSuccessToast, + addDangerToast, + forceRefreshAllCharts, + showPropertiesModal, + showReportModal, + manageEmbedded, + onChange, + updateCss, + startPeriodicRender, + setRefreshFrequency, + dashboardTitle, + logEvent, + setCurrentReportDeleting, +}: HeaderDropdownProps) => { + const [css, setCss] = useState(customCss || ''); + const [showReportSubMenu, setShowReportSubMenu] = useState( + null, + ); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const directPathToChild = useSelector( + (state: RootState) => state.dashboardState.directPathToChild, + ); + useEffect(() => { + if (customCss !== css) { + setCss(customCss || ''); + injectCustomCss(customCss); } - } + }, [css, customCss]); - setShowReportSubMenu = (show: boolean) => { - this.setState({ showReportSubMenu: show }); - }; - - changeCss = (css: string) => { - this.props.onChange(); - this.props.updateCss(css); - }; - - changeRefreshInterval = (refreshInterval: number, isPersistent: boolean) => { - this.props.setRefreshFrequency(refreshInterval, isPersistent); - this.props.startPeriodicRender(refreshInterval * 1000); - }; - - handleMenuClick = ({ key }: Record) => { - switch (key) { - case MenuKeys.RefreshDashboard: - this.props.forceRefreshAllCharts(); - this.props.addSuccessToast(t('Refreshing charts')); - break; - case MenuKeys.EditProperties: - this.props.showPropertiesModal(); - break; - case MenuKeys.ToggleFullscreen: { - const url = getDashboardUrl({ - pathname: window.location.pathname, - filters: getActiveFilters(), - hash: window.location.hash, - standalone: getUrlParam(URL_PARAMS.standalone), - }); - window.location.replace(url); - break; + const handleMenuClick = useCallback( + ({ key }: { key: string }) => { + switch (key) { + case MenuKeys.RefreshDashboard: + forceRefreshAllCharts(); + addSuccessToast(t('Refreshing charts')); + break; + case MenuKeys.EditProperties: + showPropertiesModal(); + break; + case MenuKeys.ToggleFullscreen: { + const url = getDashboardUrl({ + pathname: window.location.pathname, + filters: getActiveFilters(), + hash: window.location.hash, + standalone: getUrlParam(URL_PARAMS.standalone), + }); + window.location.replace(url); + break; + } + case MenuKeys.ManageEmbedded: + manageEmbedded(); + break; + default: + break; } - case MenuKeys.ManageEmbedded: { - this.props.manageEmbedded(); - break; - } - default: - break; - } - }; - - render() { - const { - dashboardTitle, - dashboardId, - dashboardInfo, - refreshFrequency, - shouldPersistRefreshFrequency, - editMode, - customCss, - colorNamespace, - colorScheme, - layout, - expandedSlices, - onSave, - userCanEdit, - userCanShare, - userCanSave, - userCanCurate, - isLoading, - refreshLimit, - refreshWarning, - lastModifiedTime, + setIsDropdownVisible(false); + }, + [ + forceRefreshAllCharts, addSuccessToast, - addDangerToast, - setIsDropdownVisible, - isDropdownVisible, - directPathToChild, - ...rest - } = this.props; + showPropertiesModal, + manageEmbedded, + ], + ); - const emailTitle = t('Superset dashboard'); - const emailSubject = `${emailTitle} ${dashboardTitle}`; - const emailBody = t('Check out this dashboard: '); + const changeCss = useCallback( + (newCss: string) => { + onChange(); + updateCss(newCss); + }, + [onChange, updateCss], + ); + const changeRefreshInterval = useCallback( + (refreshInterval: number, isPersistent: boolean) => { + setRefreshFrequency(refreshInterval, isPersistent); + startPeriodicRender(refreshInterval * 1000); + }, + [setRefreshFrequency, startPeriodicRender], + ); + + const emailSubject = useMemo( + () => `${t('Superset dashboard')} ${dashboardTitle}`, + [dashboardTitle], + ); + + const url = useMemo( + () => + getDashboardUrl({ + pathname: window.location.pathname, + filters: getActiveFilters(), + hash: window.location.hash, + }), + [], + ); + + const dashboardComponentId = useMemo( + () => [...(directPathToChild || [])].pop(), + [directPathToChild], + ); + + const menu = useMemo(() => { const isEmbedded = !dashboardInfo?.userId; - - const url = getDashboardUrl({ - pathname: window.location.pathname, - filters: getActiveFilters(), - hash: window.location.hash, - }); - const refreshIntervalOptions = dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS; - const dashboardComponentId = [...(directPathToChild || [])].pop(); - return ( - + {!editMode && ( {t('Refresh dashboard')} )} {!editMode && !isEmbedded && ( - + {getUrlParam(URL_PARAMS.standalone) ? t('Exit fullscreen') : t('Enter fullscreen')} )} {editMode && ( - + {t('Edit properties')} )} @@ -196,8 +194,8 @@ export class HeaderActionsDropdown extends PureComponent< {t('Edit CSS')}
} - initialCss={this.state.css} - onChange={this.changeCss} + initialCss={css} + onChange={changeCss} addDangerToast={addDangerToast} /> @@ -228,29 +226,26 @@ export class HeaderActionsDropdown extends PureComponent< /> )} - - - + pdfMenuItemTitle={t('Export to PDF')} + imageMenuItemTitle={t('Download as Image')} + dashboardTitle={dashboardTitle} + dashboardId={dashboardId} + logEvent={logEvent} + /> {userCanShare && ( )} {!editMode && userCanCurate && ( - + {t('Embed dashboard')} )} {!editMode ? ( - this.state.showReportSubMenu ? ( + showReportSubMenu ? ( <> - - - + ) : ( - - - + ) ) : null} {editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && ( @@ -302,14 +291,13 @@ export class HeaderActionsDropdown extends PureComponent< /> )} - {t('Set auto-refresh interval')}
} @@ -317,7 +305,18 @@ export class HeaderActionsDropdown extends PureComponent< ); - } -} + }, [ + css, + showReportSubMenu, + isDropdownVisible, + directPathToChild, + handleMenuClick, + changeCss, + changeRefreshInterval, + emailSubject, + url, + dashboardComponentId, + ]); -export default connect(mapStateToProps)(HeaderActionsDropdown); + return [menu, isDropdownVisible, setIsDropdownVisible]; +}; diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx index a683dfa2b..3a9878f4f 100644 --- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx +++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx @@ -22,16 +22,16 @@ import userEvent from '@testing-library/user-event'; 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'; +import { useHeaderActionsMenu } from './Header/useHeaderActionsDropdownMenu'; const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), customCss: - '.header-with-actions .right-button-panel .ant-dropdown-trigger{margin-left: 100px;}', + '.header-with-actions .right-button-panel .antd5-dropdown-trigger{margin-left: 100px;}', dashboardId: 1, dashboardInfo: { id: 1, @@ -85,12 +85,22 @@ const editModeOnProps = { }; const mockStore = configureStore([thunk]); -const store = mockStore({}); +const store = mockStore({ + dashboardState: { + dashboardInfo: createProps().dashboardInfo, + }, +}); + +const HeaderActionsMenu = (props: any) => { + const [menu] = useHeaderActionsMenu(props); + + return <>{menu}; +}; const setup = (overrides?: any) => (
- +
); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index e1f20b550..7c813809c 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -112,11 +112,13 @@ const renderWrapper = ( }); }; +const openMenu = () => { + userEvent.click(screen.getByRole('button', { name: 'More Options' })); +}; + test('Should render', () => { renderWrapper(); - expect( - screen.getByRole('button', { name: 'More Options' }), - ).toBeInTheDocument(); + openMenu(); expect(screen.getByTestId(`slice_${SLICE_ID}-menu`)).toBeInTheDocument(); }); @@ -143,6 +145,7 @@ test('Should render default props', () => { delete props.isExpanded; renderWrapper(props); + openMenu(); expect(screen.getByText('Enter fullscreen')).toBeInTheDocument(); expect(screen.getByText('Force refresh')).toBeInTheDocument(); expect(screen.getByText('Show chart description')).toBeInTheDocument(); @@ -159,6 +162,7 @@ test('Should render default props', () => { test('Should "export to CSV"', async () => { const props = createProps(); renderWrapper(props); + openMenu(); expect(props.exportCSV).toHaveBeenCalledTimes(0); userEvent.hover(screen.getByText('Download')); userEvent.click(await screen.findByText('Export to .CSV')); @@ -169,6 +173,7 @@ test('Should "export to CSV"', async () => { test('Should "export to Excel"', async () => { const props = createProps(); renderWrapper(props); + openMenu(); expect(props.exportXLSX).toHaveBeenCalledTimes(0); userEvent.hover(screen.getByText('Download')); userEvent.click(await screen.findByText('Export to Excel')); @@ -182,6 +187,7 @@ test('Export full CSV is under featureflag', async () => { }; const props = createProps(VizType.Table); renderWrapper(props); + openMenu(); userEvent.hover(screen.getByText('Download')); expect(await screen.findByText('Export to .CSV')).toBeInTheDocument(); expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument(); @@ -193,6 +199,7 @@ test('Should "export full CSV"', async () => { }; const props = createProps(VizType.Table); renderWrapper(props); + openMenu(); expect(props.exportFullCSV).toHaveBeenCalledTimes(0); userEvent.hover(screen.getByText('Download')); userEvent.click(await screen.findByText('Export to full .CSV')); @@ -205,6 +212,7 @@ test('Should not show export full CSV if report is not table', async () => { [FeatureFlag.AllowFullCsvExport]: true, }; renderWrapper(); + openMenu(); userEvent.hover(screen.getByText('Download')); expect(await screen.findByText('Export to .CSV')).toBeInTheDocument(); expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument(); @@ -216,6 +224,7 @@ test('Export full Excel is under featureflag', async () => { }; const props = createProps(VizType.Table); renderWrapper(props); + openMenu(); userEvent.hover(screen.getByText('Download')); expect(await screen.findByText('Export to Excel')).toBeInTheDocument(); expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument(); @@ -227,6 +236,7 @@ test('Should "export full Excel"', async () => { }; const props = createProps(VizType.Table); renderWrapper(props); + openMenu(); expect(props.exportFullXLSX).toHaveBeenCalledTimes(0); userEvent.hover(screen.getByText('Download')); userEvent.click(await screen.findByText('Export to full Excel')); @@ -239,6 +249,7 @@ test('Should not show export full Excel if report is not table', async () => { [FeatureFlag.AllowFullCsvExport]: true, }; renderWrapper(); + openMenu(); userEvent.hover(screen.getByText('Download')); expect(await screen.findByText('Export to Excel')).toBeInTheDocument(); expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument(); @@ -247,6 +258,7 @@ test('Should not show export full Excel if report is not table', async () => { test('Should "Show chart description"', () => { const props = createProps(); renderWrapper(props); + openMenu(); expect(props.toggleExpandSlice).toHaveBeenCalledTimes(0); userEvent.click(screen.getByText('Show chart description')); expect(props.toggleExpandSlice).toHaveBeenCalledTimes(1); @@ -256,6 +268,7 @@ test('Should "Show chart description"', () => { test('Should "Force refresh"', () => { const props = createProps(); renderWrapper(props); + openMenu(); expect(props.forceRefresh).toHaveBeenCalledTimes(0); userEvent.click(screen.getByText('Force refresh')); expect(props.forceRefresh).toHaveBeenCalledTimes(1); @@ -266,6 +279,7 @@ test('Should "Force refresh"', () => { test('Should "Enter fullscreen"', () => { const props = createProps(); renderWrapper(props); + openMenu(); expect(props.handleToggleFullSize).toHaveBeenCalledTimes(0); userEvent.click(screen.getByText('Enter fullscreen')); @@ -278,6 +292,7 @@ test('Drill to detail modal is under featureflag', () => { }; const props = createProps(); renderWrapper(props); + openMenu(); expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument(); }); @@ -293,6 +308,7 @@ test('Should show "Drill to detail" with `can_explore` & `can_samples` perms', ( ['can_explore', 'Superset'], ], }); + openMenu(); expect(screen.getByText('Drill to detail')).toBeInTheDocument(); }); @@ -311,6 +327,7 @@ test('Should show "Drill to detail" with `can_drill` & `can_samples` perms', () ['can_drill', 'Dashboard'], ], }); + openMenu(); expect(screen.getByText('Drill to detail')).toBeInTheDocument(); }); @@ -329,6 +346,7 @@ test('Should show "Drill to detail" with both `canexplore` + `can_drill` & `can_ ['can_drill', 'Dashboard'], ], }); + openMenu(); expect(screen.getByText('Drill to detail')).toBeInTheDocument(); }); @@ -344,6 +362,7 @@ test('Should not show "Drill to detail" with neither of required perms', () => { renderWrapper(props, { Admin: [['invalid_permission', 'Dashboard']], }); + openMenu(); expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument(); }); @@ -359,6 +378,7 @@ test('Should not show "Drill to detail" only `can_dril` perm', () => { renderWrapper(props, { Admin: [['can_drill', 'Dashboard']], }); + openMenu(); expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument(); }); @@ -371,6 +391,7 @@ test('Should show "View query"', () => { renderWrapper(props, { Admin: [['can_view_query', 'Dashboard']], }); + openMenu(); expect(screen.getByText('View query')).toBeInTheDocument(); }); @@ -383,6 +404,7 @@ test('Should not show "View query"', () => { renderWrapper(props, { Admin: [['invalid_permission', 'Dashboard']], }); + openMenu(); expect(screen.queryByText('View query')).not.toBeInTheDocument(); }); @@ -395,6 +417,7 @@ test('Should show "View as table"', () => { renderWrapper(props, { Admin: [['can_view_chart_as_table', 'Dashboard']], }); + openMenu(); expect(screen.getByText('View as table')).toBeInTheDocument(); }); @@ -407,6 +430,7 @@ test('Should not show "View as table"', () => { renderWrapper(props, { Admin: [['invalid_permission', 'Dashboard']], }); + openMenu(); expect(screen.queryByText('View as table')).not.toBeInTheDocument(); }); @@ -423,5 +447,6 @@ test('Should not show the "Edit chart" button', () => { ['can_view_chart_as_table', 'Dashboard'], ], }); + openMenu(); expect(screen.queryByText('Edit chart')).not.toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 12479d02f..1c31b6752 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -55,6 +55,7 @@ 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 Button from 'src/components/Button'; import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal'; import { ViewResultsModalTrigger } from './ViewResultsModalTrigger'; @@ -158,9 +159,8 @@ const SliceHeaderControls = ( props: SliceHeaderControlsPropsWithRouter | SliceHeaderControlsProps, ) => { const [drillModalIsOpen, setDrillModalIsOpen] = useState(false); - const [selectedKeys, setSelectedKeys] = useState([]); // setting openKeys undefined falls back to uncontrolled behaviour - const [openKeys, setOpenKeys] = useState(undefined); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [openScopingModal, scopingModal] = useCrossFiltersScopingModal( props.slice.slice_id, ); @@ -241,7 +241,7 @@ const SliceHeaderControls = ( // menu closes with a delay, we need to hide it manually, // so that we don't capture it on the screenshot const menu = document.querySelector( - '.ant-dropdown:not(.ant-dropdown-hidden)', + '.antd5-dropdown:not(.antd5-dropdown-hidden)', ) as HTMLElement; if (menu) { menu.style.visibility = 'hidden'; @@ -284,6 +284,7 @@ const SliceHeaderControls = ( default: break; } + setIsDropdownVisible(false); }; const { @@ -334,24 +335,12 @@ const SliceHeaderControls = ( animationDuration: '0s', }; - // controlled/uncontrolled behaviour for submenus - const openKeysProps: Record = {}; - if (openKeys) { - openKeysProps.openKeys = openKeys; - } - const menu = ( setSelectedKeys(keys)} - openKeys={openKeys} id={`slice_${slice.slice_id}-menu`} - // submenus must be rendered for handleDropdownNavigation - forceSubMenuRender - {...openKeysProps} + selectable={false} > )} @@ -532,22 +519,17 @@ const SliceHeaderControls = ( overlayStyle={dropdownOverlayStyle} trigger={['click']} placement="bottomRight" - autoFocus - forceRender + open={isDropdownVisible} + onOpenChange={visible => setIsDropdownVisible(visible)} > - css` - display: flex; - align-items: center; - `} + { expect(mockAddDangerToast).toHaveBeenCalledTimes(0); }); - userEvent.click(screen.getByRole('button', { name: 'Download as Image' })); + userEvent.click(screen.getByRole('menuitem', { name: 'Download as Image' })); await waitFor(() => { expect(downloadAsImage).toHaveBeenCalledTimes(1); @@ -68,8 +68,8 @@ test('Should call download image on click', async () => { }); }); -test('Component is rendered with role="button"', async () => { +test('Component is rendered with role="menuitem"', async () => { renderComponent(); - const button = screen.getByRole('button', { name: 'Download as Image' }); + const button = screen.getByRole('menuitem', { name: 'Download as Image' }); expect(button).toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx index 505a9b818..5490d7b80 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx @@ -27,7 +27,6 @@ export default function DownloadAsImage({ text, logEvent, dashboardTitle, - ...rest }: { text: string; dashboardTitle: string; @@ -46,10 +45,13 @@ export default function DownloadAsImage({ }; return ( - -
- {text} -
+ { + onDownloadImage(e.domEvent); + }} + > + {text} ); } diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx index 56916f4b6..ec6652ca4 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx @@ -58,7 +58,7 @@ test('Should call download pdf on click', async () => { expect(mockAddDangerToast).toHaveBeenCalledTimes(0); }); - userEvent.click(screen.getByRole('button', { name: 'Export as PDF' })); + userEvent.click(screen.getByRole('menuitem', { name: 'Export as PDF' })); await waitFor(() => { expect(downloadAsPdf).toHaveBeenCalledTimes(1); @@ -66,8 +66,8 @@ test('Should call download pdf on click', async () => { }); }); -test('Component is rendered with role="button"', async () => { +test('Component is rendered with role="menuitem"', async () => { renderComponent(); - const button = screen.getByRole('button', { name: 'Export as PDF' }); + const button = screen.getByRole('menuitem', { name: 'Export as PDF' }); expect(button).toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx index a07a2e232..47309b842 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx @@ -27,7 +27,6 @@ export default function DownloadAsPdf({ text, logEvent, dashboardTitle, - ...rest }: { text: string; dashboardTitle: string; @@ -46,10 +45,13 @@ export default function DownloadAsPdf({ }; return ( - -
- {text} -
+ { + onDownloadPdf(e.domEvent); + }} + > + {text} ); } 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 0f3049d84..2bd3a2cd9 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx @@ -26,11 +26,13 @@ const createProps = () => ({ dashboardTitle: 'Test Dashboard', logEvent: jest.fn(), dashboardId: 123, + title: 'Download', + submenuKey: 'download', }); const renderComponent = () => { render( - + , { @@ -41,10 +43,6 @@ const renderComponent = () => { test('Should render menu items', () => { renderComponent(); - expect( - screen.getByRole('menuitem', { name: 'Export to PDF' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Download as Image' }), - ).toBeInTheDocument(); + expect(screen.getByText('Export to PDF')).toBeInTheDocument(); + expect(screen.getByText('Download as Image')).toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx index d9ffaaaed..cdb73d5e1 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx @@ -17,17 +17,23 @@ * under the License. */ import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; -import DownloadScreenshot from './DownloadScreenshot'; +import { Menu } from 'src/components/Menu'; +import { useDownloadScreenshot } from 'src/dashboard/hooks/useDownloadScreenshot'; +import { ComponentProps } from 'react'; import { DownloadScreenshotFormat } from './types'; import DownloadAsPdf from './DownloadAsPdf'; import DownloadAsImage from './DownloadAsImage'; -export interface DownloadMenuItemProps { +export interface DownloadMenuItemProps + extends ComponentProps { pdfMenuItemTitle: string; imageMenuItemTitle: string; dashboardTitle: string; logEvent?: Function; dashboardId: number; + title: string; + disabled?: boolean; + submenuKey: string; } const DownloadMenuItems = (props: DownloadMenuItemProps) => { @@ -37,44 +43,45 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => { logEvent, dashboardId, dashboardTitle, + submenuKey, + disabled, + title, ...rest } = props; const isWebDriverScreenshotEnabled = isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) && isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot); + const downloadScreenshot = useDownloadScreenshot(dashboardId, logEvent); + return isWebDriverScreenshotEnabled ? ( - <> - - - + + downloadScreenshot(DownloadScreenshotFormat.PDF)} + > + {pdfMenuItemTitle} + + downloadScreenshot(DownloadScreenshotFormat.PNG)} + > + {imageMenuItemTitle} + + ) : ( - <> + - + ); }; 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 16bbcff9e..f290a1a4b 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx @@ -37,6 +37,7 @@ const createProps = () => ({ emailBody: 'Check out this dashboard: ', dashboardId: DASHBOARD_ID, title: 'Test Dashboard', + submenuKey: 'share', }); const { location } = window; diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx index 6c5468da2..4fb191a06 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RefObject } from 'react'; +import { ComponentProps, RefObject } from 'react'; import copyTextToClipboard from 'src/utils/copy'; import { t, logging } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; @@ -24,7 +24,7 @@ import { getDashboardPermalink } from 'src/utils/urlUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; import { shallowEqual, useSelector } from 'react-redux'; -interface ShareMenuItemProps { +interface ShareMenuItemProps extends ComponentProps { url?: string; copyMenuItemTitle: string; emailMenuItemTitle: string; @@ -38,8 +38,8 @@ interface ShareMenuItemProps { shareByEmailMenuItemRef?: RefObject; selectedKeys?: string[]; setOpenKeys?: Function; - key?: string; title: string; + disabled?: boolean; } const ShareMenuItems = (props: ShareMenuItemProps) => { @@ -52,8 +52,9 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { addSuccessToast, dashboardId, dashboardComponentId, - key, title, + disabled, + ...rest } = props; const { dataMask, activeTabs } = useSelector( (state: RootState) => ({ @@ -96,7 +97,12 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { } return ( - + onCopyLink()}> {copyMenuItemTitle} 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 b073fe655..45a34b110 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 @@ -121,13 +121,13 @@ test('Can enable/disable cross-filtering', async () => { }); await setup(); userEvent.click(screen.getByLabelText('gear')); - const checkbox = screen.getByRole('checkbox'); - expect(checkbox).toBeChecked(); + const initialCheckbox = screen.getByRole('checkbox'); + expect(initialCheckbox).toBeChecked(); - userEvent.click(checkbox); + userEvent.click(initialCheckbox); userEvent.click(screen.getByLabelText('gear')); - expect(checkbox).not.toBeChecked(); + expect(screen.getByRole('checkbox')).not.toBeChecked(); }); test('Popover opens with "Vertical" selected', async () => { @@ -178,19 +178,21 @@ test('On selection change, send request and update checked value', async () => { userEvent.click(screen.getByLabelText('gear')); userEvent.hover(screen.getByText('Orientation of filter bar')); - expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); - expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); + const verticalItem = await screen.findByText('Vertical (Left)'); expect( - within(screen.getAllByRole('menuitem')[4]).getByLabelText('check'), + within(verticalItem.closest('li')!).getByLabelText('check'), ).toBeInTheDocument(); userEvent.click(screen.getByText('Horizontal (Top)')); - // 1st check - checkmark appears immediately after click + userEvent.click(screen.getByLabelText('gear')); + userEvent.hover(screen.getByText('Orientation of filter bar')); + + const horizontalItem = await screen.findByText('Horizontal (Top)'); expect( - await within(screen.getAllByRole('menuitem')[5]).findByLabelText('check'), + within(horizontalItem.closest('li')!).getByLabelText('check'), ).toBeInTheDocument(); - // successful query + await waitFor(() => expect(fetchMock.lastCall()?.[1]?.body).toEqual( JSON.stringify({ @@ -201,23 +203,18 @@ test('On selection change, send request and update checked value', async () => { }), ), ); + await waitFor(() => { - const menuitems = screen.getAllByRole('menuitem'); - expect(menuitems.length).toBeGreaterThanOrEqual(6); + userEvent.click(screen.getByLabelText('gear')); + userEvent.hover(screen.getByText('Orientation of filter bar')); + const updatedHorizontalItem = screen.getByText('Horizontal (Top)'); + expect( + within(updatedHorizontalItem.closest('li')!).getByLabelText('check'), + ).toBeInTheDocument(); + expect( + within(verticalItem.closest('li')!).queryByLabelText('check'), + ).not.toBeInTheDocument(); }); - - userEvent.click(screen.getByLabelText('gear')); - userEvent.hover(screen.getByText('Orientation of filter bar')); - expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); - expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); - - // 2nd check - checkmark stays after successful query - expect( - await within(screen.getAllByRole('menuitem')[5]).findByLabelText('check'), - ).toBeInTheDocument(); - expect( - within(screen.getAllByRole('menuitem')[4]).queryByLabelText('check'), - ).not.toBeInTheDocument(); }); test('On failed request, restore previous selection', async () => { @@ -254,9 +251,8 @@ test('On failed request, restore previous selection', async () => { userEvent.click(screen.getByLabelText('gear')); userEvent.hover(screen.getByText('Orientation of filter bar')); - expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); - await waitFor(() => { + expect(screen.getByText('Vertical (Left)')).toBeInTheDocument(); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems.length).toBeGreaterThanOrEqual(6); }); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx index 404d73799..7b617abf1 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx @@ -33,12 +33,13 @@ import { saveCrossFiltersSetting, } from 'src/dashboard/actions/dashboardInfo'; import Icons from 'src/components/Icons'; -import DropdownSelectableIcon, { - DropDownSelectableProps, -} from 'src/components/DropdownSelectableIcon'; import Checkbox from 'src/components/Checkbox'; +import { Dropdown } from 'src/components/Dropdown'; +import { Button } from 'src/components'; +import { Space } from 'src/components/Space'; import { clearDataMaskState } from 'src/dataMask/actions'; import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state'; +import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal'; import { useCrossFiltersScopingModal } from '../CrossFilters/ScopingModal/useCrossFiltersScopingModal'; import FilterConfigurationLink from '../FilterConfigurationLink'; @@ -100,6 +101,12 @@ const FilterBarSettings = () => { const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(); + const { openFilterConfigModal, FilterConfigModalComponent } = + useFilterConfigModal({ + createNewOnOpen: filterValues.length === 0, + dashboardId, + }); + const updateCrossFiltersSetting = useCallback( async isEnabled => { if (!isEnabled) { @@ -133,7 +140,7 @@ const FilterBarSettings = () => { [dispatch, filterBarOrientation], ); - const handleSelect = useCallback( + const handleClick = useCallback( ( selection: Parameters< Required>['onSelect'] @@ -146,9 +153,16 @@ const FilterBarSettings = () => { toggleFilterBarOrientation(selectedKey); } else if (selectedKey === CROSS_FILTERS_SCOPING_MENU_KEY) { openScopingModal(); + } else if (selectedKey === ADD_EDIT_FILTERS_MENU_KEY) { + openFilterConfigModal(); } }, - [openScopingModal, toggleCrossFiltering, toggleFilterBarOrientation], + [ + openScopingModal, + toggleCrossFiltering, + toggleFilterBarOrientation, + openFilterConfigModal, + ], ); const crossFiltersMenuItem = useMemo( @@ -168,21 +182,20 @@ const FilterBarSettings = () => { ); const menuItems = useMemo(() => { - const items: DropDownSelectableProps['menuItems'] = []; + const items: MenuProps['items'] = []; if (canEdit) { items.push({ key: ADD_EDIT_FILTERS_MENU_KEY, label: ( - + {t('Add or edit filters')} ), - divider: canSetHorizontalFilterBar, }); + if (canSetHorizontalFilterBar) { + items.push({ type: 'divider' }); + } } if (canEdit) { items.push({ @@ -192,8 +205,10 @@ const FilterBarSettings = () => { items.push({ key: CROSS_FILTERS_SCOPING_MENU_KEY, label: t('Cross-filtering scoping'), - divider: canSetHorizontalFilterBar, }); + if (canSetHorizontalFilterBar) { + items.push({ type: 'divider' }); + } } if (canSetHorizontalFilterBar) { items.push({ @@ -202,17 +217,31 @@ const FilterBarSettings = () => { children: [ { key: FilterBarOrientation.Vertical, - label: t('Vertical (Left)'), + label: ( + + {t('Vertical (Left)')} + {selectedFilterBarOrientation === + FilterBarOrientation.Vertical && } + + ), }, { key: FilterBarOrientation.Horizontal, - label: t('Horizontal (Top)'), + label: ( + + {t('Horizontal (Top)')} + {selectedFilterBarOrientation === + FilterBarOrientation.Horizontal && } + + ), }, ], + ...{ 'data-test': 'dropdown-selectable-icon-submenu' }, }); } return items; }, [ + selectedFilterBarOrientation, canEdit, canSetHorizontalFilterBar, crossFiltersMenuItem, @@ -226,19 +255,24 @@ const FilterBarSettings = () => { return ( <> - + + {scopingModal} + {FilterConfigModalComponent} ); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx index 0b5a18835..7ce156a7c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx @@ -38,11 +38,16 @@ test('should render the config link text', () => { }); test('should render the modal on click', () => { - render(Config link, { - useRedux: true, - }); + const showModal = jest.fn(); + render( + + Config link + , + { + useRedux: true, + }, + ); const configLink = screen.getByText('Config link'); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); userEvent.click(configLink); - expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(showModal).toHaveBeenCalled(); }); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx index 6b4fea62a..367072fba 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx @@ -16,70 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import { ReactNode, FC, useCallback, useState, memo } from 'react'; +import { ReactNode, FC, memo } from 'react'; -import { useDispatch } from 'react-redux'; -import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters'; -import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal'; import { getFilterBarTestId } from '../utils'; -import { SaveFilterChangesType } from '../../FiltersConfigModal/types'; export interface FCBProps { - createNewOnOpen?: boolean; - dashboardId?: number; - initialFilterId?: string; onClick?: () => void; children?: ReactNode; } export const FilterConfigurationLink: FC = ({ - createNewOnOpen, - dashboardId, - initialFilterId, onClick, children, -}) => { - const dispatch = useDispatch(); - const [isOpen, setOpen] = useState(false); - const close = useCallback(() => { - setOpen(false); - }, [setOpen]); - - const submit = useCallback( - async (filterChanges: SaveFilterChangesType) => { - dispatch(await setFilterConfiguration(filterChanges)); - close(); - }, - [dispatch, close], - ); - - const handleClick = useCallback(() => { - setOpen(true); - if (onClick) { - onClick(); - } - }, [setOpen, onClick]); - - return ( - <> -
- {children} -
- - - ); -}; +}) => ( +
+ {children} +
+); export default memo(FilterConfigurationLink); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx new file mode 100644 index 000000000..2a08f7800 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx @@ -0,0 +1,82 @@ +/** + * 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 { useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters'; +import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types'; +import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal'; + +interface UseFilterConfigModalProps { + createNewOnOpen?: boolean; + dashboardId: number; + initialFilterId?: string; +} + +interface UseFilterConfigModalReturn { + isFilterConfigModalOpen: boolean; + openFilterConfigModal: () => void; + closeFilterConfigModal: () => void; + handleFilterSave: (filterChanges: SaveFilterChangesType) => Promise; + FilterConfigModalComponent: JSX.Element | null; +} + +export const useFilterConfigModal = ({ + createNewOnOpen = false, + dashboardId, + initialFilterId, +}: UseFilterConfigModalProps): UseFilterConfigModalReturn => { + const dispatch = useDispatch(); + const [isFilterConfigModalOpen, setIsFilterConfigModalOpen] = useState(false); + + const openFilterConfigModal = useCallback(() => { + setIsFilterConfigModalOpen(true); + }, []); + + const closeFilterConfigModal = useCallback(() => { + setIsFilterConfigModalOpen(false); + }, []); + + const handleFilterSave = useCallback( + async (filterChanges: SaveFilterChangesType) => { + dispatch(await setFilterConfiguration(filterChanges)); + closeFilterConfigModal(); + }, + [dispatch, closeFilterConfigModal], + ); + + const FilterConfigModalComponent = isFilterConfigModalOpen ? ( + + ) : null; + + return { + isFilterConfigModalOpen, + openFilterConfigModal, + closeFilterConfigModal, + handleFilterSave, + FilterConfigModalComponent, + }; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx index b82b96767..0d6887f0c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -58,9 +58,6 @@ const Wrapper = styled.div` padding: ${theme.gridUnit * 3}px ${theme.gridUnit * 2}px ${ theme.gridUnit }px; - .ant-dropdown-trigger span { - padding-right: ${theme.gridUnit * 2}px; - } `} `; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx index 01679b192..9346a72ab 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx @@ -19,6 +19,7 @@ import { useSelector } from 'react-redux'; import { css, SupersetTheme, useTheme, useTruncation } from '@superset-ui/core'; import Icons from 'src/components/Icons'; +import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal'; import { RootState } from 'src/dashboard/types'; import { Row, FilterName, InternalRow } from './Styles'; import { FilterCardRowProps } from './types'; @@ -39,6 +40,12 @@ export const NameRow = ({ ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); + const { FilterConfigModalComponent, openFilterConfigModal } = + useFilterConfigModal({ + dashboardId, + initialFilterId: filter.id, + }); + return ( css` @@ -58,9 +65,10 @@ export const NameRow = ({ {canEdit && ( { + openFilterConfigModal(); + hidePopover(); + }} > )} + {FilterConfigModalComponent} ); }; diff --git a/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts new file mode 100644 index 000000000..bc8ef7d1f --- /dev/null +++ b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts @@ -0,0 +1,184 @@ +/** + * 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 { useCallback, useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import { last } from 'lodash'; +import { + logging, + t, + SupersetClient, + SupersetApiError, +} from '@superset-ui/core'; +import { + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, +} from 'src/logger/LogUtils'; +import { RootState } from 'src/dashboard/types'; +import { getDashboardUrlParams } from 'src/utils/urlUtils'; +import { DownloadScreenshotFormat } from '../components/menu/DownloadMenuItems/types'; + +const RETRY_INTERVAL = 3000; +const MAX_RETRIES = 30; + +export const useDownloadScreenshot = ( + dashboardId: number, + logEvent?: Function, +) => { + const activeTabs = useSelector( + (state: RootState) => state.dashboardState.activeTabs || undefined, + ); + const anchor = useSelector( + (state: RootState) => + last(state.dashboardState.directPathToChild) || undefined, + ); + const dataMask = useSelector( + (state: RootState) => state.dataMask || undefined, + ); + + const { addDangerToast, addSuccessToast, addInfoToast } = useToasts(); + + const currentIntervalIds = useRef([]); + + const stopIntervals = useCallback( + (message?: 'success' | 'failure') => { + currentIntervalIds.current.forEach(clearInterval); + + if (message === 'failure') { + addDangerToast( + t('The screenshot could not be downloaded. Please, try again later.'), + ); + } + if (message === 'success') { + addSuccessToast(t('The screenshot has been downloaded.')); + } + }, + [addDangerToast, addSuccessToast], + ); + + const downloadScreenshot = useCallback( + (format: DownloadScreenshotFormat) => { + let retries = 0; + + const toastIntervalId = setInterval( + () => + addInfoToast( + t( + 'The screenshot is being generated. Please, do not leave the page.', + ), + { noDuplicate: true }, + ), + RETRY_INTERVAL, + ); + + currentIntervalIds.current = [ + ...(currentIntervalIds.current || []), + toastIntervalId, + ]; + + const checkImageReady = (cacheKey: string) => + SupersetClient.get({ + endpoint: `/api/v1/dashboard/${dashboardId}/screenshot/${cacheKey}/?download_format=${format}`, + headers: { Accept: 'application/pdf, image/png' }, + parseMethod: 'raw', + }) + .then((response: Response) => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `screenshot.${format}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + stopIntervals('success'); + }) + .catch(err => { + if ((err as SupersetApiError).status === 404) { + throw new Error('Image not ready'); + } + }); + + const fetchImageWithRetry = (cacheKey: string) => { + if (retries >= MAX_RETRIES) { + stopIntervals('failure'); + logging.error('Max retries reached'); + return; + } + checkImageReady(cacheKey).catch(() => { + retries += 1; + }); + }; + + SupersetClient.post({ + endpoint: `/api/v1/dashboard/${dashboardId}/cache_dashboard_screenshot/`, + jsonPayload: { + anchor, + activeTabs, + dataMask, + urlParams: getDashboardUrlParams(['edit']), + }, + }) + .then(({ json }) => { + const cacheKey = json?.cache_key; + if (!cacheKey) { + throw new Error('No image URL in response'); + } + const retryIntervalId = setInterval(() => { + fetchImageWithRetry(cacheKey); + }, RETRY_INTERVAL); + currentIntervalIds.current.push(retryIntervalId); + fetchImageWithRetry(cacheKey); + }) + .catch(error => { + logging.error(error); + stopIntervals('failure'); + }) + .finally(() => { + logEvent?.( + format === DownloadScreenshotFormat.PNG + ? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE + : LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, + ); + }); + }, + [ + dashboardId, + anchor, + activeTabs, + dataMask, + addInfoToast, + stopIntervals, + logEvent, + ], + ); + + useEffect( + () => () => { + if (currentIntervalIds.current.length > 0) { + stopIntervals(); + } + currentIntervalIds.current = []; + }, + [stopIntervals], + ); + + return downloadScreenshot; +}; diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts index 0aae5aa4f..f3f0d753b 100644 --- a/superset-frontend/src/dashboard/styles.ts +++ b/superset-frontend/src/dashboard/styles.ts @@ -103,10 +103,10 @@ export const filterCardPopoverStyle = (theme: SupersetTheme) => css` `; export const chartContextMenuStyles = (theme: SupersetTheme) => css` - .ant-dropdown-menu.chart-context-menu { + .antd5-dropdown-menu.chart-context-menu { min-width: ${theme.gridUnit * 43}px; } - .ant-dropdown-menu-submenu.chart-context-submenu { + .antd5-dropdown-menu-submenu.chart-context-submenu { max-width: ${theme.gridUnit * 60}px; min-width: ${theme.gridUnit * 40}px; } @@ -117,7 +117,7 @@ export const focusStyle = (theme: SupersetTheme) => css` .ant-tabs-tabpane, .ant-tabs-tab-btn, .superset-button, - .superset-button.ant-dropdown-trigger, + .superset-button.antd5-dropdown-trigger, .header-controls span { &:focus-visible { box-shadow: 0 0 0 2px ${theme.colors.primary.dark1}; diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx index 97bd8808e..69718f55f 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx @@ -31,6 +31,9 @@ import { sliceUpdated } from 'src/explore/actions/exploreActions'; import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions'; import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions'; import { applyColors, resetColors } from 'src/utils/colorScheme'; +import ReportModal from 'src/features/reports/ReportModal'; +import DeleteModal from 'src/components/DeleteModal'; +import { deleteActiveReport } from 'src/features/reports/ReportModal/actions'; import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu'; import { useExploreMetadataBar } from './useExploreMetadataBar'; @@ -86,6 +89,8 @@ export const ExploreChartHeader = ({ const dispatch = useDispatch(); const { latestQueryFormData, sliceFormData } = chart; const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false); + const [isReportModalOpen, setIsReportModalOpen] = useState(false); + const [currentReportDeleting, setCurrentReportDeleting] = useState(null); const updateCategoricalNamespace = async () => { const { dashboards } = metadata || {}; const dashboard = @@ -128,6 +133,14 @@ export const ExploreChartHeader = ({ setIsPropertiesModalOpen(false); }; + const showReportModal = () => { + setIsReportModalOpen(true); + }; + + const closeReportModal = () => { + setIsReportModalOpen(false); + }; + const showModal = useCallback(() => { dispatch(setSaveChartModalVisibility(true)); }, [dispatch]); @@ -139,6 +152,11 @@ export const ExploreChartHeader = ({ [dispatch], ); + const handleReportDelete = async report => { + await dispatch(deleteActiveReport(report)); + setCurrentReportDeleting(null); + }; + const history = useHistory(); const { redirectSQLLab } = actions; @@ -158,6 +176,8 @@ export const ExploreChartHeader = ({ openPropertiesModal, ownState, metadata?.dashboards, + showReportModal, + setCurrentReportDeleting, ); const metadataBar = useExploreMetadataBar(metadata, slice); @@ -229,8 +249,8 @@ export const ExploreChartHeader = ({ } additionalActionsMenu={menu} menuDropdownProps={{ - visible: isDropdownVisible, - onVisibleChange: setIsDropdownVisible, + open: isDropdownVisible, + onOpenChange: setIsDropdownVisible, }} /> {isPropertiesModalOpen && ( @@ -241,6 +261,33 @@ export const ExploreChartHeader = ({ slice={slice} /> )} + + + + {currentReportDeleting && ( + { + if (currentReportDeleting) { + handleReportDelete(currentReportDeleting); + } + }} + onHide={() => setCurrentReportDeleting(null)} + open + title={t('Delete Report?')} + /> + )} ); }; diff --git a/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx index 2490ee754..cc7fd23d3 100644 --- a/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx +++ b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx @@ -20,7 +20,7 @@ import { ReactChild, useCallback, Key } from 'react'; import { t, styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; enum MenuKeys { @@ -67,9 +67,9 @@ export const ExportToCSVDropdown = ({ ); return ( - ( @@ -84,9 +84,9 @@ export const ExportToCSVDropdown = ({ - } + )} > {children} - + ); }; diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx index 130e96443..92694dc1a 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx @@ -107,6 +107,12 @@ describe('DatasourceControl', () => { expect(screen.queryAllByRole('menuitem')).toHaveLength(3); }); + // Close the menu + userEvent.click(document.body); + await waitFor(() => { + expect(screen.queryAllByRole('menuitem')).toHaveLength(0); + }); + rerender(, { useRedux: true, useRouter: true, diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 4e5063b1e..3070cdc7d 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -29,7 +29,7 @@ import { } from '@superset-ui/core'; import { getTemporalColumns } from '@superset-ui/chart-controls'; import { getUrlParam } from 'src/utils/urlUtils'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; @@ -82,12 +82,8 @@ const Styles = styled.div` .error-alert { margin: ${({ theme }) => 2 * theme.gridUnit}px; } - .ant-dropdown-trigger { + .antd5-dropdown-trigger { margin-left: ${({ theme }) => 2 * theme.gridUnit}px; - box-shadow: none; - &:active { - box-shadow: none; - } } .btn-group .open .dropdown-toggle { box-shadow: none; @@ -410,8 +406,8 @@ class DatasourceControl extends PureComponent { {extra?.warning_markdown && ( )} - datasource.type === DatasourceType.Query ? queryDatasourceMenu : defaultDatasourceMenu @@ -423,7 +419,7 @@ class DatasourceControl extends PureComponent { className="datasource-modal-trigger" data-test="datasource-menu-trigger" /> - +
{/* missing dataset */} {isMissingDatasource && isMissingParams && ( diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index 21cf69cec..6f7b64bc2 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -108,13 +108,6 @@ export const MenuTrigger = styled(Button)` `} `; -const iconReset = css` - .ant-dropdown-menu-item > & > .anticon:first-child { - margin-right: 0; - vertical-align: 0; - } -`; - export const useExploreAdditionalActionsMenu = ( latestQueryFormData, canDownloadCSV, @@ -123,6 +116,8 @@ export const useExploreAdditionalActionsMenu = ( onOpenPropertiesModal, ownState, dashboards, + showReportModal, + setCurrentReportDeleting, ...rest ) => { const theme = useTheme(); @@ -330,14 +325,14 @@ export const useExploreAdditionalActionsMenu = ( <> } + icon={} disabled={!canDownloadCSV} > {t('Export to original .CSV')} } + icon={} disabled={!canDownloadCSV} > {t('Export to pivoted .CSV')} @@ -346,7 +341,7 @@ export const useExploreAdditionalActionsMenu = ( ) : ( } + icon={} disabled={!canDownloadCSV} > {t('Export to .CSV')} @@ -354,20 +349,20 @@ export const useExploreAdditionalActionsMenu = ( )} } + icon={} disabled={!canDownloadCSV} > {t('Export to .JSON')} } + icon={} > {t('Download as image')} } + icon={} disabled={!canDownloadCSV} > {t('Export to Excel')} @@ -403,28 +398,25 @@ export const useExploreAdditionalActionsMenu = ( {showReportSubMenu ? ( <> - - - + ) : ( - - - + )} )} - + menu}> - + } /> diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx index 2b09233d9..dff8b638b 100644 --- a/superset-frontend/src/features/dashboards/DashboardCard.tsx +++ b/superset-frontend/src/features/dashboards/DashboardCard.tsx @@ -26,7 +26,7 @@ import { SupersetClient, } from '@superset-ui/core'; import { CardStyles } from 'src/views/CRUD/utils'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import ListViewCard from 'src/components/ListViewCard'; import Icons from 'src/components/Icons'; @@ -179,9 +179,9 @@ function DashboardCard({ isStarred={favoriteStatus} /> )} - + menu}> - + } /> diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index 5dd8c0f88..a9518fdcb 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -409,6 +409,7 @@ const RightMenu = ({ {RightMenuExtension && } {!navbarRight.user_is_anonymous && showActionDropdown && ( @@ -474,6 +475,7 @@ const RightMenu = ({ )} } > diff --git a/superset-frontend/src/features/home/SavedQueries.tsx b/superset-frontend/src/features/home/SavedQueries.tsx index fdc8cb864..12683f581 100644 --- a/superset-frontend/src/features/home/SavedQueries.tsx +++ b/superset-frontend/src/features/home/SavedQueries.tsx @@ -25,7 +25,7 @@ import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import { LoadingCards } from 'src/pages/Home'; import { TableTab } from 'src/views/CRUD/types'; import withToasts from 'src/components/MessageToasts/withToasts'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import { copyQueryLink, useListViewResource } from 'src/views/CRUD/hooks'; import ListViewCard from 'src/components/ListViewCard'; @@ -315,11 +315,11 @@ const SavedQueries = ({ e.preventDefault(); }} > - + renderMenu(q)}> - + } 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 11fb215c3..51fa41868 100644 --- a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx +++ b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.test.tsx @@ -19,14 +19,15 @@ import userEvent from '@testing-library/user-event'; import { render, screen, act } from 'spec/helpers/testing-library'; import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; +import { Menu } from 'src/components/Menu'; import HeaderReportDropdown, { HeaderReportProps } from '.'; const createProps = () => ({ dashboardId: 1, useTextMenu: false, - isDropdownVisible: true, - setIsDropdownVisible: jest.fn, setShowReportSubMenu: jest.fn, + showReportModal: jest.fn, + setCurrentReportDeleting: jest.fn, }); const stateWithOnlyUser = { @@ -117,9 +118,9 @@ const stateWithUserAndReport = { function setup(props: HeaderReportProps, initialState = {}) { render( -
+ -
, + , { useRedux: true, initialState }, ); } @@ -147,95 +148,90 @@ describe('Header Report Dropdown', () => { act(() => { setup(mockedProps, stateWithUserAndReport); }); - expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByRole('menuitem')).toBeInTheDocument(); }); - it('renders the dropdown correctly', () => { + it('renders the dropdown correctly', async () => { const mockedProps = createProps(); act(() => { setup(mockedProps, stateWithUserAndReport); }); - const emailReportModalButton = screen.getByRole('button'); - userEvent.click(emailReportModalButton); - expect(screen.getByText('Email reports active')).toBeInTheDocument(); + const emailReportModalButton = screen.getByRole('menuitem'); + userEvent.hover(emailReportModalButton); + expect(await screen.findByText('Email reports active')).toBeInTheDocument(); expect(screen.getByText('Edit email report')).toBeInTheDocument(); expect(screen.getByText('Delete email report')).toBeInTheDocument(); }); it('opens an edit modal', async () => { const mockedProps = createProps(); + mockedProps.showReportModal = jest.fn(); act(() => { setup(mockedProps, stateWithUserAndReport); }); - const emailReportModalButton = screen.getByRole('button'); + const emailReportModalButton = screen.getByRole('menuitem'); userEvent.click(emailReportModalButton); - const editModal = screen.getByText('Edit email report'); + const editModal = await screen.findByText('Edit email report'); userEvent.click(editModal); - const textBoxes = await screen.findAllByText('Edit email report'); - expect(textBoxes).toHaveLength(2); + expect(mockedProps.showReportModal).toHaveBeenCalled(); }); - it('opens a delete modal', () => { + it('opens a delete modal', async () => { const mockedProps = createProps(); + mockedProps.setCurrentReportDeleting = jest.fn(); act(() => { setup(mockedProps, stateWithUserAndReport); }); - const emailReportModalButton = screen.getByRole('button'); + const emailReportModalButton = screen.getByRole('menuitem'); userEvent.click(emailReportModalButton); - const deleteModal = screen.getByText('Delete email report'); + const deleteModal = await screen.findByText('Delete email report'); userEvent.click(deleteModal); - expect(screen.getByText('Delete Report?')).toBeInTheDocument(); + expect(mockedProps.setCurrentReportDeleting).toHaveBeenCalled(); }); - it('renders a new report modal if there is no report', () => { - const mockedProps = createProps(); - act(() => { - setup(mockedProps, stateWithOnlyUser); - }); - const emailReportModalButton = screen.getByRole('button'); - userEvent.click(emailReportModalButton); - expect(screen.getByText('Schedule a new email report')).toBeInTheDocument(); - }); - - it('renders Manage Email Reports Menu if textMenu is set to true and there is a report', () => { + it('renders Manage Email Reports Menu if textMenu is set to true and there is a report', async () => { let mockedProps = createProps(); mockedProps = { ...mockedProps, useTextMenu: true, - isDropdownVisible: true, }; act(() => { setup(mockedProps, stateWithUserAndReport); }); - expect(screen.getByText('Email reports active')).toBeInTheDocument(); + userEvent.click(screen.getByRole('menuitem')); + expect(await screen.findByText('Email reports active')).toBeInTheDocument(); expect(screen.getByText('Edit email report')).toBeInTheDocument(); expect(screen.getByText('Delete email report')).toBeInTheDocument(); }); - it('renders Schedule Email Reports if textMenu is set to true and there is a report', () => { + it('renders Schedule Email Reports if textMenu is set to true and there is a report', async () => { let mockedProps = createProps(); mockedProps = { ...mockedProps, useTextMenu: true, - isDropdownVisible: true, }; act(() => { setup(mockedProps, stateWithOnlyUser); }); - expect(screen.getByText('Set up an email report')).toBeInTheDocument(); + userEvent.click(screen.getByRole('menuitem')); + expect( + await screen.findByText('Set up an email report'), + ).toBeInTheDocument(); }); - it('renders Schedule Email Reports as long as user has permission through any role', () => { + it('renders Schedule Email Reports as long as user has permission through any role', async () => { let mockedProps = createProps(); mockedProps = { ...mockedProps, useTextMenu: true, - isDropdownVisible: true, }; act(() => { setup(mockedProps, stateWithNonAdminUser); }); - expect(screen.getByText('Set up an email report')).toBeInTheDocument(); + userEvent.click(screen.getByRole('menuitem')); + expect( + await screen.findByText('Set up an email report'), + ).toBeInTheDocument(); }); it('do not render Schedule Email Reports if user no permission', () => { @@ -243,11 +239,12 @@ describe('Header Report Dropdown', () => { mockedProps = { ...mockedProps, useTextMenu: true, - isDropdownVisible: true, }; act(() => { setup(mockedProps, stateWithNonMenuAccessOnManage); }); + + userEvent.click(screen.getByRole('menu')); expect( screen.queryByText('Set up an email report'), ).not.toBeInTheDocument(); diff --git a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx index 66a8195f1..9f0523280 100644 --- a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx +++ b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useState, useEffect } from 'react'; +import { ReactNode, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { isEmpty } from 'lodash'; import { @@ -24,7 +24,6 @@ import { SupersetTheme, css, styled, - useTheme, FeatureFlag, isFeatureEnabled, getExtensionsRegistry, @@ -36,15 +35,11 @@ import { AlertObject } from 'src/features/alerts/types'; import { Menu } from 'src/components/Menu'; import Checkbox from 'src/components/Checkbox'; import { noOp } from 'src/utils/common'; -import { NoAnimationDropdown } from 'src/components/Dropdown'; -import DeleteModal from 'src/components/DeleteModal'; -import ReportModal from 'src/features/reports/ReportModal'; import { ChartState } from 'src/explore/types'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { fetchUISpecificReport, toggleActive, - deleteActiveReport, } from 'src/features/reports/ReportModal/actions'; import { reportSelector } from 'src/views/CRUD/hooks'; import { MenuItemWithCheckboxContainer } from 'src/explore/components/useExploreAdditionalActionsMenu/index'; @@ -99,9 +94,10 @@ export interface HeaderReportProps { chart?: ChartState; useTextMenu?: boolean; setShowReportSubMenu?: (show: boolean) => void; - setIsDropdownVisible?: (visible: boolean) => void; - isDropdownVisible?: boolean; showReportSubMenu?: boolean; + submenuTitle?: string; + showReportModal: () => void; + setCurrentReportDeleting: (report: AlertObject | null) => void; } // Same instance to be used in useEffects @@ -112,9 +108,9 @@ export default function HeaderReportDropDown({ chart, useTextMenu = false, setShowReportSubMenu, - setIsDropdownVisible, - isDropdownVisible, - ...rest + submenuTitle, + showReportModal, + setCurrentReportDeleting, }: HeaderReportProps) { const dispatch = useDispatch(); const report = useSelector(state => { @@ -156,22 +152,13 @@ export default function HeaderReportDropDown({ return permissions.some(permission => permission.length > 0); }; - const [currentReportDeleting, setCurrentReportDeleting] = - useState(null); - const theme = useTheme(); const prevDashboard = usePrevious(dashboardId); - const [showModal, setShowModal] = useState(false); const toggleActiveKey = async (data: AlertObject, checked: boolean) => { if (data?.id) { dispatch(toggleActive(data, checked)); } }; - const handleReportDelete = async (report: AlertObject) => { - await dispatch(deleteActiveReport(report)); - setCurrentReportDeleting(null); - }; - const shouldFetch = canAddReports() && !!((dashboardId && prevDashboard !== dashboardId) || chart?.id); @@ -191,12 +178,6 @@ export default function HeaderReportDropDown({ const showReportSubMenu = report && setShowReportSubMenu && canAddReports(); - // @z-index-below-dashboard-header (100) - 1 = 99 - const dropdownOverlayStyle = { - zIndex: 99, - animationDuration: '0s', - }; - useEffect(() => { if (showReportSubMenu) { setShowReportSubMenu(true); @@ -206,22 +187,16 @@ export default function HeaderReportDropDown({ }, [report]); const handleShowMenu = () => { - if (setIsDropdownVisible) { - setIsDropdownVisible(false); - setShowModal(true); - } + showReportModal(); }; const handleDeleteMenuClick = () => { - if (setIsDropdownVisible) { - setIsDropdownVisible(false); - setCurrentReportDeleting(report); - } + setCurrentReportDeleting(report); }; const textMenu = () => isEmpty(report) ? ( - + {DropdownItemExtension ? ( @@ -233,9 +208,14 @@ export default function HeaderReportDropDown({ )} - + ) : ( - + toggleActiveKey(report, !isReportActive)} @@ -251,10 +231,15 @@ export default function HeaderReportDropDown({ {t('Delete email report')} - + ); - const menu = () => ( - + const menu = (title: ReactNode) => ( + {t('Email reports active')} toggleActiveKey(report, checked)} size="small" - css={{ marginLeft: theme.gridUnit * 2 }} + css={theme => css` + margin-left: ${theme.gridUnit * 2}px; + `} /> - setShowModal(true)}> + showReportModal()}> {t('Edit email report')} {t('Delete email report')} - + ); const iconMenu = () => @@ -284,65 +271,12 @@ export default function HeaderReportDropDown({ title={t('Schedule email report')} tabIndex={0} className="action-button action-schedule-report" - onClick={() => setShowModal(true)} + onClick={() => showReportModal()} > ) : ( - <> - - triggerNode.closest('.action-button') - } - > - - - - - + menu() ); - - return ( - <> - {canAddReports() && ( - <> - setShowModal(false)} - userEmail={user.email} - dashboardId={dashboardId} - chart={chart} - creationMethod={ - dashboardId ? CreationMethod.Dashboards : CreationMethod.Charts - } - /> - {isDropdownVisible ? (useTextMenu ? textMenu() : iconMenu()) : null} - {currentReportDeleting && ( - { - if (currentReportDeleting) { - handleReportDelete(currentReportDeleting); - } - }} - onHide={() => setCurrentReportDeleting(null)} - open - title={t('Delete Report?')} - /> - )} - - )} - - ); + return <>{canAddReports() && (useTextMenu ? textMenu() : iconMenu())}; } diff --git a/superset-frontend/src/features/tags/TagCard.tsx b/superset-frontend/src/features/tags/TagCard.tsx index 6b6886869..092ba90f1 100644 --- a/superset-frontend/src/features/tags/TagCard.tsx +++ b/superset-frontend/src/features/tags/TagCard.tsx @@ -19,7 +19,7 @@ import { Link } from 'react-router-dom'; import { isFeatureEnabled, FeatureFlag, t, useTheme } from '@superset-ui/core'; import { CardStyles } from 'src/views/CRUD/utils'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListViewCard from 'src/components/ListViewCard'; @@ -108,9 +108,9 @@ function TagCard({ e.preventDefault(); }} > - + menu}> - + } />