+
{DRILL_TO_DETAIL}
) : (
-
+
{DRILL_TO_DETAIL}
);
const drillToDetailByMenuItem = drillByDisabled ? (
-
+
{DRILL_TO_DETAIL_BY}
) : (
{filters.map((filter, i) => (
{`${DRILL_TO_DETAIL_BY} `}
@@ -224,7 +226,6 @@ const DrillDetailMenuItems = ({
))}
{filters.length > 1 && (
diff --git a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
index 9e8802f12..cf77427cc 100644
--- a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
+++ b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
@@ -28,12 +28,15 @@ export type MenuItemWithTruncationProps = {
children: ReactNode;
onClick?: MenuItemProps['onClick'];
style?: CSSProperties;
+ menuKey?: string;
};
export const MenuItemWithTruncation = ({
tooltipText,
children,
- ...props
+ onClick,
+ style,
+ menuKey,
}: MenuItemWithTruncationProps) => {
const [itemRef, itemIsTruncated] = useCSSTextTruncation();
@@ -43,7 +46,9 @@ export const MenuItemWithTruncation = ({
display: flex;
line-height: 1.5em;
`}
- {...props}
+ eventKey={menuKey}
+ onClick={onClick}
+ style={style}
>
(
-
(
+
diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx
index ef81bd42c..1fdca2e75 100644
--- a/superset-frontend/src/components/Dropdown/index.tsx
+++ b/superset-frontend/src/components/Dropdown/index.tsx
@@ -24,13 +24,10 @@ import {
cloneElement,
} from 'react';
-import { AntdDropdown } from 'src/components';
-// TODO: @geido - Remove these after dropdown is fully migrated to Antd v5
import {
- Dropdown as Antd5Dropdown,
- DropDownProps as Antd5DropdownProps,
+ Dropdown as AntdDropdown,
+ DropdownProps as AntdDropdownProps,
} from 'antd-v5';
-import { DropDownProps } from 'antd/lib/dropdown';
import { styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
@@ -83,7 +80,8 @@ export enum IconOrientation {
Vertical = 'vertical',
Horizontal = 'horizontal',
}
-export interface DropdownProps extends DropDownProps {
+
+export interface MenuDotsDropdownProps extends AntdDropdownProps {
overlay: ReactElement;
iconOrientation?: IconOrientation;
}
@@ -100,19 +98,19 @@ const RenderIcon = (
return component;
};
-export const Dropdown = ({
+export const MenuDotsDropdown = ({
overlay,
iconOrientation = IconOrientation.Vertical,
...rest
-}: DropdownProps) => (
-
+}: MenuDotsDropdownProps) => (
+ overlay} {...rest}>
{RenderIcon(iconOrientation)}
);
-export interface NoAnimationDropdownProps extends Antd5DropdownProps {
+export interface NoAnimationDropdownProps extends AntdDropdownProps {
children: ReactNode;
onBlur?: (e: FocusEvent) => void;
onKeyDown?: (e: KeyboardEvent) => void;
@@ -126,8 +124,13 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => {
});
return (
-
+
{childrenWithProps}
-
+
);
};
+
+export type DropdownProps = AntdDropdownProps;
+export const Dropdown = (props: DropdownProps) => (
+
+);
diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx
index 32a7739e3..84ffd5fe2 100644
--- a/superset-frontend/src/components/DropdownButton/index.tsx
+++ b/superset-frontend/src/components/DropdownButton/index.tsx
@@ -16,90 +16,39 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ReactNode, ReactElement } from 'react';
+import { type ComponentProps } from 'react';
-import { AntdDropdown, AntdTooltip } from 'src/components';
-import { styled } from '@superset-ui/core';
+import { Dropdown } from 'antd-v5';
+import { Tooltip, TooltipPlacement } from 'src/components/Tooltip';
import { kebabCase } from 'lodash';
-const StyledDropdownButton = styled.div`
- .ant-btn-group {
- button.ant-btn {
- background-color: ${({ theme }) => theme.colors.primary.dark1};
- border-color: transparent;
- color: ${({ theme }) => theme.colors.grayscale.light5};
- font-size: 12px;
- line-height: 13px;
- outline: none;
- &:first-of-type {
- border-radius: ${({ theme }) =>
- `${theme.gridUnit}px 0 0 ${theme.gridUnit}px`};
- margin: 0;
- }
-
- &:disabled {
- background-color: ${({ theme }) => theme.colors.grayscale.light2};
- color: ${({ theme }) => theme.colors.grayscale.base};
- }
- &:nth-of-type(2) {
- margin: 0;
- border-radius: ${({ theme }) =>
- `0 ${theme.gridUnit}px ${theme.gridUnit}px 0`};
- width: ${({ theme }) => theme.gridUnit * 9}px;
- &:before,
- &:hover:before {
- border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5};
- content: '';
- display: block;
- height: ${({ theme }) => theme.gridUnit * 8}px;
- margin: 0;
- position: absolute;
- width: ${({ theme }) => theme.gridUnit * 0.25}px;
- }
-
- &:disabled:before {
- border-left: 1px solid ${({ theme }) => theme.colors.grayscale.base};
- }
- }
- }
- }
-`;
-
-export interface DropdownButtonProps {
- overlay: ReactElement;
+export type DropdownButtonProps = ComponentProps & {
tooltip?: string;
- placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
- buttonsRender?: ((buttons: ReactNode[]) => ReactNode[]) | undefined;
-}
+ tooltipPlacement?: TooltipPlacement;
+};
export const DropdownButton = ({
- overlay,
+ dropdownRender,
tooltip,
- placement,
+ tooltipPlacement,
+ children,
...rest
}: DropdownButtonProps) => {
- const buildButton = (
- props: {
- buttonsRender?: DropdownButtonProps['buttonsRender'];
- } = {},
- ) => (
-
-
-
+ const button = (
+
+ {children}
+
);
if (tooltip) {
- return buildButton({
- buttonsRender: ([leftButton, rightButton]) => [
-
- {leftButton}
- ,
- rightButton,
- ],
- });
+ return (
+
+ {button}
+
+ );
}
- return buildButton();
+ return button;
};
diff --git a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx b/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx
deleted file mode 100644
index cc12a8ba5..000000000
--- a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.stories.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import Icons from 'src/components/Icons';
-import DropdownSelectableIcon, { DropDownSelectableProps } from '.';
-
-export default {
- title: 'DropdownSelectableIcon',
- component: DropdownSelectableIcon,
-};
-
-export const Component = (props: DropDownSelectableProps) => (
- }
- />
-);
-
-Component.args = {
- info: 'Info go here',
- selectedKeys: ['vertical'],
- menuItems: [
- {
- key: 'vertical',
- label: 'Vertical',
- },
- {
- key: 'horizontal',
- label: 'Horizontal',
- },
- ],
-};
-
-Component.argTypes = {
- onSelect: {
- action: 'onSelect',
- table: {
- disable: true,
- },
- },
-};
diff --git a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx b/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx
deleted file mode 100644
index cfff77d5e..000000000
--- a/superset-frontend/src/components/DropdownSelectableIcon/DropdownSelectableIcon.test.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
-import Icons from 'src/components/Icons';
-import userEvent from '@testing-library/user-event';
-import DropdownSelectableIcon, { DropDownSelectableProps } from '.';
-
-const mockedProps = {
- menuItems: [
- {
- key: 'vertical',
- label: 'vertical',
- },
- {
- key: 'horizontal',
- label: 'horizontal',
- },
- ],
- selectedKeys: [],
- icon: ,
-};
-
-const asyncRender = (props: DropDownSelectableProps) =>
- waitFor(() => render());
-
-const openMenu = () => {
- userEvent.click(screen.getByRole('img', { name: 'gear' }));
-};
-
-test('should render', async () => {
- const { container } = await asyncRender(mockedProps);
- expect(container).toBeInTheDocument();
-});
-
-test('should render the icon', async () => {
- await asyncRender(mockedProps);
- expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
-});
-
-test('should not render the info', async () => {
- await asyncRender(mockedProps);
- openMenu();
- expect(
- screen.queryByTestId('dropdown-selectable-info'),
- ).not.toBeInTheDocument();
-});
-
-test('should render the info', async () => {
- const infoProps = {
- ...mockedProps,
- info: 'Test',
- };
- await asyncRender(infoProps);
- openMenu();
- expect(screen.getByTestId('dropdown-selectable-info')).toBeInTheDocument();
- expect(screen.getByText('Test')).toBeInTheDocument();
-});
-
-test('should render the menu items', async () => {
- await asyncRender(mockedProps);
- openMenu();
- expect(screen.getAllByRole('menuitem')).toHaveLength(2);
- expect(screen.getByText('vertical')).toBeInTheDocument();
- expect(screen.getByText('horizontal')).toBeInTheDocument();
-});
-
-test('should not render any selected menu item', async () => {
- await asyncRender(mockedProps);
- openMenu();
- expect(screen.getAllByRole('menuitem')).toHaveLength(2);
- expect(screen.queryByRole('img', { name: 'check' })).not.toBeInTheDocument();
-});
-
-test('should render the selected menu items', async () => {
- const selectedProps = {
- ...mockedProps,
- selectedKeys: ['vertical'],
- };
- await asyncRender(selectedProps);
- openMenu();
- expect(screen.getByRole('img', { name: 'check' })).toBeInTheDocument();
-});
diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx
deleted file mode 100644
index 8d791929d..000000000
--- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { addAlpha, styled, useTheme } from '@superset-ui/core';
-import { FC, RefObject, useMemo, ReactNode, useState } from 'react';
-import Icons from 'src/components/Icons';
-import { DropdownButton } from 'src/components/DropdownButton';
-import { DropdownButtonProps } from 'antd/lib/dropdown';
-import { Menu, MenuProps } from 'src/components/Menu';
-
-const { SubMenu } = Menu;
-
-type SubMenuItemProps = { key: string; label: string | ReactNode };
-
-export interface DropDownSelectableProps extends Pick {
- ref?: RefObject;
- icon: ReactNode;
- info?: string;
- menuItems: {
- key: string;
- label: string | ReactNode;
- children?: SubMenuItemProps[];
- divider?: boolean;
- }[];
- selectedKeys?: string[];
-}
-
-const StyledDropdownButton = styled(DropdownButton as FC)`
- button.ant-btn:first-of-type {
- display: none;
- }
- > button.ant-btn:nth-of-type(2) {
- display: inline-flex;
- background-color: transparent !important;
- height: unset;
- padding: 0;
- border: none;
- width: auto !important;
-
- .anticon {
- line-height: 0;
- }
- &:after {
- box-shadow: none !important;
- }
- }
-`;
-
-const StyledMenu = styled(Menu)`
- ${({ theme }) => `
- box-shadow:
- 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)},
- 0 6px 16px 0
- ${addAlpha(theme.colors.grayscale.dark2, 0.08)},
- 0 9px 28px 8px
- ${addAlpha(theme.colors.grayscale.dark2, 0.05)};
- .info {
- font-size: ${theme.typography.sizes.s}px;
- color: ${theme.colors.grayscale.base};
- padding: ${theme.gridUnit}px ${theme.gridUnit * 3}px ${
- theme.gridUnit
- }px ${theme.gridUnit * 3}px;
- }
- .ant-dropdown-menu-item-selected {
- color: ${theme.colors.grayscale.dark1};
- background-color: ${theme.colors.primary.light5};
- }
- `}
-`;
-
-const StyleMenuItem = styled(Menu.Item)<{ divider?: boolean }>`
- display: flex;
- justify-content: space-between;
- > span {
- width: 100%;
- }
- border-bottom: ${({ divider, theme }) =>
- divider ? `1px solid ${theme.colors.grayscale.light3};` : 'none;'};
-`;
-
-const StyleSubmenuItem = styled.div`
- display: flex;
- justify-content: space-between;
- width: 100%;
- > div {
- flex-grow: 1;
- }
-`;
-
-export default (props: DropDownSelectableProps) => {
- const theme = useTheme();
- const [visible, setVisible] = useState(false);
- const { icon, info, menuItems, selectedKeys, onSelect } = props;
-
- const handleVisibleChange = setVisible;
-
- const handleMenuSelect: MenuProps['onSelect'] = info => {
- if (onSelect) {
- onSelect(info);
- }
- setVisible(false);
- };
- const menuItem = useMemo(
- () => (label: string | ReactNode, key: string, divider?: boolean) => (
-
-
- {label}
- {selectedKeys?.includes(key) && (
-
- )}
-
-
- ),
- [selectedKeys, theme.colors.primary.base],
- );
-
- const overlayMenu = useMemo(
- () => (
- <>
- {info && (
-
- {info}
-
- )}
-
- {menuItems.map(m =>
- m.children?.length ? (
-
- {m.children.map(s => menuItem(s.label, s.key))}
-
- ) : (
- menuItem(m.label, m.key, m.divider)
- ),
- )}
-
- >
- ),
- [selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect],
- );
-
- return (
-
- );
-};
diff --git a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
index 38fc7cbe9..61d2c70a3 100644
--- a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
+++ b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { action } from '@storybook/addon-actions';
-import { AntdDropdown } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import Icons from 'src/components/Icons';
import FaveStar from 'src/components/FaveStar';
@@ -70,8 +70,8 @@ export const SupersetListViewCard = ({
saveFaveStar={action('saveFaveStar')}
isStarred={isStarred}
/>
- (
- }
+ )}
>
-
+
}
/>
diff --git a/superset-frontend/src/components/Menu/index.tsx b/superset-frontend/src/components/Menu/index.tsx
index ff2c1c0b6..5d0f6e0dc 100644
--- a/superset-frontend/src/components/Menu/index.tsx
+++ b/superset-frontend/src/components/Menu/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { addAlpha, styled } from '@superset-ui/core';
+import { styled } from '@superset-ui/core';
import { ReactElement } from 'react';
import { Menu as AntdMenu } from 'antd-v5';
import { MenuProps as AntdMenuProps } from 'antd-v5/es/menu';
@@ -73,23 +73,11 @@ const StyledMenuItem = styled(AntdMenu.Item)`
}
`;
-// TODO: @geido - Move this to theme after fully migrating dropdown to Antd5
const StyledMenu = styled(AntdMenu)`
- ${({ theme }) => `
- &.antd5-menu-horizontal {
- background-color: inherit;
- border-bottom: 1px solid transparent;
- }
- &.antd5-menu-vertical,
- &.ant-dropdown-menu {
- box-shadow:
- 0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)},
- 0 6px 16px 0
- ${addAlpha(theme.colors.grayscale.dark2, 0.08)},
- 0 9px 28px 8px
- ${addAlpha(theme.colors.grayscale.dark2, 0.05)};
- }
- `}
+ &.antd5-menu-horizontal {
+ background-color: inherit;
+ border-bottom: 1px solid transparent;
+ }
`;
const StyledNav = styled(AntdMenu)`
@@ -145,11 +133,6 @@ const StyledSubMenu = styled(AntdMenu.SubMenu)`
transition: all ${({ theme }) => theme.transitionTiming}s;
}
}
-
- .ant-dropdown-menu-submenu-arrow:before,
- .ant-dropdown-menu-submenu-arrow:after {
- content: none !important;
- }
`;
export type MenuMode = AntdMenuProps['mode'];
diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx
index b84e67b31..68c33d78c 100644
--- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx
+++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx
@@ -18,7 +18,7 @@
*/
import { ReactNode, ReactElement } from 'react';
import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
-import { AntdDropdown, AntdDropdownProps } from 'src/components';
+import { Dropdown, DropdownProps } from 'src/components/Dropdown';
import { TooltipPlacement } from 'src/components/Tooltip';
import {
DynamicEditableTitle,
@@ -116,7 +116,7 @@ export type PageHeaderWithActionsProps = {
titlePanelAdditionalItems: ReactNode;
rightPanelAdditionalItems: ReactNode;
additionalActionsMenu: ReactElement;
- menuDropdownProps: Omit
;
+ menuDropdownProps: Omit;
tooltipProps?: {
text?: string;
placement?: TooltipPlacement;
@@ -155,9 +155,9 @@ export const PageHeaderWithActions = ({
{rightPanelAdditionalItems}
{showMenuDropdown && (
-
additionalActionsMenu}
{...menuDropdownProps}
>
-
+
)}
diff --git a/superset-frontend/src/components/PopoverDropdown/index.tsx b/superset-frontend/src/components/PopoverDropdown/index.tsx
index 9ad507d5f..41812ea13 100644
--- a/superset-frontend/src/components/PopoverDropdown/index.tsx
+++ b/superset-frontend/src/components/PopoverDropdown/index.tsx
@@ -19,7 +19,7 @@
import { Key } from 'react';
import cx from 'classnames';
import { styled, useTheme } from '@superset-ui/core';
-import { AntdDropdown } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import Icons from 'src/components/Icons';
@@ -89,10 +89,10 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
const theme = useTheme();
const selected = options.find(opt => opt.value === value);
return (
- (
- }
+ )}
>
{selected && renderButton(selected)}
@@ -115,7 +115,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
css={{ marginTop: theme.gridUnit * 0.5 }}
/>
-
+
);
};
diff --git a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx
index 1c3127504..5b404ccbd 100644
--- a/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx
+++ b/superset-frontend/src/components/Table/cell-renderers/ActionCell/index.tsx
@@ -18,7 +18,7 @@
*/
import { useState, useEffect } from 'react';
import { styled } from '@superset-ui/core';
-import { Dropdown, IconOrientation } from 'src/components/Dropdown';
+import { MenuDotsDropdown, IconOrientation } from 'src/components/Dropdown';
import { Menu, MenuProps } from 'src/components/Menu';
/**
@@ -126,7 +126,7 @@ export function ActionCell(props: ActionCellProps) {
setVisible(flag);
};
return (
- (
- <>
-
- >
+
);
diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts
index cb949ef5c..9d9ad65eb 100644
--- a/superset-frontend/src/components/index.ts
+++ b/superset-frontend/src/components/index.ts
@@ -56,7 +56,6 @@ export {
Card as AntdCard,
Checkbox as AntdCheckbox,
Collapse as AntdCollapse,
- Dropdown as AntdDropdown,
Form as AntdForm,
Input as AntdInput,
Select as AntdSelect,
@@ -67,5 +66,4 @@ export {
// Exported types
export type { FormInstance } from 'antd/lib/form';
-export type { DropDownProps as AntdDropdownProps } from 'antd/lib/dropdown';
export type { RadioChangeEvent } from 'antd/lib/radio';
diff --git a/superset-frontend/src/dashboard/components/CssEditor/index.tsx b/superset-frontend/src/dashboard/components/CssEditor/index.tsx
index e2748c8b0..2b9b99070 100644
--- a/superset-frontend/src/dashboard/components/CssEditor/index.tsx
+++ b/superset-frontend/src/dashboard/components/CssEditor/index.tsx
@@ -17,8 +17,8 @@
* under the License.
*/
import { Key, ReactNode, PureComponent } from 'react';
+import { Dropdown } from 'src/components/Dropdown';
import rison from 'rison';
-import { AntdDropdown } from 'src/components';
import { Menu } from 'src/components/Menu';
import Button from 'src/components/Button';
import { t, styled, SupersetClient } from '@superset-ui/core';
@@ -115,9 +115,9 @@ class CssEditor extends PureComponent {
);
return (
-
+ menu} placement="bottomRight">
-
+
);
}
return null;
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
index 1a0b73165..877d9203a 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx
@@ -53,9 +53,6 @@ jest.mock('src/components/Select/Select', () => () => (
jest.mock('src/components/Select/AsyncSelect', () => () => (
));
-jest.mock('src/dashboard/components/Header/HeaderActionsDropdown', () => () => (
-
-));
jest.mock('src/components/PageHeaderWithActions', () => ({
PageHeaderWithActions: () => (
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
deleted file mode 100644
index 090f12a2b..000000000
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { shallow } from 'enzyme';
-import sinon from 'sinon';
-import { render, screen } from 'spec/helpers/testing-library';
-import userEvent from '@testing-library/user-event';
-import fetchMock from 'fetch-mock';
-import { HeaderDropdownProps } from 'src/dashboard/components/Header/types';
-import injectCustomCss from 'src/dashboard/util/injectCustomCss';
-import { HeaderActionsDropdown } from '.';
-
-const createProps = (): HeaderDropdownProps => ({
- addSuccessToast: jest.fn(),
- addDangerToast: jest.fn(),
- customCss: '.ant-menu {margin-left: 100px;}',
- dashboardId: 1,
- dashboardInfo: {
- id: 1,
- dash_edit_perm: true,
- dash_save_perm: true,
- userId: '1',
- metadata: {},
- common: {
- conf: {
- DASHBOARD_AUTO_REFRESH_INTERVALS: [
- [0, "Don't refresh"],
- [10, '10 seconds'],
- ],
- },
- },
- },
- dashboardTitle: 'Title',
- editMode: false,
- expandedSlices: {},
- forceRefreshAllCharts: jest.fn(),
- hasUnsavedChanges: false,
- isLoading: false,
- layout: {},
- onChange: jest.fn(),
- onSave: jest.fn(),
- refreshFrequency: 200,
- setRefreshFrequency: jest.fn(),
- shouldPersistRefreshFrequency: false,
- showPropertiesModal: jest.fn(),
- startPeriodicRender: jest.fn(),
- updateCss: jest.fn(),
- userCanEdit: false,
- userCanSave: false,
- userCanShare: false,
- userCanCurate: false,
- lastModifiedTime: 0,
- isDropdownVisible: true,
- setIsDropdownVisible: jest.fn(),
- directPathToChild: [],
- manageEmbedded: jest.fn(),
- dataMask: {},
- logEvent: jest.fn(),
- refreshLimit: 0,
- refreshWarning: '',
-});
-
-const editModeOnProps = {
- ...createProps(),
- editMode: true,
-};
-
-const editModeOnWithFilterScopesProps = {
- ...editModeOnProps,
- dashboardInfo: {
- ...editModeOnProps.dashboardInfo,
- metadata: {
- filter_scopes: {
- '1': { scopes: ['ROOT_ID'], immune: [] },
- },
- },
- },
-};
-
-const guestUserProps = {
- ...createProps(),
- dashboardInfo: {
- ...createProps().dashboardInfo,
- userId: undefined,
- },
-};
-
-function setup(props: HeaderDropdownProps) {
- return render(
-
-
-
,
- { useRedux: true },
- );
-}
-
-fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
-
-test('should render', () => {
- const mockedProps = createProps();
- const { container } = setup(mockedProps);
- expect(container).toBeInTheDocument();
-});
-
-test('should render the Download dropdown button when not in edit mode', () => {
- const mockedProps = createProps();
- setup(mockedProps);
- expect(
- screen.getByRole('menuitem', { name: 'Download' }),
- ).toBeInTheDocument();
-});
-
-test('should render the menu items', async () => {
- const mockedProps = createProps();
- setup(mockedProps);
- expect(screen.getAllByRole('menuitem')).toHaveLength(4);
- expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
- expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
- expect(screen.getByText('Enter fullscreen')).toBeInTheDocument();
- expect(screen.getByText('Download')).toBeInTheDocument();
-});
-
-test('should render the menu items in edit mode', async () => {
- setup(editModeOnProps);
- expect(screen.getAllByRole('menuitem')).toHaveLength(4);
- expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
- expect(screen.getByText('Edit properties')).toBeInTheDocument();
- expect(screen.getByText('Edit CSS')).toBeInTheDocument();
- expect(screen.getByText('Download')).toBeInTheDocument();
-});
-
-test('should render the menu items in Embedded mode', async () => {
- setup(guestUserProps);
- expect(screen.getAllByRole('menuitem')).toHaveLength(3);
- expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
- expect(screen.getByText('Download')).toBeInTheDocument();
- expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
-});
-
-test('should not render filter mapping in edit mode if explicit filter scopes undefined', async () => {
- setup(editModeOnProps);
- expect(screen.queryByText('Set filter mapping')).not.toBeInTheDocument();
-});
-
-test('should render filter mapping in edit mode if explicit filter scopes defined', async () => {
- setup(editModeOnWithFilterScopesProps);
- expect(screen.getByText('Set filter mapping')).toBeInTheDocument();
-});
-
-test('should show the share actions', async () => {
- const mockedProps = createProps();
- const canShareProps = {
- ...mockedProps,
- userCanShare: true,
- };
- setup(canShareProps);
-
- expect(screen.getByText('Share')).toBeInTheDocument();
-});
-
-test('should render the "Save as" menu item when user can save', async () => {
- const mockedProps = createProps();
- const canSaveProps = {
- ...mockedProps,
- userCanSave: true,
- };
- setup(canSaveProps);
- expect(screen.getByText('Save as')).toBeInTheDocument();
-});
-
-test('should NOT render the "Save as" menu item when user cannot save', async () => {
- const mockedProps = createProps();
- setup(mockedProps);
- expect(screen.queryByText('Save as')).not.toBeInTheDocument();
-});
-
-test('should render the "Refresh dashboard" menu item as disabled when loading', async () => {
- const mockedProps = createProps();
- const loadingProps = {
- ...mockedProps,
- isLoading: true,
- };
- setup(loadingProps);
- expect(screen.getByText('Refresh dashboard').parentElement).toHaveClass(
- 'ant-menu-item-disabled',
- );
-});
-
-test('should NOT render the "Refresh dashboard" menu item as disabled', async () => {
- const mockedProps = createProps();
- setup(mockedProps);
- expect(screen.getByText('Refresh dashboard')).not.toHaveClass(
- 'ant-menu-item-disabled',
- );
-});
-
-test('should render with custom css', () => {
- const mockedProps = createProps();
- const { customCss } = mockedProps;
- setup(mockedProps);
- injectCustomCss(customCss);
- expect(screen.getByTestId('header-actions-menu')).toHaveStyle(
- 'margin-left: 100px',
- );
-});
-
-test('should refresh the charts', async () => {
- const mockedProps = createProps();
- setup(mockedProps);
- userEvent.click(screen.getByText('Refresh dashboard'));
- expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1);
- expect(mockedProps.addSuccessToast).toHaveBeenCalledTimes(1);
-});
-
-test('should show the properties modal', async () => {
- setup(editModeOnProps);
- userEvent.click(screen.getByText('Edit properties'));
- expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1);
-});
-
-describe('UNSAFE_componentWillReceiveProps', () => {
- let wrapper: any;
-
- const mockedProps = createProps();
- const props = { ...mockedProps, customCss: '' };
- beforeEach(() => {
- wrapper = shallow();
- wrapper.setState({ css: props.customCss });
- sinon.spy(wrapper.instance(), 'setState');
- });
-
- afterEach(() => {
- wrapper.instance().setState.restore();
- });
-
- it('css should update state and inject custom css', () => {
- wrapper.instance().UNSAFE_componentWillReceiveProps({
- ...props,
- customCss: mockedProps.customCss,
- });
- expect(wrapper.instance().setState.calledOnce).toBe(true);
- const stateKeys = Object.keys(wrapper.instance().setState.lastCall.args[0]);
- expect(stateKeys).toContain('css');
- });
-});
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx
index 5ce86cc52..1324d785c 100644
--- a/superset-frontend/src/dashboard/components/Header/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -40,7 +40,6 @@ import { Button } from 'src/components/';
import { findPermission } from 'src/utils/findPermission';
import { Tooltip } from 'src/components/Tooltip';
import { safeStringify } from 'src/utils/safeStringify';
-import ConnectedHeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown';
import PublishedStatus from 'src/dashboard/components/PublishedStatus';
import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
@@ -53,6 +52,9 @@ import {
import setPeriodicRunner, {
stopPeriodicRender,
} from 'src/dashboard/util/setPeriodicRunner';
+import ReportModal from 'src/features/reports/ReportModal';
+import DeleteModal from 'src/components/DeleteModal';
+import { deleteActiveReport } from 'src/features/reports/ReportModal/actions';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import DashboardEmbedModal from '../EmbeddedModal';
import OverwriteConfirm from '../OverwriteConfirm';
@@ -88,6 +90,7 @@ import { dashboardInfoChanged } from '../../actions/dashboardInfo';
import isDashboardLoading from '../../util/isDashboardLoading';
import { useChartIds } from '../../util/charts/useChartIds';
import { useDashboardMetadataBar } from './useDashboardMetadataBar';
+import { useHeaderActionsMenu } from './useHeaderActionsDropdownMenu';
const extensionsRegistry = getExtensionsRegistry();
@@ -160,8 +163,9 @@ const Header = () => {
const [emphasizeUndo, setEmphasizeUndo] = useState(false);
const [emphasizeRedo, setEmphasizeRedo] = useState(false);
const [showingPropertiesModal, setShowingPropertiesModal] = useState(false);
- const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [showingEmbedModal, setShowingEmbedModal] = useState(false);
+ const [showingReportModal, setShowingReportModal] = useState(false);
+ const [currentReportDeleting, setCurrentReportDeleting] = useState(null);
const dashboardInfo = useSelector(state => state.dashboardInfo);
const layout = useSelector(state => state.dashboardLayout.present);
const undoLength = useSelector(state => state.dashboardLayout.past.length);
@@ -348,10 +352,6 @@ const Header = () => {
[boundActionCreators, dashboardTitle],
);
- const setDropdownVisible = useCallback(visible => {
- setIsDropdownVisible(visible);
- }, []);
-
const handleCtrlY = useCallback(() => {
boundActionCreators.onRedo();
setEmphasizeRedo(true);
@@ -475,6 +475,14 @@ const Header = () => {
setShowingEmbedModal(false);
}, []);
+ const showReportModal = useCallback(() => {
+ setShowingReportModal(true);
+ }, []);
+
+ const hideReportModal = useCallback(() => {
+ setShowingReportModal(false);
+ }, []);
+
const metadataBar = useDashboardMetadataBar(dashboardInfo);
const userCanEdit =
@@ -689,92 +697,47 @@ const Header = () => {
],
);
- const menuDropdownProps = useMemo(
- () => ({
- getPopupContainer: triggerNode =>
- triggerNode.closest('.header-with-actions'),
- visible: isDropdownVisible,
- onVisibleChange: setDropdownVisible,
- }),
- [isDropdownVisible, setDropdownVisible],
- );
-
- const additionalActionsMenu = useMemo(
- () => (
-
- ),
- [
- actualLastModifiedTime,
- boundActionCreators.addDangerToast,
- boundActionCreators.addSuccessToast,
- boundActionCreators.logEvent,
- boundActionCreators.onChange,
- boundActionCreators.onSave,
- boundActionCreators.setRefreshFrequency,
- boundActionCreators.updateCss,
- colorNamespace,
- colorScheme,
- customCss,
- dashboardInfo,
- dashboardTitle,
- dataMask,
- editMode,
- expandedSlices,
- forceRefresh,
- hasUnsavedChanges,
- isDropdownVisible,
- isLoading,
- layout,
- refreshFrequency,
- refreshLimit,
- refreshWarning,
- setDropdownVisible,
- shouldPersistRefreshFrequency,
- showEmbedModal,
- showPropertiesModal,
- startPeriodicRender,
- userCanCurate,
- userCanEdit,
- userCanSaveAs,
- userCanShare,
- ],
- );
+ const handleReportDelete = async report => {
+ await dispatch(deleteActiveReport(report));
+ setCurrentReportDeleting(null);
+ };
+ const [menu, isDropdownVisible, setIsDropdownVisible] = useHeaderActionsMenu({
+ addSuccessToast: boundActionCreators.addSuccessToast,
+ addDangerToast: boundActionCreators.addDangerToast,
+ dashboardInfo,
+ dashboardId: dashboardInfo.id,
+ dashboardTitle,
+ dataMask,
+ layout,
+ expandedSlices,
+ customCss,
+ colorNamespace,
+ colorScheme,
+ onSave: boundActionCreators.onSave,
+ onChange: boundActionCreators.onChange,
+ forceRefreshAllCharts: forceRefresh,
+ startPeriodicRender,
+ refreshFrequency,
+ shouldPersistRefreshFrequency,
+ setRefreshFrequency: boundActionCreators.setRefreshFrequency,
+ updateCss: boundActionCreators.updateCss,
+ editMode,
+ hasUnsavedChanges,
+ userCanEdit,
+ userCanShare,
+ userCanSave: userCanSaveAs,
+ userCanCurate,
+ isLoading,
+ showReportModal,
+ showPropertiesModal,
+ setCurrentReportDeleting,
+ manageEmbedded: showEmbedModal,
+ refreshLimit,
+ refreshWarning,
+ lastModifiedTime: actualLastModifiedTime,
+ logEvent: boundActionCreators.logEvent,
+ });
return (
{
faveStarProps={faveStarProps}
titlePanelAdditionalItems={titlePanelAdditionalItems}
rightPanelAdditionalItems={rightPanelAdditionalItems}
- menuDropdownProps={menuDropdownProps}
- additionalActionsMenu={additionalActionsMenu}
+ menuDropdownProps={{
+ open: isDropdownVisible,
+ onOpenChange: setIsDropdownVisible,
+ }}
+ additionalActionsMenu={menu}
showFaveStar={user?.userId && dashboardInfo?.id}
showTitlePanelItems
/>
@@ -806,6 +772,32 @@ const Header = () => {
/>
)}
+
+
+ {currentReportDeleting && (
+ {
+ if (currentReportDeleting) {
+ handleReportDelete(currentReportDeleting);
+ }
+ }}
+ onHide={() => setCurrentReportDeleting(null)}
+ open
+ title={t('Delete Report?')}
+ />
+ )}
+
{userCanCurate && (
@@ -817,7 +809,7 @@ const Header = () => {
)}
void;
- setIsDropdownVisible: (visible: boolean) => void;
- isDropdownVisible: boolean;
refreshLimit: number;
refreshWarning: string;
directPathToChild: string[];
+ showReportModal: () => void;
+ setCurrentReportDeleting: (alert: AlertObject | null) => void;
}
export interface HeaderProps {
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx b/superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx
similarity index 53%
rename from superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx
rename to superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx
index 6bc712f6b..420e91e55 100644
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.tsx
+++ b/superset-frontend/src/dashboard/components/Header/useHeaderActionsDropdownMenu.tsx
@@ -16,11 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { PureComponent } from 'react';
-import { isEmpty } from 'lodash';
-import { connect } from 'react-redux';
-import { t } from '@superset-ui/core';
+import { useState, useEffect, useCallback, useMemo } from 'react';
+import { useSelector } from 'react-redux';
import { Menu } from 'src/components/Menu';
+import { t } from '@superset-ui/core';
+import { isEmpty } from 'lodash';
import { URL_PARAMS } from 'src/constants';
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
import DownloadMenuItems from 'src/dashboard/components/menu/DownloadMenuItems';
@@ -37,158 +37,156 @@ import { getUrlParam } from 'src/utils/urlUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
import { HeaderDropdownProps } from 'src/dashboard/components/Header/types';
-const mapStateToProps = (state: RootState) => ({
- directPathToChild: state.dashboardState.directPathToChild,
-});
-
-interface HeaderActionsDropdownState {
- css: string;
- showReportSubMenu: boolean | null;
-}
-
-export class HeaderActionsDropdown extends PureComponent<
- HeaderDropdownProps,
- HeaderActionsDropdownState
-> {
- static defaultProps = {
- colorNamespace: undefined,
- colorScheme: undefined,
- refreshLimit: 0,
- refreshWarning: null,
- };
-
- constructor(props: HeaderDropdownProps) {
- super(props);
- this.state = {
- css: props.customCss || '',
- showReportSubMenu: null,
- };
- }
-
- UNSAFE_componentWillReceiveProps(nextProps: HeaderDropdownProps) {
- if (this.props.customCss !== nextProps.customCss) {
- this.setState({ css: nextProps.customCss }, () => {
- injectCustomCss(nextProps.customCss);
- });
+export const useHeaderActionsMenu = ({
+ customCss,
+ dashboardId,
+ dashboardInfo,
+ refreshFrequency,
+ shouldPersistRefreshFrequency,
+ editMode,
+ colorNamespace,
+ colorScheme,
+ layout,
+ expandedSlices,
+ onSave,
+ userCanEdit,
+ userCanShare,
+ userCanSave,
+ userCanCurate,
+ isLoading,
+ refreshLimit,
+ refreshWarning,
+ lastModifiedTime,
+ addSuccessToast,
+ addDangerToast,
+ forceRefreshAllCharts,
+ showPropertiesModal,
+ showReportModal,
+ manageEmbedded,
+ onChange,
+ updateCss,
+ startPeriodicRender,
+ setRefreshFrequency,
+ dashboardTitle,
+ logEvent,
+ setCurrentReportDeleting,
+}: HeaderDropdownProps) => {
+ const [css, setCss] = useState(customCss || '');
+ const [showReportSubMenu, setShowReportSubMenu] = useState(
+ null,
+ );
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+ const directPathToChild = useSelector(
+ (state: RootState) => state.dashboardState.directPathToChild,
+ );
+ useEffect(() => {
+ if (customCss !== css) {
+ setCss(customCss || '');
+ injectCustomCss(customCss);
}
- }
+ }, [css, customCss]);
- setShowReportSubMenu = (show: boolean) => {
- this.setState({ showReportSubMenu: show });
- };
-
- changeCss = (css: string) => {
- this.props.onChange();
- this.props.updateCss(css);
- };
-
- changeRefreshInterval = (refreshInterval: number, isPersistent: boolean) => {
- this.props.setRefreshFrequency(refreshInterval, isPersistent);
- this.props.startPeriodicRender(refreshInterval * 1000);
- };
-
- handleMenuClick = ({ key }: Record) => {
- switch (key) {
- case MenuKeys.RefreshDashboard:
- this.props.forceRefreshAllCharts();
- this.props.addSuccessToast(t('Refreshing charts'));
- break;
- case MenuKeys.EditProperties:
- this.props.showPropertiesModal();
- break;
- case MenuKeys.ToggleFullscreen: {
- const url = getDashboardUrl({
- pathname: window.location.pathname,
- filters: getActiveFilters(),
- hash: window.location.hash,
- standalone: getUrlParam(URL_PARAMS.standalone),
- });
- window.location.replace(url);
- break;
+ const handleMenuClick = useCallback(
+ ({ key }: { key: string }) => {
+ switch (key) {
+ case MenuKeys.RefreshDashboard:
+ forceRefreshAllCharts();
+ addSuccessToast(t('Refreshing charts'));
+ break;
+ case MenuKeys.EditProperties:
+ showPropertiesModal();
+ break;
+ case MenuKeys.ToggleFullscreen: {
+ const url = getDashboardUrl({
+ pathname: window.location.pathname,
+ filters: getActiveFilters(),
+ hash: window.location.hash,
+ standalone: getUrlParam(URL_PARAMS.standalone),
+ });
+ window.location.replace(url);
+ break;
+ }
+ case MenuKeys.ManageEmbedded:
+ manageEmbedded();
+ break;
+ default:
+ break;
}
- case MenuKeys.ManageEmbedded: {
- this.props.manageEmbedded();
- break;
- }
- default:
- break;
- }
- };
-
- render() {
- const {
- dashboardTitle,
- dashboardId,
- dashboardInfo,
- refreshFrequency,
- shouldPersistRefreshFrequency,
- editMode,
- customCss,
- colorNamespace,
- colorScheme,
- layout,
- expandedSlices,
- onSave,
- userCanEdit,
- userCanShare,
- userCanSave,
- userCanCurate,
- isLoading,
- refreshLimit,
- refreshWarning,
- lastModifiedTime,
+ setIsDropdownVisible(false);
+ },
+ [
+ forceRefreshAllCharts,
addSuccessToast,
- addDangerToast,
- setIsDropdownVisible,
- isDropdownVisible,
- directPathToChild,
- ...rest
- } = this.props;
+ showPropertiesModal,
+ manageEmbedded,
+ ],
+ );
- const emailTitle = t('Superset dashboard');
- const emailSubject = `${emailTitle} ${dashboardTitle}`;
- const emailBody = t('Check out this dashboard: ');
+ const changeCss = useCallback(
+ (newCss: string) => {
+ onChange();
+ updateCss(newCss);
+ },
+ [onChange, updateCss],
+ );
+ const changeRefreshInterval = useCallback(
+ (refreshInterval: number, isPersistent: boolean) => {
+ setRefreshFrequency(refreshInterval, isPersistent);
+ startPeriodicRender(refreshInterval * 1000);
+ },
+ [setRefreshFrequency, startPeriodicRender],
+ );
+
+ const emailSubject = useMemo(
+ () => `${t('Superset dashboard')} ${dashboardTitle}`,
+ [dashboardTitle],
+ );
+
+ const url = useMemo(
+ () =>
+ getDashboardUrl({
+ pathname: window.location.pathname,
+ filters: getActiveFilters(),
+ hash: window.location.hash,
+ }),
+ [],
+ );
+
+ const dashboardComponentId = useMemo(
+ () => [...(directPathToChild || [])].pop(),
+ [directPathToChild],
+ );
+
+ const menu = useMemo(() => {
const isEmbedded = !dashboardInfo?.userId;
-
- const url = getDashboardUrl({
- pathname: window.location.pathname,
- filters: getActiveFilters(),
- hash: window.location.hash,
- });
-
const refreshIntervalOptions =
dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS;
- const dashboardComponentId = [...(directPathToChild || [])].pop();
-
return (
-
}
- initialCss={this.state.css}
- onChange={this.changeCss}
+ initialCss={css}
+ onChange={changeCss}
addDangerToast={addDangerToast}
/>
@@ -228,29 +226,26 @@ export class HeaderActionsDropdown extends PureComponent<
/>
)}
-
-
-
+ pdfMenuItemTitle={t('Export to PDF')}
+ imageMenuItemTitle={t('Download as Image')}
+ dashboardTitle={dashboardTitle}
+ dashboardId={dashboardId}
+ logEvent={logEvent}
+ />
{userCanShare && (
)}
{!editMode && userCanCurate && (
-
+
{t('Embed dashboard')}
)}
{!editMode ? (
- this.state.showReportSubMenu ? (
+ showReportSubMenu ? (
<>
-
-
-
+
>
) : (
-
-
-
+
)
) : null}
{editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && (
@@ -302,14 +291,13 @@ export class HeaderActionsDropdown extends PureComponent<
/>
)}
-
{t('Set auto-refresh interval')} }
@@ -317,7 +305,18 @@ export class HeaderActionsDropdown extends PureComponent<
);
- }
-}
+ }, [
+ css,
+ showReportSubMenu,
+ isDropdownVisible,
+ directPathToChild,
+ handleMenuClick,
+ changeCss,
+ changeRefreshInterval,
+ emailSubject,
+ url,
+ dashboardComponentId,
+ ]);
-export default connect(mapStateToProps)(HeaderActionsDropdown);
+ return [menu, isDropdownVisible, setIsDropdownVisible];
+};
diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx
index a683dfa2b..3a9878f4f 100644
--- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx
+++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx
@@ -22,16 +22,16 @@ import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
-import { HeaderActionsDropdown } from 'src/dashboard/components/Header/HeaderActionsDropdown';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { useHeaderActionsMenu } from './Header/useHeaderActionsDropdownMenu';
const createProps = () => ({
addSuccessToast: jest.fn(),
addDangerToast: jest.fn(),
customCss:
- '.header-with-actions .right-button-panel .ant-dropdown-trigger{margin-left: 100px;}',
+ '.header-with-actions .right-button-panel .antd5-dropdown-trigger{margin-left: 100px;}',
dashboardId: 1,
dashboardInfo: {
id: 1,
@@ -85,12 +85,22 @@ const editModeOnProps = {
};
const mockStore = configureStore([thunk]);
-const store = mockStore({});
+const store = mockStore({
+ dashboardState: {
+ dashboardInfo: createProps().dashboardInfo,
+ },
+});
+
+const HeaderActionsMenu = (props: any) => {
+ const [menu] = useHeaderActionsMenu(props);
+
+ return <>{menu}>;
+};
const setup = (overrides?: any) => (
-
+
);
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
index e1f20b550..7c813809c 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
@@ -112,11 +112,13 @@ const renderWrapper = (
});
};
+const openMenu = () => {
+ userEvent.click(screen.getByRole('button', { name: 'More Options' }));
+};
+
test('Should render', () => {
renderWrapper();
- expect(
- screen.getByRole('button', { name: 'More Options' }),
- ).toBeInTheDocument();
+ openMenu();
expect(screen.getByTestId(`slice_${SLICE_ID}-menu`)).toBeInTheDocument();
});
@@ -143,6 +145,7 @@ test('Should render default props', () => {
delete props.isExpanded;
renderWrapper(props);
+ openMenu();
expect(screen.getByText('Enter fullscreen')).toBeInTheDocument();
expect(screen.getByText('Force refresh')).toBeInTheDocument();
expect(screen.getByText('Show chart description')).toBeInTheDocument();
@@ -159,6 +162,7 @@ test('Should render default props', () => {
test('Should "export to CSV"', async () => {
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(props.exportCSV).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to .CSV'));
@@ -169,6 +173,7 @@ test('Should "export to CSV"', async () => {
test('Should "export to Excel"', async () => {
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(props.exportXLSX).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to Excel'));
@@ -182,6 +187,7 @@ test('Export full CSV is under featureflag', async () => {
};
const props = createProps(VizType.Table);
renderWrapper(props);
+ openMenu();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument();
@@ -193,6 +199,7 @@ test('Should "export full CSV"', async () => {
};
const props = createProps(VizType.Table);
renderWrapper(props);
+ openMenu();
expect(props.exportFullCSV).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to full .CSV'));
@@ -205,6 +212,7 @@ test('Should not show export full CSV if report is not table', async () => {
[FeatureFlag.AllowFullCsvExport]: true,
};
renderWrapper();
+ openMenu();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
expect(screen.queryByText('Export to full .CSV')).not.toBeInTheDocument();
@@ -216,6 +224,7 @@ test('Export full Excel is under featureflag', async () => {
};
const props = createProps(VizType.Table);
renderWrapper(props);
+ openMenu();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to Excel')).toBeInTheDocument();
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
@@ -227,6 +236,7 @@ test('Should "export full Excel"', async () => {
};
const props = createProps(VizType.Table);
renderWrapper(props);
+ openMenu();
expect(props.exportFullXLSX).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to full Excel'));
@@ -239,6 +249,7 @@ test('Should not show export full Excel if report is not table', async () => {
[FeatureFlag.AllowFullCsvExport]: true,
};
renderWrapper();
+ openMenu();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to Excel')).toBeInTheDocument();
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
@@ -247,6 +258,7 @@ test('Should not show export full Excel if report is not table', async () => {
test('Should "Show chart description"', () => {
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(props.toggleExpandSlice).toHaveBeenCalledTimes(0);
userEvent.click(screen.getByText('Show chart description'));
expect(props.toggleExpandSlice).toHaveBeenCalledTimes(1);
@@ -256,6 +268,7 @@ test('Should "Show chart description"', () => {
test('Should "Force refresh"', () => {
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(props.forceRefresh).toHaveBeenCalledTimes(0);
userEvent.click(screen.getByText('Force refresh'));
expect(props.forceRefresh).toHaveBeenCalledTimes(1);
@@ -266,6 +279,7 @@ test('Should "Force refresh"', () => {
test('Should "Enter fullscreen"', () => {
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(props.handleToggleFullSize).toHaveBeenCalledTimes(0);
userEvent.click(screen.getByText('Enter fullscreen'));
@@ -278,6 +292,7 @@ test('Drill to detail modal is under featureflag', () => {
};
const props = createProps();
renderWrapper(props);
+ openMenu();
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -293,6 +308,7 @@ test('Should show "Drill to detail" with `can_explore` & `can_samples` perms', (
['can_explore', 'Superset'],
],
});
+ openMenu();
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -311,6 +327,7 @@ test('Should show "Drill to detail" with `can_drill` & `can_samples` perms', ()
['can_drill', 'Dashboard'],
],
});
+ openMenu();
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -329,6 +346,7 @@ test('Should show "Drill to detail" with both `canexplore` + `can_drill` & `can_
['can_drill', 'Dashboard'],
],
});
+ openMenu();
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -344,6 +362,7 @@ test('Should not show "Drill to detail" with neither of required perms', () => {
renderWrapper(props, {
Admin: [['invalid_permission', 'Dashboard']],
});
+ openMenu();
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -359,6 +378,7 @@ test('Should not show "Drill to detail" only `can_dril` perm', () => {
renderWrapper(props, {
Admin: [['can_drill', 'Dashboard']],
});
+ openMenu();
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -371,6 +391,7 @@ test('Should show "View query"', () => {
renderWrapper(props, {
Admin: [['can_view_query', 'Dashboard']],
});
+ openMenu();
expect(screen.getByText('View query')).toBeInTheDocument();
});
@@ -383,6 +404,7 @@ test('Should not show "View query"', () => {
renderWrapper(props, {
Admin: [['invalid_permission', 'Dashboard']],
});
+ openMenu();
expect(screen.queryByText('View query')).not.toBeInTheDocument();
});
@@ -395,6 +417,7 @@ test('Should show "View as table"', () => {
renderWrapper(props, {
Admin: [['can_view_chart_as_table', 'Dashboard']],
});
+ openMenu();
expect(screen.getByText('View as table')).toBeInTheDocument();
});
@@ -407,6 +430,7 @@ test('Should not show "View as table"', () => {
renderWrapper(props, {
Admin: [['invalid_permission', 'Dashboard']],
});
+ openMenu();
expect(screen.queryByText('View as table')).not.toBeInTheDocument();
});
@@ -423,5 +447,6 @@ test('Should not show the "Edit chart" button', () => {
['can_view_chart_as_table', 'Dashboard'],
],
});
+ openMenu();
expect(screen.queryByText('Edit chart')).not.toBeInTheDocument();
});
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 12479d02f..1c31b6752 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -55,6 +55,7 @@ import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal';
import { usePermissions } from 'src/hooks/usePermissions';
+import Button from 'src/components/Button';
import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
import { ViewResultsModalTrigger } from './ViewResultsModalTrigger';
@@ -158,9 +159,8 @@ const SliceHeaderControls = (
props: SliceHeaderControlsPropsWithRouter | SliceHeaderControlsProps,
) => {
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
- const [selectedKeys, setSelectedKeys] = useState
([]);
// setting openKeys undefined falls back to uncontrolled behaviour
- const [openKeys, setOpenKeys] = useState(undefined);
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
props.slice.slice_id,
);
@@ -241,7 +241,7 @@ const SliceHeaderControls = (
// menu closes with a delay, we need to hide it manually,
// so that we don't capture it on the screenshot
const menu = document.querySelector(
- '.ant-dropdown:not(.ant-dropdown-hidden)',
+ '.antd5-dropdown:not(.antd5-dropdown-hidden)',
) as HTMLElement;
if (menu) {
menu.style.visibility = 'hidden';
@@ -284,6 +284,7 @@ const SliceHeaderControls = (
default:
break;
}
+ setIsDropdownVisible(false);
};
const {
@@ -334,24 +335,12 @@ const SliceHeaderControls = (
animationDuration: '0s',
};
- // controlled/uncontrolled behaviour for submenus
- const openKeysProps: Record = {};
- if (openKeys) {
- openKeysProps.openKeys = openKeys;
- }
-
const menu = (
setSelectedKeys(keys)}
- openKeys={openKeys}
id={`slice_${slice.slice_id}-menu`}
- // submenus must be rendered for handleDropdownNavigation
- forceSubMenuRender
- {...openKeysProps}
+ selectable={false}
>
)}
@@ -532,22 +519,17 @@ const SliceHeaderControls = (
overlayStyle={dropdownOverlayStyle}
trigger={['click']}
placement="bottomRight"
- autoFocus
- forceRender
+ open={isDropdownVisible}
+ onOpenChange={visible => setIsDropdownVisible(visible)}
>
- css`
- display: flex;
- align-items: center;
- `}
+
+
{
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
- userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
+ userEvent.click(screen.getByRole('menuitem', { name: 'Download as Image' }));
await waitFor(() => {
expect(downloadAsImage).toHaveBeenCalledTimes(1);
@@ -68,8 +68,8 @@ test('Should call download image on click', async () => {
});
});
-test('Component is rendered with role="button"', async () => {
+test('Component is rendered with role="menuitem"', async () => {
renderComponent();
- const button = screen.getByRole('button', { name: 'Download as Image' });
+ const button = screen.getByRole('menuitem', { name: 'Download as Image' });
expect(button).toBeInTheDocument();
});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
index 505a9b818..5490d7b80 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
@@ -27,7 +27,6 @@ export default function DownloadAsImage({
text,
logEvent,
dashboardTitle,
- ...rest
}: {
text: string;
dashboardTitle: string;
@@ -46,10 +45,13 @@ export default function DownloadAsImage({
};
return (
-
-
- {text}
-
+ {
+ onDownloadImage(e.domEvent);
+ }}
+ >
+ {text}
);
}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
index 56916f4b6..ec6652ca4 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
@@ -58,7 +58,7 @@ test('Should call download pdf on click', async () => {
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
- userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));
+ userEvent.click(screen.getByRole('menuitem', { name: 'Export as PDF' }));
await waitFor(() => {
expect(downloadAsPdf).toHaveBeenCalledTimes(1);
@@ -66,8 +66,8 @@ test('Should call download pdf on click', async () => {
});
});
-test('Component is rendered with role="button"', async () => {
+test('Component is rendered with role="menuitem"', async () => {
renderComponent();
- const button = screen.getByRole('button', { name: 'Export as PDF' });
+ const button = screen.getByRole('menuitem', { name: 'Export as PDF' });
expect(button).toBeInTheDocument();
});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
index a07a2e232..47309b842 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
@@ -27,7 +27,6 @@ export default function DownloadAsPdf({
text,
logEvent,
dashboardTitle,
- ...rest
}: {
text: string;
dashboardTitle: string;
@@ -46,10 +45,13 @@ export default function DownloadAsPdf({
};
return (
-
-
- {text}
-
+ {
+ onDownloadPdf(e.domEvent);
+ }}
+ >
+ {text}
);
}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx
index 0f3049d84..2bd3a2cd9 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx
@@ -26,11 +26,13 @@ const createProps = () => ({
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
dashboardId: 123,
+ title: 'Download',
+ submenuKey: 'download',
});
const renderComponent = () => {
render(
-
+
,
{
@@ -41,10 +43,6 @@ const renderComponent = () => {
test('Should render menu items', () => {
renderComponent();
- expect(
- screen.getByRole('menuitem', { name: 'Export to PDF' }),
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitem', { name: 'Download as Image' }),
- ).toBeInTheDocument();
+ expect(screen.getByText('Export to PDF')).toBeInTheDocument();
+ expect(screen.getByText('Download as Image')).toBeInTheDocument();
});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
index d9ffaaaed..cdb73d5e1 100644
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
@@ -17,17 +17,23 @@
* under the License.
*/
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
-import DownloadScreenshot from './DownloadScreenshot';
+import { Menu } from 'src/components/Menu';
+import { useDownloadScreenshot } from 'src/dashboard/hooks/useDownloadScreenshot';
+import { ComponentProps } from 'react';
import { DownloadScreenshotFormat } from './types';
import DownloadAsPdf from './DownloadAsPdf';
import DownloadAsImage from './DownloadAsImage';
-export interface DownloadMenuItemProps {
+export interface DownloadMenuItemProps
+ extends ComponentProps {
pdfMenuItemTitle: string;
imageMenuItemTitle: string;
dashboardTitle: string;
logEvent?: Function;
dashboardId: number;
+ title: string;
+ disabled?: boolean;
+ submenuKey: string;
}
const DownloadMenuItems = (props: DownloadMenuItemProps) => {
@@ -37,44 +43,45 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => {
logEvent,
dashboardId,
dashboardTitle,
+ submenuKey,
+ disabled,
+ title,
...rest
} = props;
const isWebDriverScreenshotEnabled =
isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) &&
isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot);
+ const downloadScreenshot = useDownloadScreenshot(dashboardId, logEvent);
+
return isWebDriverScreenshotEnabled ? (
- <>
-
-
- >
+
+ downloadScreenshot(DownloadScreenshotFormat.PDF)}
+ >
+ {pdfMenuItemTitle}
+
+ downloadScreenshot(DownloadScreenshotFormat.PNG)}
+ >
+ {imageMenuItemTitle}
+
+
) : (
- <>
+
- >
+
);
};
diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx
index 16bbcff9e..f290a1a4b 100644
--- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx
+++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx
@@ -37,6 +37,7 @@ const createProps = () => ({
emailBody: 'Check out this dashboard: ',
dashboardId: DASHBOARD_ID,
title: 'Test Dashboard',
+ submenuKey: 'share',
});
const { location } = window;
diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
index 6c5468da2..4fb191a06 100644
--- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
+++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { RefObject } from 'react';
+import { ComponentProps, RefObject } from 'react';
import copyTextToClipboard from 'src/utils/copy';
import { t, logging } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
@@ -24,7 +24,7 @@ import { getDashboardPermalink } from 'src/utils/urlUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
import { shallowEqual, useSelector } from 'react-redux';
-interface ShareMenuItemProps {
+interface ShareMenuItemProps extends ComponentProps {
url?: string;
copyMenuItemTitle: string;
emailMenuItemTitle: string;
@@ -38,8 +38,8 @@ interface ShareMenuItemProps {
shareByEmailMenuItemRef?: RefObject;
selectedKeys?: string[];
setOpenKeys?: Function;
- key?: string;
title: string;
+ disabled?: boolean;
}
const ShareMenuItems = (props: ShareMenuItemProps) => {
@@ -52,8 +52,9 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
addSuccessToast,
dashboardId,
dashboardComponentId,
- key,
title,
+ disabled,
+ ...rest
} = props;
const { dataMask, activeTabs } = useSelector(
(state: RootState) => ({
@@ -96,7 +97,12 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
}
return (
-
+
onCopyLink()}>
{copyMenuItemTitle}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx
index b073fe655..45a34b110 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx
@@ -121,13 +121,13 @@ test('Can enable/disable cross-filtering', async () => {
});
await setup();
userEvent.click(screen.getByLabelText('gear'));
- const checkbox = screen.getByRole('checkbox');
- expect(checkbox).toBeChecked();
+ const initialCheckbox = screen.getByRole('checkbox');
+ expect(initialCheckbox).toBeChecked();
- userEvent.click(checkbox);
+ userEvent.click(initialCheckbox);
userEvent.click(screen.getByLabelText('gear'));
- expect(checkbox).not.toBeChecked();
+ expect(screen.getByRole('checkbox')).not.toBeChecked();
});
test('Popover opens with "Vertical" selected', async () => {
@@ -178,19 +178,21 @@ test('On selection change, send request and update checked value', async () => {
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
- expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
- expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
+ const verticalItem = await screen.findByText('Vertical (Left)');
expect(
- within(screen.getAllByRole('menuitem')[4]).getByLabelText('check'),
+ within(verticalItem.closest('li')!).getByLabelText('check'),
).toBeInTheDocument();
userEvent.click(screen.getByText('Horizontal (Top)'));
- // 1st check - checkmark appears immediately after click
+ userEvent.click(screen.getByLabelText('gear'));
+ userEvent.hover(screen.getByText('Orientation of filter bar'));
+
+ const horizontalItem = await screen.findByText('Horizontal (Top)');
expect(
- await within(screen.getAllByRole('menuitem')[5]).findByLabelText('check'),
+ within(horizontalItem.closest('li')!).getByLabelText('check'),
).toBeInTheDocument();
- // successful query
+
await waitFor(() =>
expect(fetchMock.lastCall()?.[1]?.body).toEqual(
JSON.stringify({
@@ -201,23 +203,18 @@ test('On selection change, send request and update checked value', async () => {
}),
),
);
+
await waitFor(() => {
- const menuitems = screen.getAllByRole('menuitem');
- expect(menuitems.length).toBeGreaterThanOrEqual(6);
+ userEvent.click(screen.getByLabelText('gear'));
+ userEvent.hover(screen.getByText('Orientation of filter bar'));
+ const updatedHorizontalItem = screen.getByText('Horizontal (Top)');
+ expect(
+ within(updatedHorizontalItem.closest('li')!).getByLabelText('check'),
+ ).toBeInTheDocument();
+ expect(
+ within(verticalItem.closest('li')!).queryByLabelText('check'),
+ ).not.toBeInTheDocument();
});
-
- userEvent.click(screen.getByLabelText('gear'));
- userEvent.hover(screen.getByText('Orientation of filter bar'));
- expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
- expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
-
- // 2nd check - checkmark stays after successful query
- expect(
- await within(screen.getAllByRole('menuitem')[5]).findByLabelText('check'),
- ).toBeInTheDocument();
- expect(
- within(screen.getAllByRole('menuitem')[4]).queryByLabelText('check'),
- ).not.toBeInTheDocument();
});
test('On failed request, restore previous selection', async () => {
@@ -254,9 +251,8 @@ test('On failed request, restore previous selection', async () => {
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
- expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
-
await waitFor(() => {
+ expect(screen.getByText('Vertical (Left)')).toBeInTheDocument();
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems.length).toBeGreaterThanOrEqual(6);
});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx
index 404d73799..7b617abf1 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx
@@ -33,12 +33,13 @@ import {
saveCrossFiltersSetting,
} from 'src/dashboard/actions/dashboardInfo';
import Icons from 'src/components/Icons';
-import DropdownSelectableIcon, {
- DropDownSelectableProps,
-} from 'src/components/DropdownSelectableIcon';
import Checkbox from 'src/components/Checkbox';
+import { Dropdown } from 'src/components/Dropdown';
+import { Button } from 'src/components';
+import { Space } from 'src/components/Space';
import { clearDataMaskState } from 'src/dataMask/actions';
import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
+import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal';
import { useCrossFiltersScopingModal } from '../CrossFilters/ScopingModal/useCrossFiltersScopingModal';
import FilterConfigurationLink from '../FilterConfigurationLink';
@@ -100,6 +101,12 @@ const FilterBarSettings = () => {
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal();
+ const { openFilterConfigModal, FilterConfigModalComponent } =
+ useFilterConfigModal({
+ createNewOnOpen: filterValues.length === 0,
+ dashboardId,
+ });
+
const updateCrossFiltersSetting = useCallback(
async isEnabled => {
if (!isEnabled) {
@@ -133,7 +140,7 @@ const FilterBarSettings = () => {
[dispatch, filterBarOrientation],
);
- const handleSelect = useCallback(
+ const handleClick = useCallback(
(
selection: Parameters<
Required>['onSelect']
@@ -146,9 +153,16 @@ const FilterBarSettings = () => {
toggleFilterBarOrientation(selectedKey);
} else if (selectedKey === CROSS_FILTERS_SCOPING_MENU_KEY) {
openScopingModal();
+ } else if (selectedKey === ADD_EDIT_FILTERS_MENU_KEY) {
+ openFilterConfigModal();
}
},
- [openScopingModal, toggleCrossFiltering, toggleFilterBarOrientation],
+ [
+ openScopingModal,
+ toggleCrossFiltering,
+ toggleFilterBarOrientation,
+ openFilterConfigModal,
+ ],
);
const crossFiltersMenuItem = useMemo(
@@ -168,21 +182,20 @@ const FilterBarSettings = () => {
);
const menuItems = useMemo(() => {
- const items: DropDownSelectableProps['menuItems'] = [];
+ const items: MenuProps['items'] = [];
if (canEdit) {
items.push({
key: ADD_EDIT_FILTERS_MENU_KEY,
label: (
-
+
{t('Add or edit filters')}
),
- divider: canSetHorizontalFilterBar,
});
+ if (canSetHorizontalFilterBar) {
+ items.push({ type: 'divider' });
+ }
}
if (canEdit) {
items.push({
@@ -192,8 +205,10 @@ const FilterBarSettings = () => {
items.push({
key: CROSS_FILTERS_SCOPING_MENU_KEY,
label: t('Cross-filtering scoping'),
- divider: canSetHorizontalFilterBar,
});
+ if (canSetHorizontalFilterBar) {
+ items.push({ type: 'divider' });
+ }
}
if (canSetHorizontalFilterBar) {
items.push({
@@ -202,17 +217,31 @@ const FilterBarSettings = () => {
children: [
{
key: FilterBarOrientation.Vertical,
- label: t('Vertical (Left)'),
+ label: (
+
+ {t('Vertical (Left)')}
+ {selectedFilterBarOrientation ===
+ FilterBarOrientation.Vertical && }
+
+ ),
},
{
key: FilterBarOrientation.Horizontal,
- label: t('Horizontal (Top)'),
+ label: (
+
+ {t('Horizontal (Top)')}
+ {selectedFilterBarOrientation ===
+ FilterBarOrientation.Horizontal && }
+
+ ),
},
],
+ ...{ 'data-test': 'dropdown-selectable-icon-submenu' },
});
}
return items;
}, [
+ selectedFilterBarOrientation,
canEdit,
canSetHorizontalFilterBar,
crossFiltersMenuItem,
@@ -226,19 +255,24 @@ const FilterBarSettings = () => {
return (
<>
-
+
+
{scopingModal}
+ {FilterConfigModalComponent}
>
);
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx
index 0b5a18835..7ce156a7c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/FilterConfigurationLink.test.tsx
@@ -38,11 +38,16 @@ test('should render the config link text', () => {
});
test('should render the modal on click', () => {
- render(Config link, {
- useRedux: true,
- });
+ const showModal = jest.fn();
+ render(
+
+ Config link
+ ,
+ {
+ useRedux: true,
+ },
+ );
const configLink = screen.getByText('Config link');
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
userEvent.click(configLink);
- expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(showModal).toHaveBeenCalled();
});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
index 6b4fea62a..367072fba 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
@@ -16,70 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ReactNode, FC, useCallback, useState, memo } from 'react';
+import { ReactNode, FC, memo } from 'react';
-import { useDispatch } from 'react-redux';
-import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
-import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
import { getFilterBarTestId } from '../utils';
-import { SaveFilterChangesType } from '../../FiltersConfigModal/types';
export interface FCBProps {
- createNewOnOpen?: boolean;
- dashboardId?: number;
- initialFilterId?: string;
onClick?: () => void;
children?: ReactNode;
}
export const FilterConfigurationLink: FC = ({
- createNewOnOpen,
- dashboardId,
- initialFilterId,
onClick,
children,
-}) => {
- const dispatch = useDispatch();
- const [isOpen, setOpen] = useState(false);
- const close = useCallback(() => {
- setOpen(false);
- }, [setOpen]);
-
- const submit = useCallback(
- async (filterChanges: SaveFilterChangesType) => {
- dispatch(await setFilterConfiguration(filterChanges));
- close();
- },
- [dispatch, close],
- );
-
- const handleClick = useCallback(() => {
- setOpen(true);
- if (onClick) {
- onClick();
- }
- }, [setOpen, onClick]);
-
- return (
- <>
-
- {children}
-
-
- >
- );
-};
+}) => (
+
+ {children}
+
+);
export default memo(FilterConfigurationLink);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx
new file mode 100644
index 000000000..2a08f7800
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
+import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
+import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
+
+interface UseFilterConfigModalProps {
+ createNewOnOpen?: boolean;
+ dashboardId: number;
+ initialFilterId?: string;
+}
+
+interface UseFilterConfigModalReturn {
+ isFilterConfigModalOpen: boolean;
+ openFilterConfigModal: () => void;
+ closeFilterConfigModal: () => void;
+ handleFilterSave: (filterChanges: SaveFilterChangesType) => Promise;
+ FilterConfigModalComponent: JSX.Element | null;
+}
+
+export const useFilterConfigModal = ({
+ createNewOnOpen = false,
+ dashboardId,
+ initialFilterId,
+}: UseFilterConfigModalProps): UseFilterConfigModalReturn => {
+ const dispatch = useDispatch();
+ const [isFilterConfigModalOpen, setIsFilterConfigModalOpen] = useState(false);
+
+ const openFilterConfigModal = useCallback(() => {
+ setIsFilterConfigModalOpen(true);
+ }, []);
+
+ const closeFilterConfigModal = useCallback(() => {
+ setIsFilterConfigModalOpen(false);
+ }, []);
+
+ const handleFilterSave = useCallback(
+ async (filterChanges: SaveFilterChangesType) => {
+ dispatch(await setFilterConfiguration(filterChanges));
+ closeFilterConfigModal();
+ },
+ [dispatch, closeFilterConfigModal],
+ );
+
+ const FilterConfigModalComponent = isFilterConfigModalOpen ? (
+
+ ) : null;
+
+ return {
+ isFilterConfigModalOpen,
+ openFilterConfigModal,
+ closeFilterConfigModal,
+ handleFilterSave,
+ FilterConfigModalComponent,
+ };
+};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
index b82b96767..0d6887f0c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx
@@ -58,9 +58,6 @@ const Wrapper = styled.div`
padding: ${theme.gridUnit * 3}px ${theme.gridUnit * 2}px ${
theme.gridUnit
}px;
- .ant-dropdown-trigger span {
- padding-right: ${theme.gridUnit * 2}px;
- }
`}
`;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx
index 01679b192..9346a72ab 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx
@@ -19,6 +19,7 @@
import { useSelector } from 'react-redux';
import { css, SupersetTheme, useTheme, useTruncation } from '@superset-ui/core';
import Icons from 'src/components/Icons';
+import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal';
import { RootState } from 'src/dashboard/types';
import { Row, FilterName, InternalRow } from './Styles';
import { FilterCardRowProps } from './types';
@@ -39,6 +40,12 @@ export const NameRow = ({
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
+ const { FilterConfigModalComponent, openFilterConfigModal } =
+ useFilterConfigModal({
+ dashboardId,
+ initialFilterId: filter.id,
+ });
+
return (
css`
@@ -58,9 +65,10 @@ export const NameRow = ({
{canEdit && (
{
+ openFilterConfigModal();
+ hidePopover();
+ }}
>
)}
+ {FilterConfigModalComponent}
);
};
diff --git a/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
new file mode 100644
index 000000000..bc8ef7d1f
--- /dev/null
+++ b/superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
@@ -0,0 +1,184 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useCallback, useEffect, useRef } from 'react';
+import { useSelector } from 'react-redux';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import { last } from 'lodash';
+import {
+ logging,
+ t,
+ SupersetClient,
+ SupersetApiError,
+} from '@superset-ui/core';
+import {
+ LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE,
+ LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
+} from 'src/logger/LogUtils';
+import { RootState } from 'src/dashboard/types';
+import { getDashboardUrlParams } from 'src/utils/urlUtils';
+import { DownloadScreenshotFormat } from '../components/menu/DownloadMenuItems/types';
+
+const RETRY_INTERVAL = 3000;
+const MAX_RETRIES = 30;
+
+export const useDownloadScreenshot = (
+ dashboardId: number,
+ logEvent?: Function,
+) => {
+ const activeTabs = useSelector(
+ (state: RootState) => state.dashboardState.activeTabs || undefined,
+ );
+ const anchor = useSelector(
+ (state: RootState) =>
+ last(state.dashboardState.directPathToChild) || undefined,
+ );
+ const dataMask = useSelector(
+ (state: RootState) => state.dataMask || undefined,
+ );
+
+ const { addDangerToast, addSuccessToast, addInfoToast } = useToasts();
+
+ const currentIntervalIds = useRef([]);
+
+ const stopIntervals = useCallback(
+ (message?: 'success' | 'failure') => {
+ currentIntervalIds.current.forEach(clearInterval);
+
+ if (message === 'failure') {
+ addDangerToast(
+ t('The screenshot could not be downloaded. Please, try again later.'),
+ );
+ }
+ if (message === 'success') {
+ addSuccessToast(t('The screenshot has been downloaded.'));
+ }
+ },
+ [addDangerToast, addSuccessToast],
+ );
+
+ const downloadScreenshot = useCallback(
+ (format: DownloadScreenshotFormat) => {
+ let retries = 0;
+
+ const toastIntervalId = setInterval(
+ () =>
+ addInfoToast(
+ t(
+ 'The screenshot is being generated. Please, do not leave the page.',
+ ),
+ { noDuplicate: true },
+ ),
+ RETRY_INTERVAL,
+ );
+
+ currentIntervalIds.current = [
+ ...(currentIntervalIds.current || []),
+ toastIntervalId,
+ ];
+
+ const checkImageReady = (cacheKey: string) =>
+ SupersetClient.get({
+ endpoint: `/api/v1/dashboard/${dashboardId}/screenshot/${cacheKey}/?download_format=${format}`,
+ headers: { Accept: 'application/pdf, image/png' },
+ parseMethod: 'raw',
+ })
+ .then((response: Response) => response.blob())
+ .then(blob => {
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `screenshot.${format}`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ stopIntervals('success');
+ })
+ .catch(err => {
+ if ((err as SupersetApiError).status === 404) {
+ throw new Error('Image not ready');
+ }
+ });
+
+ const fetchImageWithRetry = (cacheKey: string) => {
+ if (retries >= MAX_RETRIES) {
+ stopIntervals('failure');
+ logging.error('Max retries reached');
+ return;
+ }
+ checkImageReady(cacheKey).catch(() => {
+ retries += 1;
+ });
+ };
+
+ SupersetClient.post({
+ endpoint: `/api/v1/dashboard/${dashboardId}/cache_dashboard_screenshot/`,
+ jsonPayload: {
+ anchor,
+ activeTabs,
+ dataMask,
+ urlParams: getDashboardUrlParams(['edit']),
+ },
+ })
+ .then(({ json }) => {
+ const cacheKey = json?.cache_key;
+ if (!cacheKey) {
+ throw new Error('No image URL in response');
+ }
+ const retryIntervalId = setInterval(() => {
+ fetchImageWithRetry(cacheKey);
+ }, RETRY_INTERVAL);
+ currentIntervalIds.current.push(retryIntervalId);
+ fetchImageWithRetry(cacheKey);
+ })
+ .catch(error => {
+ logging.error(error);
+ stopIntervals('failure');
+ })
+ .finally(() => {
+ logEvent?.(
+ format === DownloadScreenshotFormat.PNG
+ ? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE
+ : LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
+ );
+ });
+ },
+ [
+ dashboardId,
+ anchor,
+ activeTabs,
+ dataMask,
+ addInfoToast,
+ stopIntervals,
+ logEvent,
+ ],
+ );
+
+ useEffect(
+ () => () => {
+ if (currentIntervalIds.current.length > 0) {
+ stopIntervals();
+ }
+ currentIntervalIds.current = [];
+ },
+ [stopIntervals],
+ );
+
+ return downloadScreenshot;
+};
diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts
index 0aae5aa4f..f3f0d753b 100644
--- a/superset-frontend/src/dashboard/styles.ts
+++ b/superset-frontend/src/dashboard/styles.ts
@@ -103,10 +103,10 @@ export const filterCardPopoverStyle = (theme: SupersetTheme) => css`
`;
export const chartContextMenuStyles = (theme: SupersetTheme) => css`
- .ant-dropdown-menu.chart-context-menu {
+ .antd5-dropdown-menu.chart-context-menu {
min-width: ${theme.gridUnit * 43}px;
}
- .ant-dropdown-menu-submenu.chart-context-submenu {
+ .antd5-dropdown-menu-submenu.chart-context-submenu {
max-width: ${theme.gridUnit * 60}px;
min-width: ${theme.gridUnit * 40}px;
}
@@ -117,7 +117,7 @@ export const focusStyle = (theme: SupersetTheme) => css`
.ant-tabs-tabpane,
.ant-tabs-tab-btn,
.superset-button,
- .superset-button.ant-dropdown-trigger,
+ .superset-button.antd5-dropdown-trigger,
.header-controls span {
&:focus-visible {
box-shadow: 0 0 0 2px ${theme.colors.primary.dark1};
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 97bd8808e..69718f55f 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -31,6 +31,9 @@ import { sliceUpdated } from 'src/explore/actions/exploreActions';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { applyColors, resetColors } from 'src/utils/colorScheme';
+import ReportModal from 'src/features/reports/ReportModal';
+import DeleteModal from 'src/components/DeleteModal';
+import { deleteActiveReport } from 'src/features/reports/ReportModal/actions';
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
import { useExploreMetadataBar } from './useExploreMetadataBar';
@@ -86,6 +89,8 @@ export const ExploreChartHeader = ({
const dispatch = useDispatch();
const { latestQueryFormData, sliceFormData } = chart;
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
+ const [isReportModalOpen, setIsReportModalOpen] = useState(false);
+ const [currentReportDeleting, setCurrentReportDeleting] = useState(null);
const updateCategoricalNamespace = async () => {
const { dashboards } = metadata || {};
const dashboard =
@@ -128,6 +133,14 @@ export const ExploreChartHeader = ({
setIsPropertiesModalOpen(false);
};
+ const showReportModal = () => {
+ setIsReportModalOpen(true);
+ };
+
+ const closeReportModal = () => {
+ setIsReportModalOpen(false);
+ };
+
const showModal = useCallback(() => {
dispatch(setSaveChartModalVisibility(true));
}, [dispatch]);
@@ -139,6 +152,11 @@ export const ExploreChartHeader = ({
[dispatch],
);
+ const handleReportDelete = async report => {
+ await dispatch(deleteActiveReport(report));
+ setCurrentReportDeleting(null);
+ };
+
const history = useHistory();
const { redirectSQLLab } = actions;
@@ -158,6 +176,8 @@ export const ExploreChartHeader = ({
openPropertiesModal,
ownState,
metadata?.dashboards,
+ showReportModal,
+ setCurrentReportDeleting,
);
const metadataBar = useExploreMetadataBar(metadata, slice);
@@ -229,8 +249,8 @@ export const ExploreChartHeader = ({
}
additionalActionsMenu={menu}
menuDropdownProps={{
- visible: isDropdownVisible,
- onVisibleChange: setIsDropdownVisible,
+ open: isDropdownVisible,
+ onOpenChange: setIsDropdownVisible,
}}
/>
{isPropertiesModalOpen && (
@@ -241,6 +261,33 @@ export const ExploreChartHeader = ({
slice={slice}
/>
)}
+
+
+
+ {currentReportDeleting && (
+ {
+ if (currentReportDeleting) {
+ handleReportDelete(currentReportDeleting);
+ }
+ }}
+ onHide={() => setCurrentReportDeleting(null)}
+ open
+ title={t('Delete Report?')}
+ />
+ )}
>
);
};
diff --git a/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx
index 2490ee754..cc7fd23d3 100644
--- a/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx
+++ b/superset-frontend/src/explore/components/ExportToCSVDropdown/index.tsx
@@ -20,7 +20,7 @@ import { ReactChild, useCallback, Key } from 'react';
import { t, styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
-import { AntdDropdown } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
enum MenuKeys {
@@ -67,9 +67,9 @@ export const ExportToCSVDropdown = ({
);
return (
- (
@@ -84,9 +84,9 @@ export const ExportToCSVDropdown = ({
- }
+ )}
>
{children}
-
+
);
};
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx
index 130e96443..92694dc1a 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.jsx
@@ -107,6 +107,12 @@ describe('DatasourceControl', () => {
expect(screen.queryAllByRole('menuitem')).toHaveLength(3);
});
+ // Close the menu
+ userEvent.click(document.body);
+ await waitFor(() => {
+ expect(screen.queryAllByRole('menuitem')).toHaveLength(0);
+ });
+
rerender(, {
useRedux: true,
useRouter: true,
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 4e5063b1e..3070cdc7d 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -29,7 +29,7 @@ import {
} from '@superset-ui/core';
import { getTemporalColumns } from '@superset-ui/chart-controls';
import { getUrlParam } from 'src/utils/urlUtils';
-import { AntdDropdown } from 'src/components';
+import { Dropdown } from 'src/components/Dropdown';
import { Menu } from 'src/components/Menu';
import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons';
@@ -82,12 +82,8 @@ const Styles = styled.div`
.error-alert {
margin: ${({ theme }) => 2 * theme.gridUnit}px;
}
- .ant-dropdown-trigger {
+ .antd5-dropdown-trigger {
margin-left: ${({ theme }) => 2 * theme.gridUnit}px;
- box-shadow: none;
- &:active {
- box-shadow: none;
- }
}
.btn-group .open .dropdown-toggle {
box-shadow: none;
@@ -410,8 +406,8 @@ class DatasourceControl extends PureComponent {
{extra?.warning_markdown && (
)}
-
datasource.type === DatasourceType.Query
? queryDatasourceMenu
: defaultDatasourceMenu
@@ -423,7 +419,7 @@ class DatasourceControl extends PureComponent {
className="datasource-modal-trigger"
data-test="datasource-menu-trigger"
/>
-
+
{/* missing dataset */}
{isMissingDatasource && isMissingParams && (
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index 21cf69cec..6f7b64bc2 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -108,13 +108,6 @@ export const MenuTrigger = styled(Button)`
`}
`;
-const iconReset = css`
- .ant-dropdown-menu-item > & > .anticon:first-child {
- margin-right: 0;
- vertical-align: 0;
- }
-`;
-
export const useExploreAdditionalActionsMenu = (
latestQueryFormData,
canDownloadCSV,
@@ -123,6 +116,8 @@ export const useExploreAdditionalActionsMenu = (
onOpenPropertiesModal,
ownState,
dashboards,
+ showReportModal,
+ setCurrentReportDeleting,
...rest
) => {
const theme = useTheme();
@@ -330,14 +325,14 @@ export const useExploreAdditionalActionsMenu = (
<>