diff --git a/superset-frontend/src/GlobalStyles.tsx b/superset-frontend/src/GlobalStyles.tsx index c305fb214a0a8..b65cc6d35b2b2 100644 --- a/superset-frontend/src/GlobalStyles.tsx +++ b/superset-frontend/src/GlobalStyles.tsx @@ -99,11 +99,11 @@ export const GlobalStyles = () => ( margin-right: 0; } } - .ant-dropdown-menu-sub .antd5-menu.antd5-menu-vertical { + .antd5-dropdown-menu-sub .antd5-menu.antd5-menu-vertical { box-shadow: none; } - .ant-dropdown-menu-submenu-title, - .ant-dropdown-menu-item { + .antd5-dropdown-menu-submenu-title, + .antd5-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 df35351e5c231..21b979fe5b3dc 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx @@ -18,7 +18,7 @@ */ import { useDispatch } from 'react-redux'; import { styled, useTheme, t } 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'; import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab'; @@ -36,7 +36,7 @@ export function convertToNumWithSpaces(num: number) { const LimitSelectStyled = styled.span` ${({ theme }) => ` - .ant-dropdown-trigger { + .antd5-dropdown-trigger { align-items: center; color: ${theme.colors.grayscale.dark2}; display: flex; @@ -95,8 +95,8 @@ const QueryLimitSelect = ({ return ( - renderQueryLimit(maxRow, setQueryLimit)} trigger={['click']} > e.preventDefault()}> @@ -106,7 +106,7 @@ const QueryLimitSelect = ({ - + ); }; diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx index 2acd7665bfb15..6cb17bc2b4ff3 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx @@ -16,12 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { FC } from 'react'; -import { t, useTheme, styled } from '@superset-ui/core'; +import { t, useTheme } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import { DropdownButton } from 'src/components/DropdownButton'; import Button from 'src/components/Button'; -import { DropdownButtonProps } from 'antd/lib/dropdown'; interface SaveDatasetActionButtonProps { setShowSave: (arg0: boolean) => void; @@ -34,34 +32,14 @@ const SaveDatasetActionButton = ({ }: SaveDatasetActionButtonProps) => { const theme = useTheme(); - const StyledDropdownButton = styled( - DropdownButton as FC, - )` - &.ant-dropdown-button button.ant-btn.ant-btn-default { - font-weight: ${theme.gridUnit * 150}; - background-color: ${theme.colors.primary.light4}; - color: ${theme.colors.primary.dark1}; - &:nth-of-type(2) { - &:before, - &:hover:before { - border-left: 2px solid ${theme.colors.primary.dark2}; - } - } - } - span[name='caret-down'] { - margin-left: ${theme.gridUnit * 1}px; - color: ${theme.colors.primary.dark2}; - } - `; - return !overlayMenu ? ( setShowSave(true)} buttonStyle="primary"> {t('Save')} ) : ( - setShowSave(true)} - overlay={overlayMenu} + dropdownRender={() => overlayMenu} icon={ {t('Save')} - + ); }; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 4d1aded5be29f..ab4797d51327e 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'; @@ -860,9 +861,12 @@ const SqlEditor: FC = ({ - + renderDropdown()} + trigger={['click']} + > - + > )} diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx index d8f4c8e4ed072..01594e210dae7 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,7 +88,7 @@ const SqlEditorTabHeader: FC = ({ queryEditor }) => { return ( - diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index 9b3752152ea25..7390b606e5bbf 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -42,7 +42,7 @@ 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 { DrillDetailMenuItems } from '../DrillDetail'; import { getMenuAdjustedY } from '../utils'; @@ -115,7 +115,7 @@ const ChartContextMenu = ( const [drillModalIsOpen, setDrillModalIsOpen] = useState(false); - const menuItems = []; + const menuItems: React.JSX.Element[] = []; const showDrillToDetail = isFeatureEnabled(FeatureFlag.DrillToDetail) && @@ -288,7 +288,7 @@ const ChartContextMenu = ( return ReactDOM.createPortal( <> ( {t('No actions')} )} - } + )} trigger={['click']} - onVisibleChange={value => { + onOpenChange={value => { setVisible(value); if (!value) { setOpenKeys([]); } }} - visible={visible} + open={visible} > ( - ( + diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx index ef81bd42ccd07..c4547b9a10a7b 100644 --- a/superset-frontend/src/components/Dropdown/index.tsx +++ b/superset-frontend/src/components/Dropdown/index.tsx @@ -24,13 +24,7 @@ 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, -} from 'antd-v5'; -import { DropDownProps } from 'antd/lib/dropdown'; +import { Dropdown as AntdDropdown, DropDownProps } from 'antd-v5'; import { styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -83,7 +77,8 @@ export enum IconOrientation { Vertical = 'vertical', Horizontal = 'horizontal', } -export interface DropdownProps extends DropDownProps { + +export interface MenuDotsDropdownProps extends DropDownProps { overlay: ReactElement; iconOrientation?: IconOrientation; } @@ -100,19 +95,19 @@ const RenderIcon = ( return component; }; -export const Dropdown = ({ +export const MenuDotsDropdown = ({ overlay, iconOrientation = IconOrientation.Vertical, ...rest -}: DropdownProps) => ( - +}: MenuDotsDropdownProps) => ( + overlay} {...rest}> {RenderIcon(iconOrientation)} ); -export interface NoAnimationDropdownProps extends Antd5DropdownProps { +export interface NoAnimationDropdownProps extends DropDownProps { children: ReactNode; onBlur?: (e: FocusEvent) => void; onKeyDown?: (e: KeyboardEvent) => void; @@ -126,8 +121,13 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => { }); return ( - + {childrenWithProps} - + ); }; + +export const Dropdown = Object.assign(AntdDropdown, { + Button: AntdDropdown.Button, +}); +export type DropdownProps = DropDownProps; diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx index 32a7739e3c308..e41ff143ca1db 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 'src/components/Dropdown'; +import { Tooltip, TooltipPlacement } from 'src/components/Tooltip'; import { kebabCase } from 'lodash'; -const StyledDropdownButton = styled.div` - .ant-btn-group { - button.ant-btn { - background-color: ${({ theme }) => theme.colors.primary.dark1}; - border-color: transparent; - color: ${({ theme }) => theme.colors.grayscale.light5}; - font-size: 12px; - line-height: 13px; - outline: none; - &:first-of-type { - border-radius: ${({ theme }) => - `${theme.gridUnit}px 0 0 ${theme.gridUnit}px`}; - margin: 0; - } - - &:disabled { - background-color: ${({ theme }) => theme.colors.grayscale.light2}; - color: ${({ theme }) => theme.colors.grayscale.base}; - } - &:nth-of-type(2) { - margin: 0; - border-radius: ${({ theme }) => - `0 ${theme.gridUnit}px ${theme.gridUnit}px 0`}; - width: ${({ theme }) => theme.gridUnit * 9}px; - &:before, - &:hover:before { - border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5}; - content: ''; - display: block; - height: ${({ theme }) => theme.gridUnit * 8}px; - margin: 0; - position: absolute; - width: ${({ theme }) => theme.gridUnit * 0.25}px; - } - - &:disabled:before { - border-left: 1px solid ${({ theme }) => theme.colors.grayscale.base}; - } - } - } - } -`; - -export interface DropdownButtonProps { - overlay: ReactElement; +export type DropdownButtonProps = ComponentProps & { tooltip?: string; - placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; - buttonsRender?: ((buttons: ReactNode[]) => ReactNode[]) | undefined; -} + tooltipPlacement?: TooltipPlacement; +}; export const DropdownButton = ({ - overlay, + dropdownRender, tooltip, - placement, + tooltipPlacement, + children, ...rest }: DropdownButtonProps) => { - const buildButton = ( - props: { - buttonsRender?: DropdownButtonProps['buttonsRender']; - } = {}, - ) => ( - - - + const button = ( + + {children} + ); if (tooltip) { - return buildButton({ - buttonsRender: ([leftButton, rightButton]) => [ - - {leftButton} - , - rightButton, - ], - }); + return ( + + {button} + + ); } - return buildButton(); + return button; }; diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index 8d791929d3719..65a60a9b836c5 100644 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx @@ -16,12 +16,11 @@ * 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 { useTheme } from '@superset-ui/core'; +import { 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'; +import { Dropdown } from '../Dropdown'; const { SubMenu } = Menu; @@ -40,69 +39,7 @@ export interface DropDownSelectableProps extends Pick { selectedKeys?: string[]; } -const StyledDropdownButton = styled(DropdownButton as FC)` - button.ant-btn:first-of-type { - display: none; - } - > button.ant-btn:nth-of-type(2) { - display: inline-flex; - background-color: transparent !important; - height: unset; - padding: 0; - border: none; - width: auto !important; - - .anticon { - line-height: 0; - } - &:after { - box-shadow: none !important; - } - } -`; - -const StyledMenu = styled(Menu)` - ${({ theme }) => ` - box-shadow: - 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)}, - 0 6px 16px 0 - ${addAlpha(theme.colors.grayscale.dark2, 0.08)}, - 0 9px 28px 8px - ${addAlpha(theme.colors.grayscale.dark2, 0.05)}; - .info { - font-size: ${theme.typography.sizes.s}px; - color: ${theme.colors.grayscale.base}; - padding: ${theme.gridUnit}px ${theme.gridUnit * 3}px ${ - theme.gridUnit - }px ${theme.gridUnit * 3}px; - } - .ant-dropdown-menu-item-selected { - color: ${theme.colors.grayscale.dark1}; - background-color: ${theme.colors.primary.light5}; - } - `} -`; - -const StyleMenuItem = styled(Menu.Item)<{ divider?: boolean }>` - display: flex; - justify-content: space-between; - > span { - width: 100%; - } - border-bottom: ${({ divider, theme }) => - divider ? `1px solid ${theme.colors.grayscale.light3};` : 'none;'}; -`; - -const StyleSubmenuItem = styled.div` - display: flex; - justify-content: space-between; - width: 100%; - > div { - flex-grow: 1; - } -`; - -export default (props: DropDownSelectableProps) => { +const DropdownSelectableIcon = (props: DropDownSelectableProps) => { const theme = useTheme(); const [visible, setVisible] = useState(false); const { icon, info, menuItems, selectedKeys, onSelect } = props; @@ -117,8 +54,8 @@ export default (props: DropDownSelectableProps) => { }; const menuItem = useMemo( () => (label: string | ReactNode, key: string, divider?: boolean) => ( - - + <> + {label} {selectedKeys?.includes(key) && ( { iconSize="xl" /> )} - - + + {divider && } + > ), [selectedKeys, theme.colors.primary.base], ); @@ -141,7 +79,7 @@ export default (props: DropDownSelectableProps) => { {info} )} - { menuItem(m.label, m.key, m.divider) ), )} - + > ), [selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect], ); return ( - overlayMenu} trigger={['click']} - icon={icon} - visible={visible} - onVisibleChange={handleVisibleChange} - /> + open={visible} + onOpenChange={handleVisibleChange} + > + {icon} + ); }; + +export default DropdownSelectableIcon; diff --git a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx index 38fc7cbe94f76..61d2c70a31cde 100644 --- a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx +++ b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx @@ -17,7 +17,7 @@ * under the License. */ import { action } from '@storybook/addon-actions'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; import FaveStar from 'src/components/FaveStar'; @@ -70,8 +70,8 @@ export const SupersetListViewCard = ({ saveFaveStar={action('saveFaveStar')} isStarred={isStarred} /> - ( Delete @@ -80,10 +80,10 @@ export const SupersetListViewCard = ({ Edit - } + )} > - + } /> diff --git a/superset-frontend/src/components/Menu/index.tsx b/superset-frontend/src/components/Menu/index.tsx index ff2c1c0b6ec2e..de55ad689409a 100644 --- a/superset-frontend/src/components/Menu/index.tsx +++ b/superset-frontend/src/components/Menu/index.tsx @@ -81,7 +81,7 @@ const StyledMenu = styled(AntdMenu)` border-bottom: 1px solid transparent; } &.antd5-menu-vertical, - &.ant-dropdown-menu { + &.antd5-dropdown-menu { box-shadow: 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)}, 0 6px 16px 0 @@ -146,8 +146,8 @@ const StyledSubMenu = styled(AntdMenu.SubMenu)` } } - .ant-dropdown-menu-submenu-arrow:before, - .ant-dropdown-menu-submenu-arrow:after { + .antd5-dropdown-menu-submenu-arrow:before, + .antd5-dropdown-menu-submenu-arrow:after { content: none !important; } `; diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx index b84e67b31f939..68c33d78c7e8c 100644 --- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx @@ -18,7 +18,7 @@ */ import { ReactNode, ReactElement } from 'react'; import { css, SupersetTheme, t, useTheme } from '@superset-ui/core'; -import { AntdDropdown, AntdDropdownProps } from 'src/components'; +import { Dropdown, DropdownProps } from 'src/components/Dropdown'; import { TooltipPlacement } from 'src/components/Tooltip'; import { DynamicEditableTitle, @@ -116,7 +116,7 @@ export type PageHeaderWithActionsProps = { titlePanelAdditionalItems: ReactNode; rightPanelAdditionalItems: ReactNode; additionalActionsMenu: ReactElement; - menuDropdownProps: Omit; + menuDropdownProps: Omit; tooltipProps?: { text?: string; placement?: TooltipPlacement; @@ -155,9 +155,9 @@ export const PageHeaderWithActions = ({ {rightPanelAdditionalItems} {showMenuDropdown && ( - additionalActionsMenu} {...menuDropdownProps} > - + )} diff --git a/superset-frontend/src/components/PopoverDropdown/index.tsx b/superset-frontend/src/components/PopoverDropdown/index.tsx index 9ad507d5f72a2..41812ea13f5de 100644 --- a/superset-frontend/src/components/PopoverDropdown/index.tsx +++ b/superset-frontend/src/components/PopoverDropdown/index.tsx @@ -19,7 +19,7 @@ import { Key } from 'react'; import cx from 'classnames'; import { styled, useTheme } from '@superset-ui/core'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; @@ -89,10 +89,10 @@ const PopoverDropdown = (props: PopoverDropdownProps) => { const theme = useTheme(); const selected = options.find(opt => opt.value === value); return ( - ( onChange(key)}> {options.map(option => ( { ))} - } + )} > {selected && renderButton(selected)} @@ -115,7 +115,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => { css={{ marginTop: theme.gridUnit * 0.5 }} /> - + ); }; diff --git a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx index 675bffe493bc8..ad958ff122c09 100644 --- a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx +++ b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx @@ -18,7 +18,7 @@ */ import { useState, useEffect } from 'react'; import { styled } from '@superset-ui/core'; -import { Dropdown, IconOrientation } from 'src/components/Dropdown'; +import { MenuDotsDropdown, IconOrientation } from 'src/components/Dropdown'; import { Menu, MenuProps } from 'src/components/Menu'; /** @@ -126,7 +126,7 @@ export function ActionCell(props: ActionCellProps) { setVisible(flag); }; return ( - ( - <> - - > + ); diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts index 50ce180eb392f..273dde2e9511b 100644 --- a/superset-frontend/src/components/index.ts +++ b/superset-frontend/src/components/index.ts @@ -55,7 +55,6 @@ export { Card as AntdCard, Checkbox as AntdCheckbox, Collapse as AntdCollapse, - Dropdown as AntdDropdown, Form as AntdForm, Input as AntdInput, Select as AntdSelect, diff --git a/superset-frontend/src/dashboard/components/CssEditor/index.tsx b/superset-frontend/src/dashboard/components/CssEditor/index.tsx index 996afcca4407b..e26744b5696d6 100644 --- a/superset-frontend/src/dashboard/components/CssEditor/index.tsx +++ b/superset-frontend/src/dashboard/components/CssEditor/index.tsx @@ -17,7 +17,7 @@ * under the License. */ import { Key, ReactNode, PureComponent } from 'react'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import Button from 'src/components/Button'; import { t, styled, SupersetClient } from '@superset-ui/core'; @@ -114,9 +114,9 @@ class CssEditor extends PureComponent { ); return ( - + menu} placement="bottomRight"> {t('Load a CSS template')} - + ); } return null; diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index 090f12a2bd079..59370e421bc7c 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -23,7 +23,7 @@ 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 '.'; +import HeaderActionsDropdown from '.'; const createProps = (): HeaderDropdownProps => ({ addSuccessToast: jest.fn(), diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx index 6bc712f6b8490..3c90ce1d937d1 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { PureComponent } from 'react'; +import { useState } from 'react'; import { isEmpty } from 'lodash'; import { connect } from 'react-redux'; -import { t } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; import { URL_PARAMS } from 'src/constants'; import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; @@ -36,65 +35,70 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; import { getUrlParam } from 'src/utils/urlUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; +import { t } from '@superset-ui/core'; 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); - }); - } - } +const HeaderActionsDropdown: React.FC = props => { + const [css, setCss] = useState(props.customCss || ''); + const [showReportSubMenu, setShowReportSubMenu] = useState( + null, + ); - setShowReportSubMenu = (show: boolean) => { - this.setState({ showReportSubMenu: show }); - }; + 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 + } = props; - changeCss = (css: string) => { - this.props.onChange(); - this.props.updateCss(css); + const changeCss = (newCss: string) => { + props.onChange(); + props.updateCss(newCss); + setCss(newCss); + injectCustomCss(newCss); }; - changeRefreshInterval = (refreshInterval: number, isPersistent: boolean) => { - this.props.setRefreshFrequency(refreshInterval, isPersistent); - this.props.startPeriodicRender(refreshInterval * 1000); + const changeRefreshInterval = ( + refreshInterval: number, + isPersistent: boolean, + ) => { + props.setRefreshFrequency(refreshInterval, isPersistent); + props.startPeriodicRender(refreshInterval * 1000); }; - handleMenuClick = ({ key }: Record) => { + const handleMenuClick = ({ key }: Record) => { switch (key) { case MenuKeys.RefreshDashboard: - this.props.forceRefreshAllCharts(); - this.props.addSuccessToast(t('Refreshing charts')); + props.forceRefreshAllCharts(); + props.addSuccessToast(t('Refreshing charts')); break; case MenuKeys.EditProperties: - this.props.showPropertiesModal(); + props.showPropertiesModal(); break; case MenuKeys.ToggleFullscreen: { const url = getDashboardUrl({ @@ -106,218 +110,176 @@ export class HeaderActionsDropdown extends PureComponent< window.location.replace(url); break; } - case MenuKeys.ManageEmbedded: { - this.props.manageEmbedded(); + case MenuKeys.ManageEmbedded: + props.manageEmbedded(); break; - } default: break; } }; - render() { - const { - dashboardTitle, - dashboardId, - dashboardInfo, - refreshFrequency, - shouldPersistRefreshFrequency, - editMode, - customCss, - colorNamespace, - colorScheme, - layout, - expandedSlices, - onSave, - userCanEdit, - userCanShare, - userCanSave, - userCanCurate, - isLoading, - refreshLimit, - refreshWarning, - lastModifiedTime, - addSuccessToast, - addDangerToast, - setIsDropdownVisible, - isDropdownVisible, - directPathToChild, - ...rest - } = this.props; - - const emailTitle = t('Superset dashboard'); - const emailSubject = `${emailTitle} ${dashboardTitle}`; - const emailBody = t('Check out this dashboard: '); - - const isEmbedded = !dashboardInfo?.userId; - - const url = 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 refreshIntervalOptions = - dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS; + const isEmbedded = !dashboardInfo?.userId; + const url = getDashboardUrl({ + pathname: window.location.pathname, + filters: getActiveFilters(), + hash: window.location.hash, + }); - const dashboardComponentId = [...(directPathToChild || [])].pop(); - - return ( - - {!editMode && ( - - {t('Refresh dashboard')} - - )} - {!editMode && !isEmbedded && ( - - {getUrlParam(URL_PARAMS.standalone) - ? t('Exit fullscreen') - : t('Enter fullscreen')} - - )} - {editMode && ( - - {t('Edit properties')} - - )} - {editMode && ( - - {t('Edit CSS')}} - initialCss={this.state.css} - onChange={this.changeCss} - addDangerToast={addDangerToast} - /> - - )} - - {userCanSave && ( - - {t('Save as')} - } - canOverwrite={userCanEdit} - /> - - )} - + {!editMode && ( + - + )} + {!editMode && !isEmbedded && ( + + {getUrlParam(URL_PARAMS.standalone) + ? t('Exit fullscreen') + : t('Enter fullscreen')} + + )} + {editMode && ( + + {t('Edit properties')} + + )} + {editMode && ( + + {t('Edit CSS')}} + initialCss={css} + onChange={changeCss} + addDangerToast={addDangerToast} /> - - {userCanShare && ( - + )} + + {userCanSave && ( + + {t('Save as')} + } + canOverwrite={userCanEdit} /> - )} - {!editMode && userCanCurate && ( - - {t('Embed dashboard')} - - )} - - {!editMode ? ( - this.state.showReportSubMenu ? ( - <> - - - - - > - ) : ( - + + )} + + {userCanShare && ( + + )} + {!editMode && userCanCurate && ( + + {t('Embed dashboard')} + + )} + + {!editMode ? ( + showReportSubMenu ? ( + <> + - - ) - ) : null} - {editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && ( - - {t('Set filter mapping')} - } + + + > + ) : ( + + - - )} - - - {t('Set auto-refresh interval')}} + + ) + ) : null} + {editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && ( + + {t('Set filter mapping')}} /> - - ); - } -} + )} + + + {t('Set auto-refresh interval')}} + /> + + + ); +}; export default connect(mapStateToProps)(HeaderActionsDropdown); diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 5ce86cc52f12c..7665ab20800c1 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -817,7 +817,7 @@ const Header = () => { )} ({ 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, diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 25750dcca1121..d3301d5705cb4 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -242,7 +242,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'; @@ -461,7 +461,7 @@ const SliceHeaderControls = ( addDangerToast={addDangerToast} setOpenKeys={setOpenKeys} title={t('Share')} - key={MenuKeys.Share} + submenuKey={MenuKeys.Share} /> )} diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx index 505a9b8184ae1..5490d7b8059ad 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx @@ -27,7 +27,6 @@ export default function DownloadAsImage({ text, logEvent, dashboardTitle, - ...rest }: { text: string; dashboardTitle: string; @@ -46,10 +45,13 @@ export default function DownloadAsImage({ }; return ( - - - {text} - + { + onDownloadImage(e.domEvent); + }} + > + {text} ); } diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx index a07a2e232c6dc..47309b842a9af 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx @@ -27,7 +27,6 @@ export default function DownloadAsPdf({ text, logEvent, dashboardTitle, - ...rest }: { text: string; dashboardTitle: string; @@ -46,10 +45,13 @@ export default function DownloadAsPdf({ }; return ( - - - {text} - + { + onDownloadPdf(e.domEvent); + }} + > + {text} ); } diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx index 0f3049d84776b..2bd3a2cd9d0d2 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx @@ -26,11 +26,13 @@ const createProps = () => ({ dashboardTitle: 'Test Dashboard', logEvent: jest.fn(), dashboardId: 123, + title: 'Download', + submenuKey: 'download', }); const renderComponent = () => { render( - + , { @@ -41,10 +43,6 @@ const renderComponent = () => { test('Should render menu items', () => { renderComponent(); - expect( - screen.getByRole('menuitem', { name: 'Export to PDF' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Download as Image' }), - ).toBeInTheDocument(); + expect(screen.getByText('Export to PDF')).toBeInTheDocument(); + expect(screen.getByText('Download as Image')).toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx index d9ffaaaedf5a3..cdb73d5e17bb4 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx @@ -17,17 +17,23 @@ * under the License. */ import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; -import DownloadScreenshot from './DownloadScreenshot'; +import { Menu } from 'src/components/Menu'; +import { useDownloadScreenshot } from 'src/dashboard/hooks/useDownloadScreenshot'; +import { ComponentProps } from 'react'; import { DownloadScreenshotFormat } from './types'; import DownloadAsPdf from './DownloadAsPdf'; import DownloadAsImage from './DownloadAsImage'; -export interface DownloadMenuItemProps { +export interface DownloadMenuItemProps + extends ComponentProps { pdfMenuItemTitle: string; imageMenuItemTitle: string; dashboardTitle: string; logEvent?: Function; dashboardId: number; + title: string; + disabled?: boolean; + submenuKey: string; } const DownloadMenuItems = (props: DownloadMenuItemProps) => { @@ -37,44 +43,45 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => { logEvent, dashboardId, dashboardTitle, + submenuKey, + disabled, + title, ...rest } = props; const isWebDriverScreenshotEnabled = isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) && isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot); + const downloadScreenshot = useDownloadScreenshot(dashboardId, logEvent); + return isWebDriverScreenshotEnabled ? ( - <> - - - > + + downloadScreenshot(DownloadScreenshotFormat.PDF)} + > + {pdfMenuItemTitle} + + downloadScreenshot(DownloadScreenshotFormat.PNG)} + > + {imageMenuItemTitle} + + ) : ( - <> + - > + ); }; diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/CopyMenuItemLabel.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/CopyMenuItemLabel.tsx new file mode 100644 index 0000000000000..e69de29bb2d1d 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 16bbcff9eccc3..f290a1a4b32a8 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 6c5468da24264..7f506af10b732 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RefObject } from 'react'; +import { ComponentProps, RefObject } from 'react'; import copyTextToClipboard from 'src/utils/copy'; import { t, logging } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; @@ -24,7 +24,7 @@ import { getDashboardPermalink } from 'src/utils/urlUtils'; import { MenuKeys, RootState } from 'src/dashboard/types'; import { shallowEqual, useSelector } from 'react-redux'; -interface ShareMenuItemProps { +interface ShareMenuItemProps extends ComponentProps { url?: string; copyMenuItemTitle: string; emailMenuItemTitle: string; @@ -38,8 +38,9 @@ interface ShareMenuItemProps { shareByEmailMenuItemRef?: RefObject; selectedKeys?: string[]; setOpenKeys?: Function; - key?: string; title: string; + disabled?: boolean; + submenuKey: string; } const ShareMenuItems = (props: ShareMenuItemProps) => { @@ -52,8 +53,10 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { addSuccessToast, dashboardId, dashboardComponentId, - key, title, + disabled, + submenuKey, + ...rest } = props; const { dataMask, activeTabs } = useSelector( (state: RootState) => ({ @@ -96,7 +99,7 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { } return ( - + onCopyLink()}> {copyMenuItemTitle} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx index 2c6196962ec5a..b52f57f3fde8a 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 @@ -206,19 +206,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')[2]).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')[3]).findByLabelText('check'), + within(horizontalItem.closest('li')!).getByLabelText('check'), ).toBeInTheDocument(); - // successful query + await waitFor(() => expect(fetchMock.lastCall()?.[1]?.body).toEqual( JSON.stringify({ @@ -229,17 +231,16 @@ test('On selection change, send request and update checked value', async () => { }), ), ); - await waitFor(() => { - const menuitems = screen.getAllByRole('menuitem'); - expect(menuitems.length).toBeGreaterThanOrEqual(3); - }); - // 2nd check - checkmark stays after successful query + userEvent.click(screen.getByLabelText('gear')); + userEvent.hover(screen.getByText('Orientation of filter bar')); + + const updatedHorizontalItem = await screen.findByText('Horizontal (Top)'); expect( - await within(screen.getAllByRole('menuitem')[3]).findByLabelText('check'), + within(updatedHorizontalItem.closest('li')!).getByLabelText('check'), ).toBeInTheDocument(); expect( - within(screen.getAllByRole('menuitem')[2]).queryByLabelText('check'), + within(verticalItem.closest('li')!).queryByLabelText('check'), ).not.toBeInTheDocument(); fetchMock.reset(); 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 b82b96767f232..2f5d849028480 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -37,6 +37,7 @@ const TitleArea = styled.div` font-size: ${theme.typography.sizes.l}px; flex-grow: 1; font-weight: ${theme.typography.weights.bold}; + order: -1; } & > div:first-of-type { @@ -49,6 +50,13 @@ const TitleArea = styled.div` `} `; +const ButtonsContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: ${({ theme }) => theme.gridUnit * 2}px; +`; + const HeaderButton = styled(Button)` padding: 0; `; @@ -58,7 +66,8 @@ const Wrapper = styled.div` padding: ${theme.gridUnit * 3}px ${theme.gridUnit * 2}px ${ theme.gridUnit }px; - .ant-dropdown-trigger span { + .antd5-dropdown-trigger span { + margin-left: auto; padding-right: ${theme.gridUnit * 2}px; } `} @@ -75,15 +84,17 @@ const Header: FC = ({ toggleFiltersBar }) => { {t('Filters')} - - toggleFiltersBar(false)} - > - - + + + toggleFiltersBar(false)} + > + + + ); diff --git a/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts new file mode 100644 index 0000000000000..bc8ef7d1ffae1 --- /dev/null +++ b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts @@ -0,0 +1,184 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useCallback, useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import { last } from 'lodash'; +import { + logging, + t, + SupersetClient, + SupersetApiError, +} from '@superset-ui/core'; +import { + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, +} from 'src/logger/LogUtils'; +import { RootState } from 'src/dashboard/types'; +import { getDashboardUrlParams } from 'src/utils/urlUtils'; +import { DownloadScreenshotFormat } from '../components/menu/DownloadMenuItems/types'; + +const RETRY_INTERVAL = 3000; +const MAX_RETRIES = 30; + +export const useDownloadScreenshot = ( + dashboardId: number, + logEvent?: Function, +) => { + const activeTabs = useSelector( + (state: RootState) => state.dashboardState.activeTabs || undefined, + ); + const anchor = useSelector( + (state: RootState) => + last(state.dashboardState.directPathToChild) || undefined, + ); + const dataMask = useSelector( + (state: RootState) => state.dataMask || undefined, + ); + + const { addDangerToast, addSuccessToast, addInfoToast } = useToasts(); + + const currentIntervalIds = useRef([]); + + const stopIntervals = useCallback( + (message?: 'success' | 'failure') => { + currentIntervalIds.current.forEach(clearInterval); + + if (message === 'failure') { + addDangerToast( + t('The screenshot could not be downloaded. Please, try again later.'), + ); + } + if (message === 'success') { + addSuccessToast(t('The screenshot has been downloaded.')); + } + }, + [addDangerToast, addSuccessToast], + ); + + const downloadScreenshot = useCallback( + (format: DownloadScreenshotFormat) => { + let retries = 0; + + const toastIntervalId = setInterval( + () => + addInfoToast( + t( + 'The screenshot is being generated. Please, do not leave the page.', + ), + { noDuplicate: true }, + ), + RETRY_INTERVAL, + ); + + currentIntervalIds.current = [ + ...(currentIntervalIds.current || []), + toastIntervalId, + ]; + + const checkImageReady = (cacheKey: string) => + SupersetClient.get({ + endpoint: `/api/v1/dashboard/${dashboardId}/screenshot/${cacheKey}/?download_format=${format}`, + headers: { Accept: 'application/pdf, image/png' }, + parseMethod: 'raw', + }) + .then((response: Response) => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `screenshot.${format}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + stopIntervals('success'); + }) + .catch(err => { + if ((err as SupersetApiError).status === 404) { + throw new Error('Image not ready'); + } + }); + + const fetchImageWithRetry = (cacheKey: string) => { + if (retries >= MAX_RETRIES) { + stopIntervals('failure'); + logging.error('Max retries reached'); + return; + } + checkImageReady(cacheKey).catch(() => { + retries += 1; + }); + }; + + SupersetClient.post({ + endpoint: `/api/v1/dashboard/${dashboardId}/cache_dashboard_screenshot/`, + jsonPayload: { + anchor, + activeTabs, + dataMask, + urlParams: getDashboardUrlParams(['edit']), + }, + }) + .then(({ json }) => { + const cacheKey = json?.cache_key; + if (!cacheKey) { + throw new Error('No image URL in response'); + } + const retryIntervalId = setInterval(() => { + fetchImageWithRetry(cacheKey); + }, RETRY_INTERVAL); + currentIntervalIds.current.push(retryIntervalId); + fetchImageWithRetry(cacheKey); + }) + .catch(error => { + logging.error(error); + stopIntervals('failure'); + }) + .finally(() => { + logEvent?.( + format === DownloadScreenshotFormat.PNG + ? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE + : LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, + ); + }); + }, + [ + dashboardId, + anchor, + activeTabs, + dataMask, + addInfoToast, + stopIntervals, + logEvent, + ], + ); + + useEffect( + () => () => { + if (currentIntervalIds.current.length > 0) { + stopIntervals(); + } + currentIntervalIds.current = []; + }, + [stopIntervals], + ); + + return downloadScreenshot; +}; diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts index 0aae5aa4ffe09..f3f0d753bf26a 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/ExportToCSVDropdown/index.tsx b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx index 2490ee7544f30..cc7fd23d30849 100644 --- a/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx +++ b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx @@ -20,7 +20,7 @@ import { ReactChild, useCallback, Key } from 'react'; import { t, styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; enum MenuKeys { @@ -67,9 +67,9 @@ export const ExportToCSVDropdown = ({ ); return ( - ( @@ -84,9 +84,9 @@ export const ExportToCSVDropdown = ({ - } + )} > {children} - + ); }; diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index cc8474b6dcfd4..ab6bdf3d2ff0f 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,7 +82,7 @@ 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 { @@ -410,8 +410,8 @@ class DatasourceControl extends PureComponent { {extra?.warning_markdown && ( )} - datasource.type === DatasourceType.Query ? queryDatasourceMenu : defaultDatasourceMenu @@ -423,7 +423,7 @@ class DatasourceControl extends PureComponent { className="datasource-modal-trigger" data-test="datasource-menu-trigger" /> - + {/* missing dataset */} {isMissingDatasource && isMissingParams && ( diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index 21cf69cec4174..74967dc0d3544 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -109,7 +109,7 @@ export const MenuTrigger = styled(Button)` `; const iconReset = css` - .ant-dropdown-menu-item > & > .anticon:first-child { + .antd5-dropdown-menu-item > & > .anticon:first-child { margin-right: 0; vertical-align: 0; } diff --git a/superset-frontend/src/features/charts/ChartCard.tsx b/superset-frontend/src/features/charts/ChartCard.tsx index 9dd18d1a893db..76554c00b8fc6 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} /> )} - + menu}> - + } /> diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx index 5eb02aeab8a4a..a81e7724a8deb 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'; @@ -181,9 +181,9 @@ function DashboardCard({ isStarred={favoriteStatus} /> )} - + menu}> - + } /> diff --git a/superset-frontend/src/features/home/SavedQueries.tsx b/superset-frontend/src/features/home/SavedQueries.tsx index fdc8cb8648d5e..12683f581f949 100644 --- a/superset-frontend/src/features/home/SavedQueries.tsx +++ b/superset-frontend/src/features/home/SavedQueries.tsx @@ -25,7 +25,7 @@ import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import { LoadingCards } from 'src/pages/Home'; import { TableTab } from 'src/views/CRUD/types'; import withToasts from 'src/components/MessageToasts/withToasts'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import { copyQueryLink, useListViewResource } from 'src/views/CRUD/hooks'; import ListViewCard from 'src/components/ListViewCard'; @@ -315,11 +315,11 @@ const SavedQueries = ({ e.preventDefault(); }} > - + renderMenu(q)}> - + } diff --git a/superset-frontend/src/features/tags/TagCard.tsx b/superset-frontend/src/features/tags/TagCard.tsx index 6b68868695986..092ba90f12610 100644 --- a/superset-frontend/src/features/tags/TagCard.tsx +++ b/superset-frontend/src/features/tags/TagCard.tsx @@ -19,7 +19,7 @@ import { Link } from 'react-router-dom'; import { isFeatureEnabled, FeatureFlag, t, useTheme } from '@superset-ui/core'; import { CardStyles } from 'src/views/CRUD/utils'; -import { AntdDropdown } from 'src/components'; +import { Dropdown } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListViewCard from 'src/components/ListViewCard'; @@ -108,9 +108,9 @@ function TagCard({ e.preventDefault(); }} > - + menu}> - + } />