You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2025/02/07 17:38:16 UTC
(superset) branch master updated: refactor(Dropdown): Migrate Dropdown to Ant Design 5 (#31972)
This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new bcc61bd933 refactor(Dropdown): Migrate Dropdown to Ant Design 5 (#31972)
bcc61bd933 is described below
commit bcc61bd9339543f43712b6966a5221bc37f1e802
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Fri Feb 7 20:38:04 2025 +0300
refactor(Dropdown): Migrate Dropdown to Ant Design 5 (#31972)
---
.../cypress/e2e/dashboard/_skip.controls.test.ts | 10 +-
.../cypress/e2e/dashboard/drillby.test.ts | 5 +-
.../cypress/e2e/dashboard/drilltodetail.test.ts | 5 +-
.../e2e/dashboard/horizontalFilterBar.test.ts | 8 +-
.../cypress-base/cypress/e2e/explore/chart.test.js | 22 +-
.../explore/visualizations/download_chart.test.js | 6 +-
.../cypress-base/cypress/e2e/sqllab/tabs.test.ts | 4 +-
.../cypress-base/cypress/support/directories.ts | 8 +-
superset-frontend/src/GlobalStyles.tsx | 9 -
.../SqlLab/components/QueryLimitSelect/index.tsx | 53 +---
.../components/SaveDatasetActionButton/index.tsx | 30 +-
.../src/SqlLab/components/SqlEditor/index.tsx | 10 +-
.../SqlLab/components/SqlEditorTabHeader/index.tsx | 6 +-
.../src/SqlLab/components/TablePreview/index.tsx | 15 +-
superset-frontend/src/components/Button/index.tsx | 2 +-
.../Chart/ChartContextMenu/ChartContextMenu.tsx | 42 ++-
.../Chart/DrillBy/DrillByMenuItems.test.tsx | 1 -
.../components/Chart/DrillBy/DrillByMenuItems.tsx | 35 +--
.../Chart/DrillDetail/DrillDetailMenuItems.tsx | 27 +-
.../components/Chart/MenuItemWithTruncation.tsx | 9 +-
.../src/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 ------
.../components/DropdownSelectableIcon/index.tsx | 177 -----------
.../ListViewCard/ListViewCard.stories.tsx | 10 +-
superset-frontend/src/components/Menu/index.tsx | 27 +-
.../src/components/PageHeaderWithActions/index.tsx | 10 +-
.../src/components/PopoverDropdown/index.tsx | 10 +-
.../Table/cell-renderers/ActionCell/index.tsx | 4 +-
superset-frontend/src/components/Tooltip/index.tsx | 18 +-
superset-frontend/src/components/index.ts | 2 -
.../src/dashboard/components/CssEditor/index.tsx | 6 +-
.../DashboardBuilder/DashboardBuilder.test.tsx | 3 -
.../HeaderActionsDropdown.test.tsx | 260 ----------------
.../src/dashboard/components/Header/index.jsx | 180 ++++++-----
.../src/dashboard/components/Header/types.ts | 5 +-
.../index.tsx => useHeaderActionsDropdownMenu.tsx} | 339 ++++++++++-----------
.../components/RefreshIntervalModal.test.tsx | 18 +-
.../SliceHeaderControls.test.tsx | 31 +-
.../components/SliceHeaderControls/index.tsx | 38 +--
.../DownloadMenuItems/DownloadAsImage.test.tsx | 6 +-
.../menu/DownloadMenuItems/DownloadAsImage.tsx | 12 +-
.../menu/DownloadMenuItems/DownloadAsPdf.test.tsx | 6 +-
.../menu/DownloadMenuItems/DownloadAsPdf.tsx | 12 +-
.../DownloadMenuItems/DownloadMenuItems.test.tsx | 12 +-
.../components/menu/DownloadMenuItems/index.tsx | 51 ++--
.../menu/ShareMenuItems/ShareMenuItems.test.tsx | 1 +
.../components/menu/ShareMenuItems/index.tsx | 16 +-
.../FilterBarSettings/FilterBarSettings.test.tsx | 50 ++-
.../FilterBar/FilterBarSettings/index.tsx | 76 +++--
.../FilterConfigurationLink.test.tsx | 15 +-
.../FilterBar/FilterConfigurationLink/index.tsx | 65 +---
.../useFilterConfigModal.tsx | 82 +++++
.../nativeFilters/FilterBar/Header/index.tsx | 3 -
.../nativeFilters/FilterCard/NameRow.tsx | 15 +-
.../src/dashboard/hooks/useDownloadScreenshot.ts | 184 +++++++++++
superset-frontend/src/dashboard/styles.ts | 6 +-
.../components/ExploreChartHeader/index.jsx | 51 +++-
.../components/ExportToCSVDropdown/index.tsx | 10 +-
.../DatasourceControl/DatasourceControl.test.jsx | 6 +
.../controls/DatasourceControl/index.jsx | 14 +-
.../useExploreAdditionalActionsMenu/index.jsx | 52 ++--
.../src/features/charts/ChartCard.tsx | 6 +-
.../src/features/dashboards/DashboardCard.tsx | 6 +-
superset-frontend/src/features/home/RightMenu.tsx | 2 +
.../src/features/home/SavedQueries.tsx | 6 +-
.../HeaderReportDropdown/index.test.tsx | 71 +++--
.../ReportModal/HeaderReportDropdown/index.tsx | 132 ++------
superset-frontend/src/features/tags/TagCard.tsx | 6 +-
71 files changed, 1132 insertions(+), 1571 deletions(-)
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 4a65d68cf5..0e5c733131 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 e471d1da8c..2586fbd438 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 4ebd64dd6e..d2eb482a9f 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 bcacae8a36..ec20224fce 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 c7b81a3e12..abe5a0c51d 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 ce7e522774..76653dada9 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 0deeabde8d..ef97eb1c19 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 3445cf5f26..c2754e4543 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 09a2792dde..d213faac8d 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 df35351e5c..ce9cb2b06d 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 (
- <LimitSelectStyled>
- <AntdDropdown
- overlay={renderQueryLimit(maxRow, setQueryLimit)}
- trigger={['click']}
- >
- <button type="button" onClick={e => e.preventDefault()}>
- <span>{t('LIMIT')}:</span>
- <span className="limitDropdown">
- {convertToNumWithSpaces(queryLimit)}
- </span>
- <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
- </button>
- </AntdDropdown>
- </LimitSelectStyled>
+ <Dropdown
+ dropdownRender={() => renderQueryLimit(maxRow, setQueryLimit)}
+ trigger={['click']}
+ >
+ <Button size="small" showMarginRight={false} type="link">
+ <span>{t('LIMIT')}:</span>
+ <span className="limitDropdown">
+ {convertToNumWithSpaces(queryLimit)}
+ </span>
+ <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
+ </Button>
+ </Dropdown>
);
};
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
index 2acd7665bf..6cb17bc2b4 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<DropdownButtonProps>,
- )`
- &.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 ? (
<Button onClick={() => setShowSave(true)} buttonStyle="primary">
{t('Save')}
</Button>
) : (
- <StyledDropdownButton
+ <DropdownButton
onClick={() => setShowSave(true)}
- overlay={overlayMenu}
+ dropdownRender={() => overlayMenu}
icon={
<Icons.CaretDown
iconColor={theme.colors.grayscale.light5}
@@ -71,7 +49,7 @@ const SaveDatasetActionButton = ({
trigger={['click']}
>
{t('Save')}
- </StyledDropdownButton>
+ </DropdownButton>
);
};
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 527c22752d..957de4fbc7 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<Props> = ({
<span>
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
</span>
- <AntdDropdown overlay={renderDropdown()} trigger={['click']}>
+ <Dropdown
+ dropdownRender={() => renderDropdown()}
+ trigger={['click']}
+ >
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
- </AntdDropdown>
+ </Dropdown>
</div>
</>
)}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
index d8f4c8e4ed..76c8b74b19 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<Props> = ({ queryEditor }) => {
return (
<TabTitleWrapper>
- <Dropdown
+ <MenuDotsDropdown
trigger={['click']}
overlay={
- <Menu style={{ width: 176 }}>
+ <Menu>
<Menu.Item
className="close-btn"
key="1"
diff --git a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
index 7f17b8e327..31714607cc 100644
--- a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
@@ -30,11 +30,8 @@ import {
import AutoSizer from 'react-virtualized-auto-sizer';
import Icons from 'src/components/Icons';
import type { SqlLabRootState } from 'src/SqlLab/types';
-import {
- Skeleton,
- AntdBreadcrumb as Breadcrumb,
- AntdDropdown,
-} from 'src/components';
+import { Skeleton, AntdBreadcrumb as Breadcrumb } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import FilterableTable from 'src/components/FilterableTable';
import Tabs from 'src/components/Tabs';
import {
@@ -308,8 +305,8 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
<Title>
<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>
</Title>
{isMetadataRefreshing ? (
<Skeleton active />
diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx
index c38fbc15ca..aa447e13bc 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 d619dae489..efea8cc23f 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<Column>();
+ const [showDrillByModal, setShowDrillByModal] = useState(false);
+ const [dataset, setDataset] = useState<Dataset>();
+ 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(
<>
<Dropdown
- overlay={
+ dropdownRender={() => (
<Menu
className="chart-context-menu"
data-test="chart-context-menu"
@@ -302,15 +320,15 @@ const ChartContextMenu = (
<Menu.Item disabled>{t('No actions')}</Menu.Item>
)}
</Menu>
- }
+ )}
trigger={['click']}
- onVisibleChange={value => {
+ onOpenChange={value => {
setVisible(value);
if (!value) {
setOpenKeys([]);
}
}}
- visible={visible}
+ open={visible}
>
<span
id={`hidden-span-${id}`}
@@ -335,6 +353,16 @@ const ChartContextMenu = (
}}
/>
)}
+ {showDrillByModal && drillByColumn && dataset && filters?.drillBy && (
+ <DrillByModal
+ column={drillByColumn}
+ drillByConfig={filters?.drillBy}
+ formData={formData}
+ onHideModal={handleCloseDrillByModal}
+ dataset={{ ...dataset!, verbose_map: verboseMap }}
+ canDownload={canDownload}
+ />
+ )}
</>,
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 c874da6854..c494ce52aa 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 = ({
<DrillByMenuItems
formData={formData ?? defaultFormData}
drillByConfig={drillByConfig}
- canDownload
open
{...rest}
/>
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index 47666db780..b054ff02d6 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<Dataset>();
const [columns, setColumns] = useState<Column[]>([]);
- const [showModal, setShowModal] = useState(false);
- const [currentColumn, setCurrentColumn] = useState();
const ref = useRef<InputRef>(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 (
<MenuItemWithTruncation
- key={`drill-by-item-${column.column_name}`}
+ menuKey={`drill-by-item-${column.column_name}`}
tooltipText={column.verbose_name || column.column_name}
- {...rest}
onClick={e => handleSelection(e, column)}
style={style}
+ {...rest}
>
{column.verbose_name || column.column_name}
</MenuItemWithTruncation>
@@ -289,6 +281,7 @@ export const DrillByMenuItems = ({
return (
<>
<Menu.SubMenu
+ key="drill-by-submenu"
title={t('Drill by')}
popupClassName="chart-context-submenu"
popupOffset={[0, submenuYOffset]}
@@ -349,16 +342,6 @@ export const DrillByMenuItems = ({
)}
</div>
</Menu.SubMenu>
- {showModal && (
- <DrillByModal
- column={currentColumn}
- drillByConfig={drillByConfig}
- formData={formData}
- onHideModal={closeModal}
- dataset={{ ...dataset!, verbose_map: verboseMap }}
- canDownload={canDownload}
- />
- )}
</>
);
};
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
index e20bc0290f..aae82d8309 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 }) => (
- <Menu.Item disabled {...props}>
+const DisabledMenuItem = ({
+ children,
+ menuKey,
+ ...rest
+}: {
+ children: ReactNode;
+ menuKey: string;
+}) => (
+ <Menu.Item disabled key={menuKey} {...rest}>
<div
css={css`
white-space: normal;
@@ -183,39 +190,34 @@ const DrillDetailMenuItems = ({
}
const drillToDetailMenuItem = drillDisabled ? (
- <DisabledMenuItem {...props} key="drill-to-detail-disabled">
+ <DisabledMenuItem menuKey="drill-to-detail-disabled" {...props}>
{DRILL_TO_DETAIL}
<MenuItemTooltip title={drillDisabled} />
</DisabledMenuItem>
) : (
- <Menu.Item
- {...props}
- key="drill-to-detail"
- onClick={openModal.bind(null, [])}
- >
+ <Menu.Item key="drill-to-detail" onClick={openModal.bind(null, [])}>
{DRILL_TO_DETAIL}
</Menu.Item>
);
const drillToDetailByMenuItem = drillByDisabled ? (
- <DisabledMenuItem {...props} key="drill-to-detail-by-disabled">
+ <DisabledMenuItem menuKey="drill-to-detail-by-disabled" {...props}>
{DRILL_TO_DETAIL_BY}
<MenuItemTooltip title={drillByDisabled} />
</DisabledMenuItem>
) : (
<Menu.SubMenu
- {...props}
popupOffset={[0, submenuYOffset]}
popupClassName="chart-context-submenu"
title={DRILL_TO_DETAIL_BY}
key={key}
+ {...props}
>
<div data-test="drill-to-detail-by-submenu">
{filters.map((filter, i) => (
<MenuItemWithTruncation
- {...props}
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
- key={`drill-detail-filter-${i}`}
+ menuKey={`drill-detail-filter-${i}`}
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_BY} `}
@@ -224,7 +226,6 @@ const DrillDetailMenuItems = ({
))}
{filters.length > 1 && (
<Menu.Item
- {...props}
key="drill-detail-filter-all"
onClick={openModal.bind(null, filters)}
>
diff --git a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
index 9e8802f128..cf77427cc7 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<HTMLDivElement>();
@@ -43,7 +46,9 @@ export const MenuItemWithTruncation = ({
display: flex;
line-height: 1.5em;
`}
- {...props}
+ eventKey={menuKey}
+ onClick={onClick}
+ style={style}
>
<Tooltip title={itemIsTruncated ? tooltipText : null}>
<div
diff --git a/superset-frontend/src/components/Dropdown/Dropdown.stories.tsx b/superset-frontend/src/components/Dropdown/Dropdown.stories.tsx
index 43d281505a..e30a69a38a 100644
--- a/superset-frontend/src/components/Dropdown/Dropdown.stories.tsx
+++ b/superset-frontend/src/components/Dropdown/Dropdown.stories.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Menu } from 'src/components/Menu';
-import { Dropdown, DropdownProps } from '.';
+import { MenuDotsDropdown, MenuDotsDropdownProps } from '.';
export default {
title: 'Dropdown',
@@ -50,8 +50,8 @@ const customOverlay = (
export const InteractiveDropdown = ({
overlayType,
...rest
-}: DropdownProps & { overlayType: string }) => (
- <Dropdown
+}: MenuDotsDropdownProps & { overlayType: string }) => (
+ <MenuDotsDropdown
{...rest}
overlay={overlayType === 'custom' ? customOverlay : menu}
/>
diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx
index ef81bd42cc..1fdca2e75d 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) => (
- <AntdDropdown overlay={overlay} {...rest}>
+}: MenuDotsDropdownProps) => (
+ <AntdDropdown dropdownRender={() => overlay} {...rest}>
<MenuDotsWrapper data-test="dropdown-trigger">
{RenderIcon(iconOrientation)}
</MenuDotsWrapper>
</AntdDropdown>
);
-export interface NoAnimationDropdownProps extends Antd5DropdownProps {
+export interface NoAnimationDropdownProps extends AntdDropdownProps {
children: ReactNode;
onBlur?: (e: FocusEvent<HTMLDivElement>) => void;
onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;
@@ -126,8 +124,13 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => {
});
return (
- <Antd5Dropdown overlayStyle={props.overlayStyle} {...rest}>
+ <AntdDropdown autoFocus overlayStyle={props.overlayStyle} {...rest}>
{childrenWithProps}
- </Antd5Dropdown>
+ </AntdDropdown>
);
};
+
+export type DropdownProps = AntdDropdownProps;
+export const Dropdown = (props: DropdownProps) => (
+ <AntdDropdown autoFocus {...props} />
+);
diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx
index 32a7739e3c..84ffd5fe28 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<typeof Dropdown.Button> & {
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'];
- } = {},
- ) => (
- <StyledDropdownButton>
- <AntdDropdown.Button overlay={overlay} {...rest} {...props} />
- </StyledDropdownButton>
+ const button = (
+ <Dropdown.Button dropdownRender={dropdownRender} {...rest}>
+ {children}
+ </Dropdown.Button>
);
if (tooltip) {
- return buildButton({
- buttonsRender: ([leftButton, rightButton]) => [
- <AntdTooltip
- placement={placement}
- id={`${kebabCase(tooltip)}-tooltip`}
- title={tooltip}
- >
- {leftButton}
- </AntdTooltip>,
- rightButton,
- ],
- });
+ return (
+ <Tooltip
+ placement={tooltipPlacement}
+ id={`${kebabCase(tooltip)}-tooltip`}
+ title={tooltip}
+ >
+ {button}
+ </Tooltip>
+ );
}
- 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 cc12a8ba5b..0000000000
--- 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) => (
- <DropdownSelectableIcon
- {...props}
- icon={<Icons.Gear name="gear" iconColor="#000000" />}
- />
-);
-
-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 cfff77d5ed..0000000000
--- 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: <Icons.Gear name="gear" />,
-};
-
-const asyncRender = (props: DropDownSelectableProps) =>
- waitFor(() => render(<DropdownSelectableIcon {...props} />));
-
-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 8d791929d3..0000000000
--- 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<MenuProps, 'onSelect'> {
- ref?: RefObject<HTMLDivElement>;
- icon: ReactNode;
- info?: string;
- menuItems: {
- key: string;
- label: string | ReactNode;
- children?: SubMenuItemProps[];
- divider?: boolean;
- }[];
- selectedKeys?: string[];
-}
-
-const StyledDropdownButton = styled(DropdownButton as FC<DropdownButtonProps>)`
- 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) => (
- <StyleMenuItem key={key} divider={divider}>
- <StyleSubmenuItem>
- {label}
- {selectedKeys?.includes(key) && (
- <Icons.Check
- iconColor={theme.colors.primary.base}
- className="tick-menu-item"
- iconSize="xl"
- />
- )}
- </StyleSubmenuItem>
- </StyleMenuItem>
- ),
- [selectedKeys, theme.colors.primary.base],
- );
-
- const overlayMenu = useMemo(
- () => (
- <>
- {info && (
- <div className="info" data-test="dropdown-selectable-info">
- {info}
- </div>
- )}
- <StyledMenu
- selectedKeys={selectedKeys}
- onSelect={handleMenuSelect}
- selectable
- >
- {menuItems.map(m =>
- m.children?.length ? (
- <SubMenu
- title={m.label}
- key={m.key}
- data-test="dropdown-selectable-icon-submenu"
- >
- {m.children.map(s => menuItem(s.label, s.key))}
- </SubMenu>
- ) : (
- menuItem(m.label, m.key, m.divider)
- ),
- )}
- </StyledMenu>
- </>
- ),
- [selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect],
- );
-
- return (
- <StyledDropdownButton
- overlay={overlayMenu}
- trigger={['click']}
- icon={icon}
- visible={visible}
- onVisibleChange={handleVisibleChange}
- />
- );
-};
diff --git a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
index 38fc7cbe94..61d2c70a31 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}
/>
- <AntdDropdown
- overlay={
+ <Dropdown
+ dropdownRender={() => (
<Menu>
<Menu.Item role="button" tabIndex={0} onClick={action('Delete')}>
<Icons.Trash /> Delete
@@ -80,10 +80,10 @@ export const SupersetListViewCard = ({
<Icons.EditAlt /> Edit
</Menu.Item>
</Menu>
- }
+ )}
>
<Icons.MoreHoriz />
- </AntdDropdown>
+ </Dropdown>
</ListViewCard.Actions>
}
/>
diff --git a/superset-frontend/src/components/Menu/index.tsx b/superset-frontend/src/components/Menu/index.tsx
index ff2c1c0b6e..5d0f6e0dcc 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 b84e67b31f..68c33d78c7 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<AntdDropdownProps, 'overlay'>;
+ menuDropdownProps: Omit<DropdownProps, 'overlay'>;
tooltipProps?: {
text?: string;
placement?: TooltipPlacement;
@@ -155,9 +155,9 @@ export const PageHeaderWithActions = ({
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
- <AntdDropdown
+ <Dropdown
trigger={['click']}
- overlay={additionalActionsMenu}
+ dropdownRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<Button
@@ -173,7 +173,7 @@ export const PageHeaderWithActions = ({
iconSize="l"
/>
</Button>
- </AntdDropdown>
+ </Dropdown>
)}
</div>
</div>
diff --git a/superset-frontend/src/components/PopoverDropdown/index.tsx b/superset-frontend/src/components/PopoverDropdown/index.tsx
index 9ad507d5f7..41812ea13f 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 (
- <AntdDropdown
+ <Dropdown
trigger={['click']}
overlayStyle={{ zIndex: theme.zIndex.max }}
- overlay={
+ dropdownRender={() => (
<Menu onClick={({ key }: HandleSelectProps) => onChange(key)}>
{options.map(option => (
<MenuItem
@@ -106,7 +106,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
</MenuItem>
))}
</Menu>
- }
+ )}
>
<div role="button" css={{ display: 'flex', alignItems: 'center' }}>
{selected && renderButton(selected)}
@@ -115,7 +115,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
css={{ marginTop: theme.gridUnit * 0.5 }}
/>
</div>
- </AntdDropdown>
+ </Dropdown>
);
};
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 1c3127504f..5b404ccbdd 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 (
- <Dropdown
+ <MenuDotsDropdown
iconOrientation={IconOrientation.Horizontal}
onVisibleChange={handleVisibleChange}
trigger={['click']}
diff --git a/superset-frontend/src/components/Tooltip/index.tsx b/superset-frontend/src/components/Tooltip/index.tsx
index 252aac3402..615ad802e7 100644
--- a/superset-frontend/src/components/Tooltip/index.tsx
+++ b/superset-frontend/src/components/Tooltip/index.tsx
@@ -23,14 +23,12 @@ import { TooltipProps, TooltipPlacement } from 'antd-v5/lib/tooltip';
export { TooltipProps, TooltipPlacement };
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
- <>
- <AntdTooltip
- styles={{
- body: { overflow: 'hidden', textOverflow: 'ellipsis' },
- root: overlayStyle ?? {},
- }}
- color={`${supersetTheme.colors.grayscale.dark2}e6`}
- {...props}
- />
- </>
+ <AntdTooltip
+ styles={{
+ body: { overflow: 'hidden', textOverflow: 'ellipsis' },
+ root: overlayStyle ?? {},
+ }}
+ color={`${supersetTheme.colors.grayscale.dark2}e6`}
+ {...props}
+ />
);
diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts
index cb949ef5c3..9d9ad65ebe 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 e2748c8b0b..2b9b990704 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<CssEditorProps, CssEditorState> {
</Menu>
);
return (
- <AntdDropdown overlay={menu} placement="bottomRight">
+ <Dropdown dropdownRender={() => menu} placement="bottomRight">
<Button>{t('Load a CSS template')}</Button>
- </AntdDropdown>
+ </Dropdown>
);
}
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 1a0b73165a..877d9203a0 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', () => () => (
<div data-test="mock-async-select" />
));
-jest.mock('src/dashboard/components/Header/HeaderActionsDropdown', () => () => (
- <div data-test="mock-header-actions-dropdown" />
-));
jest.mock('src/components/PageHeaderWithActions', () => ({
PageHeaderWithActions: () => (
<div data-test="mock-page-header-with-actions" />
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 090f12a2bd..0000000000
--- 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(
- <div className="dashboard-header">
- <HeaderActionsDropdown {...props} />
- </div>,
- { 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(<HeaderActionsDropdown {...props} />);
- 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 5ce86cc52f..1324d785cd 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(
- () => (
- <ConnectedHeaderActionsDropdown
- addSuccessToast={boundActionCreators.addSuccessToast}
- addDangerToast={boundActionCreators.addDangerToast}
- dashboardId={dashboardInfo.id}
- dashboardTitle={dashboardTitle}
- dashboardInfo={dashboardInfo}
- dataMask={dataMask}
- layout={layout}
- expandedSlices={expandedSlices}
- customCss={customCss}
- colorNamespace={colorNamespace}
- colorScheme={colorScheme}
- onSave={boundActionCreators.onSave}
- onChange={boundActionCreators.onChange}
- forceRefreshAllCharts={forceRefresh}
- startPeriodicRender={startPeriodicRender}
- refreshFrequency={refreshFrequency}
- shouldPersistRefreshFrequency={shouldPersistRefreshFrequency}
- setRefreshFrequency={boundActionCreators.setRefreshFrequency}
- updateCss={boundActionCreators.updateCss}
- editMode={editMode}
- hasUnsavedChanges={hasUnsavedChanges}
- userCanEdit={userCanEdit}
- userCanShare={userCanShare}
- userCanSave={userCanSaveAs}
- userCanCurate={userCanCurate}
- isLoading={isLoading}
- showPropertiesModal={showPropertiesModal}
- manageEmbedded={showEmbedModal}
- refreshLimit={refreshLimit}
- refreshWarning={refreshWarning}
- lastModifiedTime={actualLastModifiedTime}
- isDropdownVisible={isDropdownVisible}
- setIsDropdownVisible={setDropdownVisible}
- logEvent={boundActionCreators.logEvent}
- />
- ),
- [
- 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 (
<div
css={headerContainerStyle}
@@ -788,8 +751,11 @@ const Header = () => {
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 = () => {
/>
)}
+ <ReportModal
+ userId={user.userId}
+ show={showingReportModal}
+ onHide={hideReportModal}
+ userEmail={user.email}
+ dashboardId={dashboardInfo.id}
+ creationMethod="dashboards"
+ />
+
+ {currentReportDeleting && (
+ <DeleteModal
+ description={t(
+ 'This action will permanently delete %s.',
+ currentReportDeleting?.name,
+ )}
+ onConfirm={() => {
+ if (currentReportDeleting) {
+ handleReportDelete(currentReportDeleting);
+ }
+ }}
+ onHide={() => setCurrentReportDeleting(null)}
+ open
+ title={t('Delete Report?')}
+ />
+ )}
+
<OverwriteConfirm />
{userCanCurate && (
@@ -817,7 +809,7 @@ const Header = () => {
)}
<Global
styles={css`
- .ant-menu-vertical {
+ .antd5-menu-vertical {
border-right: none;
}
`}
diff --git a/superset-frontend/src/dashboard/components/Header/types.ts b/superset-frontend/src/dashboard/components/Header/types.ts
index caf087404f..d69a33e87b 100644
--- a/superset-frontend/src/dashboard/components/Header/types.ts
+++ b/superset-frontend/src/dashboard/components/Header/types.ts
@@ -19,6 +19,7 @@
import { Layout } from 'src/dashboard/types';
import { ChartState } from 'src/explore/types';
+import { AlertObject } from 'src/features/alerts/types';
interface DashboardInfo {
id: number;
@@ -60,11 +61,11 @@ export interface HeaderDropdownProps {
dataMask: any;
lastModifiedTime: number;
logEvent: () => 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 6bc712f6b8..420e91e552 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<boolean | null>(
+ 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 });
- };
+ 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;
+ }
+ setIsDropdownVisible(false);
+ },
+ [
+ forceRefreshAllCharts,
+ addSuccessToast,
+ showPropertiesModal,
+ manageEmbedded,
+ ],
+ );
- changeCss = (css: string) => {
- this.props.onChange();
- this.props.updateCss(css);
- };
+ const changeCss = useCallback(
+ (newCss: string) => {
+ onChange();
+ updateCss(newCss);
+ },
+ [onChange, updateCss],
+ );
- changeRefreshInterval = (refreshInterval: number, isPersistent: boolean) => {
- this.props.setRefreshFrequency(refreshInterval, isPersistent);
- this.props.startPeriodicRender(refreshInterval * 1000);
- };
+ const changeRefreshInterval = useCallback(
+ (refreshInterval: number, isPersistent: boolean) => {
+ setRefreshFrequency(refreshInterval, isPersistent);
+ startPeriodicRender(refreshInterval * 1000);
+ },
+ [setRefreshFrequency, startPeriodicRender],
+ );
- handleMenuClick = ({ key }: Record<string, any>) => {
- 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;
- }
- case MenuKeys.ManageEmbedded: {
- this.props.manageEmbedded();
- break;
- }
- default:
- break;
- }
- };
+ const emailSubject = useMemo(
+ () => `${t('Superset dashboard')} ${dashboardTitle}`,
+ [dashboardTitle],
+ );
- render() {
- const {
- dashboardTitle,
- dashboardId,
- dashboardInfo,
- refreshFrequency,
- shouldPersistRefreshFrequency,
- editMode,
- customCss,
- colorNamespace,
- colorScheme,
- layout,
- expandedSlices,
- onSave,
- userCanEdit,
- userCanShare,
- userCanSave,
- userCanCurate,
- isLoading,
- refreshLimit,
- refreshWarning,
- lastModifiedTime,
- addSuccessToast,
- addDangerToast,
- setIsDropdownVisible,
- isDropdownVisible,
- directPathToChild,
- ...rest
- } = this.props;
+ const url = useMemo(
+ () =>
+ getDashboardUrl({
+ pathname: window.location.pathname,
+ filters: getActiveFilters(),
+ hash: window.location.hash,
+ }),
+ [],
+ );
- const emailTitle = t('Superset dashboard');
- const emailSubject = `${emailTitle} ${dashboardTitle}`;
- const emailBody = t('Check out this dashboard: ');
+ 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 (
- <Menu selectable={false} data-test="header-actions-menu" {...rest}>
+ <Menu
+ selectable={false}
+ data-test="header-actions-menu"
+ onClick={handleMenuClick}
+ >
{!editMode && (
<Menu.Item
key={MenuKeys.RefreshDashboard}
data-test="refresh-dashboard-menu-item"
disabled={isLoading}
- onClick={this.handleMenuClick}
>
{t('Refresh dashboard')}
</Menu.Item>
)}
{!editMode && !isEmbedded && (
- <Menu.Item
- key={MenuKeys.ToggleFullscreen}
- onClick={this.handleMenuClick}
- >
+ <Menu.Item key={MenuKeys.ToggleFullscreen}>
{getUrlParam(URL_PARAMS.standalone)
? t('Exit fullscreen')
: t('Enter fullscreen')}
</Menu.Item>
)}
{editMode && (
- <Menu.Item
- key={MenuKeys.EditProperties}
- onClick={this.handleMenuClick}
- >
+ <Menu.Item key={MenuKeys.EditProperties}>
{t('Edit properties')}
</Menu.Item>
)}
@@ -196,8 +194,8 @@ export class HeaderActionsDropdown extends PureComponent<
<Menu.Item key={MenuKeys.EditCss}>
<CssEditor
triggerNode={<div>{t('Edit CSS')}</div>}
- initialCss={this.state.css}
- onChange={this.changeCss}
+ initialCss={css}
+ onChange={changeCss}
addDangerToast={addDangerToast}
/>
</Menu.Item>
@@ -228,29 +226,26 @@ export class HeaderActionsDropdown extends PureComponent<
/>
</Menu.Item>
)}
- <Menu.SubMenu
- key={MenuKeys.Download}
+ <DownloadMenuItems
+ submenuKey={MenuKeys.Download}
disabled={isLoading}
title={t('Download')}
- >
- <DownloadMenuItems
- pdfMenuItemTitle={t('Export to PDF')}
- imageMenuItemTitle={t('Download as Image')}
- dashboardTitle={dashboardTitle}
- dashboardId={dashboardId}
- logEvent={this.props.logEvent}
- />
- </Menu.SubMenu>
+ pdfMenuItemTitle={t('Export to PDF')}
+ imageMenuItemTitle={t('Download as Image')}
+ dashboardTitle={dashboardTitle}
+ dashboardId={dashboardId}
+ logEvent={logEvent}
+ />
{userCanShare && (
<ShareMenuItems
- key={MenuKeys.Share}
+ disabled={isLoading}
data-test="share-dashboard-menu-item"
title={t('Share')}
url={url}
copyMenuItemTitle={t('Copy permalink to clipboard')}
emailMenuItemTitle={t('Share permalink by email')}
emailSubject={emailSubject}
- emailBody={emailBody}
+ emailBody={t('Check out this dashboard: ')}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
dashboardId={dashboardId}
@@ -258,39 +253,33 @@ export class HeaderActionsDropdown extends PureComponent<
/>
)}
{!editMode && userCanCurate && (
- <Menu.Item
- key={MenuKeys.ManageEmbedded}
- onClick={this.handleMenuClick}
- >
+ <Menu.Item key={MenuKeys.ManageEmbedded}>
{t('Embed dashboard')}
</Menu.Item>
)}
<Menu.Divider />
{!editMode ? (
- this.state.showReportSubMenu ? (
+ showReportSubMenu ? (
<>
- <Menu.SubMenu title={t('Manage email report')}>
- <HeaderReportDropdown
- dashboardId={dashboardInfo.id}
- setShowReportSubMenu={this.setShowReportSubMenu}
- showReportSubMenu={this.state.showReportSubMenu}
- setIsDropdownVisible={setIsDropdownVisible}
- isDropdownVisible={isDropdownVisible}
- useTextMenu
- />
- </Menu.SubMenu>
- <Menu.Divider />
- </>
- ) : (
- <Menu>
<HeaderReportDropdown
+ submenuTitle={t('Manage email report')}
dashboardId={dashboardInfo.id}
- setShowReportSubMenu={this.setShowReportSubMenu}
- setIsDropdownVisible={setIsDropdownVisible}
- isDropdownVisible={isDropdownVisible}
+ setShowReportSubMenu={setShowReportSubMenu}
+ showReportModal={showReportModal}
+ showReportSubMenu={showReportSubMenu}
+ setCurrentReportDeleting={setCurrentReportDeleting}
useTextMenu
/>
- </Menu>
+ <Menu.Divider />
+ </>
+ ) : (
+ <HeaderReportDropdown
+ dashboardId={dashboardInfo.id}
+ setShowReportSubMenu={setShowReportSubMenu}
+ showReportModal={showReportModal}
+ setCurrentReportDeleting={setCurrentReportDeleting}
+ useTextMenu
+ />
)
) : null}
{editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && (
@@ -302,14 +291,13 @@ export class HeaderActionsDropdown extends PureComponent<
/>
</Menu.Item>
)}
-
<Menu.Item key={MenuKeys.AutorefreshModal}>
<RefreshIntervalModal
addSuccessToast={addSuccessToast}
refreshFrequency={refreshFrequency}
refreshLimit={refreshLimit}
refreshWarning={refreshWarning}
- onChange={this.changeRefreshInterval}
+ onChange={changeRefreshInterval}
editMode={editMode}
refreshIntervalOptions={refreshIntervalOptions}
triggerNode={<div>{t('Set auto-refresh interval')}</div>}
@@ -317,7 +305,18 @@ export class HeaderActionsDropdown extends PureComponent<
</Menu.Item>
</Menu>
);
- }
-}
+ }, [
+ 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 a683dfa2b2..3a9878f4fc 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) => (
<Provider store={store}>
<div className="dashboard-header">
- <HeaderActionsDropdown {...editModeOnProps} {...overrides} />
+ <HeaderActionsMenu {...editModeOnProps} {...overrides} />
</div>
</Provider>
);
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
index e1f20b5505..7c813809c0 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 12479d02f6..1c31b67529 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<string[]>([]);
// setting openKeys undefined falls back to uncontrolled behaviour
- const [openKeys, setOpenKeys] = useState<string[] | undefined>(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<string, string[]> = {};
- if (openKeys) {
- openKeysProps.openKeys = openKeys;
- }
-
const menu = (
<Menu
onClick={handleMenuClick}
- selectable={false}
data-test={`slice_${slice.slice_id}-menu`}
- selectedKeys={selectedKeys}
- onSelect={({ selectedKeys: keys }) => setSelectedKeys(keys)}
- openKeys={openKeys}
id={`slice_${slice.slice_id}-menu`}
- // submenus must be rendered for handleDropdownNavigation
- forceSubMenuRender
- {...openKeysProps}
+ selectable={false}
>
<Menu.Item
key={MenuKeys.ForceRefresh}
@@ -458,9 +447,7 @@ const SliceHeaderControls = (
emailBody={t('Check out this chart: ')}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
- setOpenKeys={setOpenKeys}
title={t('Share')}
- key={MenuKeys.Share}
/>
)}
@@ -532,22 +519,17 @@ const SliceHeaderControls = (
overlayStyle={dropdownOverlayStyle}
trigger={['click']}
placement="bottomRight"
- autoFocus
- forceRender
+ open={isDropdownVisible}
+ onOpenChange={visible => setIsDropdownVisible(visible)}
>
- <span
- css={() => css`
- display: flex;
- align-items: center;
- `}
+ <Button
+ type="link"
id={`slice_${slice.slice_id}-controls`}
- role="button"
aria-label="More Options"
aria-haspopup="true"
- tabIndex={0}
>
<VerticalDotsTrigger />
- </span>
+ </Button>
</NoAnimationDropdown>
<DrillDetailModal
formData={props.formData}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
index 8401ece73c..3f4b232bca 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
@@ -60,7 +60,7 @@ test('Should call download image on click', async () => {
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 505a9b8184..5490d7b805 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 (
- <Menu.Item key="download-image" {...rest}>
- <div onClick={onDownloadImage} role="button" tabIndex={0}>
- {text}
- </div>
+ <Menu.Item
+ key="download-image"
+ onClick={e => {
+ onDownloadImage(e.domEvent);
+ }}
+ >
+ {text}
</Menu.Item>
);
}
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 56916f4b64..ec6652ca41 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 a07a2e232c..47309b842a 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 (
- <Menu.Item key="download-pdf" {...rest}>
- <div onClick={onDownloadPdf} role="button" tabIndex={0}>
- {text}
- </div>
+ <Menu.Item
+ key="download-pdf"
+ onClick={e => {
+ onDownloadPdf(e.domEvent);
+ }}
+ >
+ {text}
</Menu.Item>
);
}
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 0f3049d847..2bd3a2cd9d 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(
- <Menu>
+ <Menu forceSubMenuRender>
<DownloadMenuItems {...createProps()} />
</Menu>,
{
@@ -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 d9ffaaaedf..cdb73d5e17 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<typeof Menu.SubMenu> {
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
- text={pdfMenuItemTitle}
- dashboardId={dashboardId}
- logEvent={logEvent}
- format={DownloadScreenshotFormat.PDF}
- {...rest}
- />
- <DownloadScreenshot
- text={imageMenuItemTitle}
- dashboardId={dashboardId}
- logEvent={logEvent}
- format={DownloadScreenshotFormat.PNG}
- {...rest}
- />
- </>
+ <Menu.SubMenu key={submenuKey} title={title} disabled={disabled} {...rest}>
+ <Menu.Item
+ key={DownloadScreenshotFormat.PDF}
+ onClick={() => downloadScreenshot(DownloadScreenshotFormat.PDF)}
+ >
+ {pdfMenuItemTitle}
+ </Menu.Item>
+ <Menu.Item
+ key={DownloadScreenshotFormat.PNG}
+ onClick={() => downloadScreenshot(DownloadScreenshotFormat.PNG)}
+ >
+ {imageMenuItemTitle}
+ </Menu.Item>
+ </Menu.SubMenu>
) : (
- <>
+ <Menu.SubMenu key={submenuKey} title={title} disabled={disabled} {...rest}>
<DownloadAsPdf
text={pdfMenuItemTitle}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
- {...rest}
/>
<DownloadAsImage
text={imageMenuItemTitle}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
- {...rest}
/>
- </>
+ </Menu.SubMenu>
);
};
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 16bbcff9ec..f290a1a4b3 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 6c5468da24..4fb191a067 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<typeof Menu.SubMenu> {
url?: string;
copyMenuItemTitle: string;
emailMenuItemTitle: string;
@@ -38,8 +38,8 @@ interface ShareMenuItemProps {
shareByEmailMenuItemRef?: RefObject<any>;
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 (
- <Menu.SubMenu title={title} key={key}>
+ <Menu.SubMenu
+ title={title}
+ key={MenuKeys.Share}
+ disabled={disabled}
+ {...rest}
+ >
<Menu.Item key={MenuKeys.CopyLink} onClick={() => onCopyLink()}>
{copyMenuItemTitle}
</Menu.Item>
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 b073fe655e..45a34b110f 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 404d737999..7b617abf18 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<Pick<MenuProps, 'onSelect'>>['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: (
- <FilterConfigurationLink
- dashboardId={dashboardId}
- createNewOnOpen={filterValues.length === 0}
- >
+ <FilterConfigurationLink>
{t('Add or edit filters')}
</FilterConfigurationLink>
),
- 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: (
+ <Space>
+ {t('Vertical (Left)')}
+ {selectedFilterBarOrientation ===
+ FilterBarOrientation.Vertical && <Icons.Check />}
+ </Space>
+ ),
},
{
key: FilterBarOrientation.Horizontal,
- label: t('Horizontal (Top)'),
+ label: (
+ <Space>
+ {t('Horizontal (Top)')}
+ {selectedFilterBarOrientation ===
+ FilterBarOrientation.Horizontal && <Icons.Check />}
+ </Space>
+ ),
},
],
+ ...{ 'data-test': 'dropdown-selectable-icon-submenu' },
});
}
return items;
}, [
+ selectedFilterBarOrientation,
canEdit,
canSetHorizontalFilterBar,
crossFiltersMenuItem,
@@ -226,19 +255,24 @@ const FilterBarSettings = () => {
return (
<>
- <DropdownSelectableIcon
- onSelect={handleSelect}
- icon={
+ <Dropdown
+ menu={{
+ onClick: handleClick,
+ items: menuItems,
+ selectedKeys: [selectedFilterBarOrientation],
+ }}
+ trigger={['click']}
+ >
+ <Button type="link">
<Icons.Gear
name="gear"
iconColor={theme.colors.grayscale.base}
data-test="filterbar-orientation-icon"
/>
- }
- menuItems={menuItems}
- selectedKeys={[selectedFilterBarOrientation]}
- />
+ </Button>
+ </Dropdown>
{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 0b5a18835d..7ce156a7c4 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(<FilterConfigurationLink>Config link</FilterConfigurationLink>, {
- useRedux: true,
- });
+ const showModal = jest.fn();
+ render(
+ <FilterConfigurationLink onClick={showModal}>
+ Config link
+ </FilterConfigurationLink>,
+ {
+ 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 6b4fea62a4..367072fbab 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<FCBProps> = ({
- 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 (
- <>
- <div
- {...getFilterBarTestId('create-filter')}
- onClick={handleClick}
- role="button"
- tabIndex={0}
- >
- {children}
- </div>
- <FiltersConfigModal
- isOpen={isOpen}
- onSave={submit}
- onCancel={close}
- initialFilterId={initialFilterId}
- createNewOnOpen={createNewOnOpen}
- key={`filters-for-${dashboardId}`}
- />
- </>
- );
-};
+}) => (
+ <div
+ {...getFilterBarTestId('create-filter')}
+ onClick={onClick}
+ role="button"
+ tabIndex={0}
+ >
+ {children}
+ </div>
+);
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 0000000000..2a08f7800f
--- /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<void>;
+ 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 ? (
+ <FiltersConfigModal
+ isOpen={isFilterConfigModalOpen}
+ onSave={handleFilterSave}
+ onCancel={closeFilterConfigModal}
+ key={`filters-for-${dashboardId}`}
+ createNewOnOpen={createNewOnOpen}
+ initialFilterId={initialFilterId}
+ />
+ ) : 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 b82b96767f..0d6887f0c5 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 01679b1926..9346a72ab5 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 (
<Row
css={(theme: SupersetTheme) => css`
@@ -58,9 +65,10 @@ export const NameRow = ({
</InternalRow>
{canEdit && (
<FilterConfigurationLink
- dashboardId={dashboardId}
- onClick={hidePopover}
- initialFilterId={filter.id}
+ onClick={() => {
+ openFilterConfigModal();
+ hidePopover();
+ }}
>
<Icons.Edit
iconSize="l"
@@ -71,6 +79,7 @@ export const NameRow = ({
/>
</FilterConfigurationLink>
)}
+ {FilterConfigModalComponent}
</Row>
);
};
diff --git a/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
new file mode 100644
index 0000000000..bc8ef7d1ff
--- /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<NodeJS.Timeout[]>([]);
+
+ 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 0aae5aa4ff..f3f0d753bf 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 97bd8808eb..69718f55fb 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}
/>
)}
+
+ <ReportModal
+ userId={user.userId}
+ show={isReportModalOpen}
+ onHide={closeReportModal}
+ userEmail={user.email}
+ dashboardId={dashboardId}
+ chart={chart}
+ creationMethod="charts"
+ />
+
+ {currentReportDeleting && (
+ <DeleteModal
+ description={t(
+ 'This action will permanently delete %s.',
+ currentReportDeleting?.name,
+ )}
+ onConfirm={() => {
+ 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 2490ee7544..cc7fd23d30 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 (
- <AntdDropdown
+ <Dropdown
trigger={['click']}
- overlay={
+ dropdownRender={() => (
<Menu onClick={handleMenuClick} selectable={false}>
<Menu.Item key={MenuKeys.ExportOriginal}>
<MenuItemContent>
@@ -84,9 +84,9 @@ export const ExportToCSVDropdown = ({
</MenuItemContent>
</Menu.Item>
</Menu>
- }
+ )}
>
{children}
- </AntdDropdown>
+ </Dropdown>
);
};
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 130e96443a..92694dc1ae 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(<DatasourceControl {...{ ...props, isEditable: false }} />, {
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 4e5063b1e4..3070cdc7d1 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 && (
<WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
)}
- <AntdDropdown
- overlay={
+ <Dropdown
+ dropdownRender={() =>
datasource.type === DatasourceType.Query
? queryDatasourceMenu
: defaultDatasourceMenu
@@ -423,7 +419,7 @@ class DatasourceControl extends PureComponent {
className="datasource-modal-trigger"
data-test="datasource-menu-trigger"
/>
- </AntdDropdown>
+ </Dropdown>
</div>
{/* 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 21cf69cec4..6f7b64bc2e 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 = (
<>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
- icon={<Icons.FileOutlined css={iconReset} />}
+ icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to original .CSV')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
- icon={<Icons.FileOutlined css={iconReset} />}
+ icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to pivoted .CSV')}
@@ -346,7 +341,7 @@ export const useExploreAdditionalActionsMenu = (
) : (
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
- icon={<Icons.FileOutlined css={iconReset} />}
+ icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to .CSV')}
@@ -354,20 +349,20 @@ export const useExploreAdditionalActionsMenu = (
)}
<Menu.Item
key={MENU_KEYS.EXPORT_TO_JSON}
- icon={<Icons.FileOutlined css={iconReset} />}
+ icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to .JSON')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
- icon={<Icons.FileImageOutlined css={iconReset} />}
+ icon={<Icons.FileImageOutlined />}
>
{t('Download as image')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_XLSX}
- icon={<Icons.FileOutlined css={iconReset} />}
+ icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to Excel')}
@@ -403,28 +398,25 @@ export const useExploreAdditionalActionsMenu = (
<Menu.Divider />
{showReportSubMenu ? (
<>
- <Menu.SubMenu title={t('Manage email report')}>
- <HeaderReportDropDown
- chart={chart}
- setShowReportSubMenu={setShowReportSubMenu}
- showReportSubMenu={showReportSubMenu}
- setIsDropdownVisible={setIsDropdownVisible}
- isDropdownVisible={isDropdownVisible}
- useTextMenu
- />
- </Menu.SubMenu>
- <Menu.Divider />
- </>
- ) : (
- <Menu>
<HeaderReportDropDown
+ submenuTitle={t('Manage email report')}
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
- setIsDropdownVisible={setIsDropdownVisible}
- isDropdownVisible={isDropdownVisible}
+ showReportSubMenu={showReportSubMenu}
+ showReportModal={showReportModal}
+ setCurrentReportDeleting={setCurrentReportDeleting}
useTextMenu
/>
- </Menu>
+ <Menu.Divider />
+ </>
+ ) : (
+ <HeaderReportDropDown
+ chart={chart}
+ setShowReportSubMenu={setShowReportSubMenu}
+ showReportModal={showReportModal}
+ setCurrentReportDeleting={setCurrentReportDeleting}
+ useTextMenu
+ />
)}
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
<ModalTrigger
diff --git a/superset-frontend/src/features/charts/ChartCard.tsx b/superset-frontend/src/features/charts/ChartCard.tsx
index 9dd18d1a89..76554c00b8 100644
--- a/superset-frontend/src/features/charts/ChartCard.tsx
+++ b/superset-frontend/src/features/charts/ChartCard.tsx
@@ -24,7 +24,7 @@ import Chart from 'src/types/Chart';
import ListViewCard from 'src/components/ListViewCard';
import Label from 'src/components/Label';
-import { AntdDropdown } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import FaveStar from 'src/components/FaveStar';
import FacePile from 'src/components/FacePile';
@@ -172,9 +172,9 @@ export default function ChartCard({
isStarred={favoriteStatus}
/>
)}
- <AntdDropdown overlay={menu}>
+ <Dropdown dropdownRender={() => menu}>
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
- </AntdDropdown>
+ </Dropdown>
</ListViewCard.Actions>
}
/>
diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx
index 2b09233d96..dff8b638b9 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}
/>
)}
- <AntdDropdown overlay={menu}>
+ <Dropdown dropdownRender={() => menu}>
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
- </AntdDropdown>
+ </Dropdown>
</ListViewCard.Actions>
}
/>
diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx
index 5dd8c0f883..a9518fdcbd 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -409,6 +409,7 @@ const RightMenu = ({
{RightMenuExtension && <RightMenuExtension />}
{!navbarRight.user_is_anonymous && showActionDropdown && (
<StyledSubMenu
+ key="sub1"
data-test="new-dropdown"
title={
<StyledI data-test="new-dropdown-icon" className="fa fa-plus" />
@@ -474,6 +475,7 @@ const RightMenu = ({
</StyledSubMenu>
)}
<StyledSubMenu
+ key="sub3_settings"
title={t('Settings')}
icon={<Icons.TriangleDown iconSize="xl" />}
>
diff --git a/superset-frontend/src/features/home/SavedQueries.tsx b/superset-frontend/src/features/home/SavedQueries.tsx
index fdc8cb8648..12683f581f 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();
}}
>
- <AntdDropdown overlay={renderMenu(q)}>
+ <Dropdown dropdownRender={() => renderMenu(q)}>
<Icons.MoreVert
iconColor={theme.colors.grayscale.base}
/>
- </AntdDropdown>
+ </Dropdown>
</ListViewCard.Actions>
</QueryData>
}
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 11fb215c35..51fa418689 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(
- <div>
+ <Menu>
<HeaderReportDropdown {...props} />
- </div>,
+ </Menu>,
{ 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 66a8195f1f..9f05232809 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<any, AlertObject>(state => {
@@ -156,22 +152,13 @@ export default function HeaderReportDropDown({
return permissions.some(permission => permission.length > 0);
};
- const [currentReportDeleting, setCurrentReportDeleting] =
- useState<AlertObject | null>(null);
- const theme = useTheme();
const prevDashboard = usePrevious(dashboardId);
- const [showModal, setShowModal] = useState<boolean>(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) ? (
- <Menu selectable={false} {...rest} css={onMenuHover}>
+ <Menu.SubMenu title={submenuTitle} css={onMenuHover}>
<Menu.Item onClick={handleShowMenu}>
{DropdownItemExtension ? (
<StyledDropdownItemWithIcon>
@@ -233,9 +208,14 @@ export default function HeaderReportDropDown({
)}
</Menu.Item>
<Menu.Divider />
- </Menu>
+ </Menu.SubMenu>
) : (
- <Menu selectable={false} css={{ border: 'none' }}>
+ <Menu.SubMenu
+ title={submenuTitle}
+ css={css`
+ border: none;
+ `}
+ >
<Menu.Item
css={onMenuItemHover}
onClick={() => toggleActiveKey(report, !isReportActive)}
@@ -251,10 +231,15 @@ export default function HeaderReportDropDown({
<Menu.Item css={onMenuItemHover} onClick={handleDeleteMenuClick}>
{t('Delete email report')}
</Menu.Item>
- </Menu>
+ </Menu.SubMenu>
);
- const menu = () => (
- <Menu selectable={false} css={{ width: '200px' }}>
+ const menu = (title: ReactNode) => (
+ <Menu.SubMenu
+ title={title}
+ css={css`
+ width: 200px;
+ `}
+ >
<Menu.Item>
{t('Email reports active')}
<Switch
@@ -262,10 +247,12 @@ export default function HeaderReportDropDown({
checked={isReportActive}
onClick={(checked: boolean) => toggleActiveKey(report, checked)}
size="small"
- css={{ marginLeft: theme.gridUnit * 2 }}
+ css={theme => css`
+ margin-left: ${theme.gridUnit * 2}px;
+ `}
/>
</Menu.Item>
- <Menu.Item onClick={() => setShowModal(true)}>
+ <Menu.Item onClick={() => showReportModal()}>
{t('Edit email report')}
</Menu.Item>
<Menu.Item
@@ -274,7 +261,7 @@ export default function HeaderReportDropDown({
>
{t('Delete email report')}
</Menu.Item>
- </Menu>
+ </Menu.SubMenu>
);
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()}
>
<Icons.Calendar />
</span>
) : (
- <>
- <NoAnimationDropdown
- overlay={menu()}
- overlayStyle={dropdownOverlayStyle}
- trigger={['click']}
- getPopupContainer={(triggerNode: any) =>
- triggerNode.closest('.action-button')
- }
- >
- <span
- role="button"
- className="action-button action-schedule-report"
- tabIndex={0}
- >
- <Icons.Calendar />
- </span>
- </NoAnimationDropdown>
- </>
+ menu(<Icons.Calendar />)
);
-
- return (
- <>
- {canAddReports() && (
- <>
- <ReportModal
- userId={user.userId}
- show={showModal}
- onHide={() => setShowModal(false)}
- userEmail={user.email}
- dashboardId={dashboardId}
- chart={chart}
- creationMethod={
- dashboardId ? CreationMethod.Dashboards : CreationMethod.Charts
- }
- />
- {isDropdownVisible ? (useTextMenu ? textMenu() : iconMenu()) : null}
- {currentReportDeleting && (
- <DeleteModal
- description={t(
- 'This action will permanently delete %s.',
- currentReportDeleting?.name,
- )}
- onConfirm={() => {
- 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 6b68868695..092ba90f12 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();
}}
>
- <AntdDropdown overlay={menu}>
+ <Dropdown dropdownRender={() => menu}>
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
- </AntdDropdown>
+ </Dropdown>
</ListViewCard.Actions>
}
/>