refactor(Dropdown): Migrate Dropdown to Ant Design 5 (#31972)
This commit is contained in:
parent
38c46fcafd
commit
bcc61bd933
|
|
@ -47,12 +47,12 @@ describe.skip('Dashboard top-level controls', () => {
|
|||
// Solution: pause the network before clicking, assert, then unpause network.
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
waitForChartLoad(mapSpec);
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -65,7 +65,7 @@ describe.skip('Dashboard top-level controls', () => {
|
|||
cy.get('[aria-label="more-horiz"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').click({
|
||||
|
|
@ -73,7 +73,7 @@ describe.skip('Dashboard top-level controls', () => {
|
|||
});
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
// wait all charts force refreshed.
|
||||
|
|
@ -94,7 +94,7 @@ describe.skip('Dashboard top-level controls', () => {
|
|||
cy.get('[aria-label="more-horiz"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').and(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
'antd5-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,15 +54,14 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
|||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
|
|
|
|||
|
|
@ -61,15 +61,14 @@ function drillToDetail(targetMenuItem: string) {
|
|||
const drillToDetailBy = (targetDrill: string) => {
|
||||
interceptSamples();
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
.first()
|
||||
cy.get('.antd5-dropdown:not(.antd5-dropdown-hidden)')
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill to detail by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]',
|
||||
'.antd5-dropdown-menu-submenu:not(.antd5-dropdown-menu-submenu-hidden) [data-test="drill-to-detail-by-submenu"]',
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
|
|
|
|||
|
|
@ -57,16 +57,16 @@ function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
|||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.antd5-menu-item-selected')
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-menu-item').contains('Vertical (Left)').click();
|
||||
cy.get('.antd5-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.antd5-menu-item-selected')
|
||||
cy.get('.antd5-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.antd5-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.get('.antd5-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
|
|
|
|||
|
|
@ -31,35 +31,35 @@ const SAMPLE_DASHBOARDS_INDEXES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|||
|
||||
function openDashboardsAddedTo() {
|
||||
cy.getBySel('actions-trigger').click();
|
||||
cy.get('.ant-dropdown-menu-submenu-title')
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseover', { force: true });
|
||||
}
|
||||
|
||||
function closeDashboardsAddedTo() {
|
||||
cy.get('.ant-dropdown-menu-submenu-title')
|
||||
cy.get('.antd5-dropdown-menu-submenu-title')
|
||||
.contains('On dashboards')
|
||||
.trigger('mouseout', { force: true });
|
||||
cy.getBySel('actions-trigger').click();
|
||||
}
|
||||
|
||||
function verifyDashboardsSubmenuItem(dashboardName) {
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains(dashboardName);
|
||||
closeDashboardsAddedTo();
|
||||
}
|
||||
|
||||
function verifyDashboardSearch() {
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('1');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('1 - Sample dashboard');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('input[placeholder="Search"]')
|
||||
.type('Blahblah');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').contains('No results found');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup')
|
||||
.find('[aria-label="close-circle"]')
|
||||
.click();
|
||||
closeDashboardsAddedTo();
|
||||
|
|
@ -68,8 +68,8 @@ function verifyDashboardSearch() {
|
|||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.antd5-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ describe('Download Chart > Bar chart', () => {
|
|||
};
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .ant-dropdown-menu-submenu-title').click();
|
||||
cy.get('.header-with-actions .antd5-dropdown-trigger').click();
|
||||
cy.get(':nth-child(3) > .antd5-dropdown-menu-submenu-title').click();
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
||||
'.antd5-dropdown-menu-submenu > .antd5-dropdown-menu li:nth-child(3)',
|
||||
).click();
|
||||
cy.verifyDownload('.jpg', {
|
||||
contains: true,
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ describe('SqlLab query tabs', () => {
|
|||
// configure some editor settings
|
||||
cy.get(editorInput).type('some random query string', { force: true });
|
||||
cy.get(queryLimitSelector).parent().click({ force: true });
|
||||
cy.get('.ant-dropdown-menu')
|
||||
cy.get('.antd5-dropdown-menu')
|
||||
.last()
|
||||
.find('.ant-dropdown-menu-item')
|
||||
.find('.antd5-dropdown-menu-item')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -158,10 +158,10 @@ export const sqlLabView = {
|
|||
runButton: '.css-d3dxop',
|
||||
},
|
||||
rowsLimit: {
|
||||
dropdown: '.ant-dropdown-menu',
|
||||
limitButton: '.ant-dropdown-menu-item',
|
||||
dropdown: '.antd5-dropdown-menu',
|
||||
limitButton: '.antd5-dropdown-menu-item',
|
||||
limitButtonText: '.css-151uxnz',
|
||||
limitTextWithValue: '[class="ant-dropdown-trigger"]',
|
||||
limitTextWithValue: '[class="antd5-dropdown-trigger"]',
|
||||
},
|
||||
renderedTableHeader: '.ReactVirtualized__Table__headerRow',
|
||||
renderedTableRow: '.ReactVirtualized__Table__row',
|
||||
|
|
@ -633,7 +633,7 @@ export const dashboardView = {
|
|||
refreshChart: dataTestLocator('refresh-chart-menu-item'),
|
||||
},
|
||||
threeDotsMenuIcon:
|
||||
'.header-with-actions .right-button-panel .ant-dropdown-trigger',
|
||||
'.header-with-actions .right-button-panel .antd5-dropdown-trigger',
|
||||
threeDotsMenuDropdown: dataTestLocator('header-actions-menu'),
|
||||
refreshDashboard: dataTestLocator('refresh-dashboard-menu-item'),
|
||||
saveAsMenuOption: dataTestLocator('save-as-menu-item'),
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ export const GlobalStyles = () => (
|
|||
// TODO: Remove z-indexes when Ant Design is fully upgraded to v5
|
||||
// Prefer vanilla Ant Design z-indexes that should work out of the box
|
||||
.ant-popover,
|
||||
.antd5-dropdown,
|
||||
.ant-dropdown,
|
||||
.ant-select-dropdown,
|
||||
.antd5-modal-wrap,
|
||||
.antd5-modal-mask,
|
||||
|
|
@ -105,13 +103,6 @@ export const GlobalStyles = () => (
|
|||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.ant-dropdown-menu-sub .antd5-menu.antd5-menu-vertical {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ant-dropdown-menu-submenu-title,
|
||||
.ant-dropdown-menu-item {
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { styled, useTheme, t } from '@superset-ui/core';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { useTheme, t } from '@superset-ui/core';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
export interface QueryLimitSelectProps {
|
||||
queryEditorId: string;
|
||||
|
|
@ -34,28 +35,6 @@ export function convertToNumWithSpaces(num: number) {
|
|||
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
|
||||
}
|
||||
|
||||
const LimitSelectStyled = styled.span`
|
||||
${({ theme }) => `
|
||||
.ant-dropdown-trigger {
|
||||
align-items: center;
|
||||
color: ${theme.colors.grayscale.dark2};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
&:last-of-type: {
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function renderQueryLimit(
|
||||
maxRow: number,
|
||||
setQueryLimit: (limit: number) => void,
|
||||
|
|
@ -94,20 +73,18 @@ const QueryLimitSelect = ({
|
|||
dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
|
||||
|
||||
return (
|
||||
<LimitSelectStyled>
|
||||
<AntdDropdown
|
||||
overlay={renderQueryLimit(maxRow, setQueryLimit)}
|
||||
<Dropdown
|
||||
dropdownRender={() => renderQueryLimit(maxRow, setQueryLimit)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<button type="button" onClick={e => e.preventDefault()}>
|
||||
<Button size="small" showMarginRight={false} type="link">
|
||||
<span>{t('LIMIT')}:</span>
|
||||
<span className="limitDropdown">
|
||||
{convertToNumWithSpaces(queryLimit)}
|
||||
</span>
|
||||
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
|
||||
</button>
|
||||
</AntdDropdown>
|
||||
</LimitSelectStyled>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,10 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t, useTheme, styled } from '@superset-ui/core';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { DropdownButton } from 'src/components/DropdownButton';
|
||||
import Button from 'src/components/Button';
|
||||
import { DropdownButtonProps } from 'antd/lib/dropdown';
|
||||
|
||||
interface SaveDatasetActionButtonProps {
|
||||
setShowSave: (arg0: boolean) => void;
|
||||
|
|
@ -34,34 +32,14 @@ const SaveDatasetActionButton = ({
|
|||
}: SaveDatasetActionButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const StyledDropdownButton = styled(
|
||||
DropdownButton as FC<DropdownButtonProps>,
|
||||
)`
|
||||
&.ant-dropdown-button button.ant-btn.ant-btn-default {
|
||||
font-weight: ${theme.gridUnit * 150};
|
||||
background-color: ${theme.colors.primary.light4};
|
||||
color: ${theme.colors.primary.dark1};
|
||||
&:nth-of-type(2) {
|
||||
&:before,
|
||||
&:hover:before {
|
||||
border-left: 2px solid ${theme.colors.primary.dark2};
|
||||
}
|
||||
}
|
||||
}
|
||||
span[name='caret-down'] {
|
||||
margin-left: ${theme.gridUnit * 1}px;
|
||||
color: ${theme.colors.primary.dark2};
|
||||
}
|
||||
`;
|
||||
|
||||
return !overlayMenu ? (
|
||||
<Button onClick={() => setShowSave(true)} buttonStyle="primary">
|
||||
{t('Save')}
|
||||
</Button>
|
||||
) : (
|
||||
<StyledDropdownButton
|
||||
<DropdownButton
|
||||
onClick={() => setShowSave(true)}
|
||||
overlay={overlayMenu}
|
||||
dropdownRender={() => overlayMenu}
|
||||
icon={
|
||||
<Icons.CaretDown
|
||||
iconColor={theme.colors.grayscale.light5}
|
||||
|
|
@ -71,7 +49,7 @@ const SaveDatasetActionButton = ({
|
|||
trigger={['click']}
|
||||
>
|
||||
{t('Save')}
|
||||
</StyledDropdownButton>
|
||||
</DropdownButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ import Mousetrap from 'mousetrap';
|
|||
import Button from 'src/components/Button';
|
||||
import Timer from 'src/components/Timer';
|
||||
import ResizableSidebar from 'src/components/ResizableSidebar';
|
||||
import { AntdDropdown, Skeleton } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Skeleton } from 'src/components';
|
||||
import { Switch } from 'src/components/Switch';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
|
|
@ -868,9 +869,12 @@ const SqlEditor: FC<Props> = ({
|
|||
<span>
|
||||
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
|
||||
</span>
|
||||
<AntdDropdown overlay={renderDropdown()} trigger={['click']}>
|
||||
<Dropdown
|
||||
dropdownRender={() => renderDropdown()}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { useMemo, FC } from 'react';
|
|||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { MenuDotsDropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { styled, t, QueryState } from '@superset-ui/core';
|
||||
import {
|
||||
|
|
@ -88,10 +88,10 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
|
|||
|
||||
return (
|
||||
<TabTitleWrapper>
|
||||
<Dropdown
|
||||
<MenuDotsDropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu style={{ width: 176 }}>
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
className="close-btn"
|
||||
key="1"
|
||||
|
|
|
|||
|
|
@ -30,11 +30,8 @@ import {
|
|||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import Icons from 'src/components/Icons';
|
||||
import type { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import {
|
||||
Skeleton,
|
||||
AntdBreadcrumb as Breadcrumb,
|
||||
AntdDropdown,
|
||||
} from 'src/components';
|
||||
import { Skeleton, AntdBreadcrumb as Breadcrumb } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import FilterableTable from 'src/components/FilterableTable';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import {
|
||||
|
|
@ -308,8 +305,8 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
|||
<Title>
|
||||
<Icons.Table iconSize="l" />
|
||||
{tableName}
|
||||
<AntdDropdown
|
||||
overlay={
|
||||
<Dropdown
|
||||
dropdownRender={() => (
|
||||
<Menu
|
||||
onClick={({ key }) => {
|
||||
if (key === 'refresh-table') {
|
||||
|
|
@ -324,7 +321,7 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
|||
}}
|
||||
items={dropdownMenu}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Icons.DownSquareOutlined
|
||||
|
|
@ -332,7 +329,7 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
|
|||
style={{ marginTop: 2, marginLeft: 4 }}
|
||||
aria-label={t('Table actions')}
|
||||
/>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</Title>
|
||||
{isMetadataRefreshing ? (
|
||||
<Skeleton active />
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const decideType = (buttonStyle: ButtonStyle) => {
|
|||
success: 'primary',
|
||||
secondary: 'default',
|
||||
default: 'default',
|
||||
tertiary: 'dashed',
|
||||
tertiary: 'default',
|
||||
dashed: 'dashed',
|
||||
link: 'link',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import {
|
||||
Behavior,
|
||||
BinaryQueryObjectFilterClause,
|
||||
Column,
|
||||
ContextMenuFilters,
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
|
|
@ -42,8 +43,11 @@ import {
|
|||
import { RootState } from 'src/dashboard/types';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { usePermissions } from 'src/hooks/usePermissions';
|
||||
import { AntdDropdown as Dropdown } from 'src/components/index';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { updateDataMask } from 'src/dataMask/actions';
|
||||
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
|
||||
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
|
||||
import { Dataset } from 'src/components/Chart/types';
|
||||
import { DrillDetailMenuItems } from '../DrillDetail';
|
||||
import { getMenuAdjustedY } from '../utils';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
|
|
@ -114,8 +118,22 @@ const ChartContextMenu = (
|
|||
}>({ clientX: 0, clientY: 0 });
|
||||
|
||||
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
|
||||
const [drillByColumn, setDrillByColumn] = useState<Column>();
|
||||
const [showDrillByModal, setShowDrillByModal] = useState(false);
|
||||
const [dataset, setDataset] = useState<Dataset>();
|
||||
const verboseMap = useVerboseMap(dataset);
|
||||
|
||||
const menuItems = [];
|
||||
const handleDrillBy = useCallback((column: Column, dataset: Dataset) => {
|
||||
setDrillByColumn(column);
|
||||
setDataset(dataset); // Save dataset when drilling
|
||||
setShowDrillByModal(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseDrillByModal = useCallback(() => {
|
||||
setShowDrillByModal(false);
|
||||
}, []);
|
||||
|
||||
const menuItems: React.JSX.Element[] = [];
|
||||
|
||||
const showDrillToDetail =
|
||||
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
|
||||
|
|
@ -249,9 +267,9 @@ const ChartContextMenu = (
|
|||
formData={formData}
|
||||
contextMenuY={clientY}
|
||||
submenuIndex={submenuIndex}
|
||||
canDownload={canDownload}
|
||||
open={openKeys.includes('drill-by-submenu')}
|
||||
key="drill-by-submenu"
|
||||
onDrillBy={handleDrillBy}
|
||||
{...(additionalConfig?.drillBy || {})}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -286,7 +304,7 @@ const ChartContextMenu = (
|
|||
return ReactDOM.createPortal(
|
||||
<>
|
||||
<Dropdown
|
||||
overlay={
|
||||
dropdownRender={() => (
|
||||
<Menu
|
||||
className="chart-context-menu"
|
||||
data-test="chart-context-menu"
|
||||
|
|
@ -302,15 +320,15 @@ const ChartContextMenu = (
|
|||
<Menu.Item disabled>{t('No actions')}</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
)}
|
||||
trigger={['click']}
|
||||
onVisibleChange={value => {
|
||||
onOpenChange={value => {
|
||||
setVisible(value);
|
||||
if (!value) {
|
||||
setOpenKeys([]);
|
||||
}
|
||||
}}
|
||||
visible={visible}
|
||||
open={visible}
|
||||
>
|
||||
<span
|
||||
id={`hidden-span-${id}`}
|
||||
|
|
@ -335,6 +353,16 @@ const ChartContextMenu = (
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{showDrillByModal && drillByColumn && dataset && filters?.drillBy && (
|
||||
<DrillByModal
|
||||
column={drillByColumn}
|
||||
drillByConfig={filters?.drillBy}
|
||||
formData={formData}
|
||||
onHideModal={handleCloseDrillByModal}
|
||||
dataset={{ ...dataset!, verbose_map: verboseMap }}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
)}
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ const renderMenu = ({
|
|||
<DrillByMenuItems
|
||||
formData={formData ?? defaultFormData}
|
||||
drillByConfig={drillByConfig}
|
||||
canDownload
|
||||
open
|
||||
{...rest}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -53,10 +53,8 @@ import {
|
|||
cachedSupersetGet,
|
||||
supersetGetCache,
|
||||
} from 'src/utils/cachedSupersetGet';
|
||||
import { useVerboseMap } from 'src/hooks/apiResources/datasets';
|
||||
import { InputRef } from 'antd-v5';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
import DrillByModal from './DrillByModal';
|
||||
import { getSubmenuYOffset } from '../utils';
|
||||
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
||||
import { Dataset } from '../types';
|
||||
|
|
@ -74,8 +72,8 @@ export interface DrillByMenuItemsProps {
|
|||
onClick?: (event: MouseEvent) => void;
|
||||
openNewModal?: boolean;
|
||||
excludedColumns?: Column[];
|
||||
canDownload: boolean;
|
||||
open: boolean;
|
||||
onDrillBy?: (column: Column, dataset: Dataset) => void;
|
||||
}
|
||||
|
||||
const loadDrillByOptions = getExtensionsRegistry().get('load.drillby.options');
|
||||
|
|
@ -106,8 +104,8 @@ export const DrillByMenuItems = ({
|
|||
onClick = () => {},
|
||||
excludedColumns,
|
||||
openNewModal = true,
|
||||
canDownload,
|
||||
open,
|
||||
onDrillBy,
|
||||
...rest
|
||||
}: DrillByMenuItemsProps) => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -117,25 +115,20 @@ export const DrillByMenuItems = ({
|
|||
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
||||
const [dataset, setDataset] = useState<Dataset>();
|
||||
const [columns, setColumns] = useState<Column[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [currentColumn, setCurrentColumn] = useState();
|
||||
const ref = useRef<InputRef>(null);
|
||||
const showSearch =
|
||||
loadDrillByOptions || columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
|
||||
|
||||
const handleSelection = useCallback(
|
||||
(event, column) => {
|
||||
onClick(event);
|
||||
onSelection(column, drillByConfig);
|
||||
setCurrentColumn(column);
|
||||
if (openNewModal) {
|
||||
setShowModal(true);
|
||||
if (openNewModal && onDrillBy && dataset) {
|
||||
onDrillBy(column, dataset);
|
||||
}
|
||||
},
|
||||
[drillByConfig, onClick, onSelection, openNewModal],
|
||||
[drillByConfig, onClick, onSelection, openNewModal, onDrillBy, dataset],
|
||||
);
|
||||
const closeModal = useCallback(() => {
|
||||
setShowModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -156,7 +149,6 @@ export const DrillByMenuItems = ({
|
|||
?.behaviors.find(behavior => behavior === Behavior.DrillBy),
|
||||
[formData.viz_type],
|
||||
);
|
||||
const verboseMap = useVerboseMap(dataset);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadOptions() {
|
||||
|
|
@ -275,11 +267,11 @@ export const DrillByMenuItems = ({
|
|||
const column = columns[index];
|
||||
return (
|
||||
<MenuItemWithTruncation
|
||||
key={`drill-by-item-${column.column_name}`}
|
||||
menuKey={`drill-by-item-${column.column_name}`}
|
||||
tooltipText={column.verbose_name || column.column_name}
|
||||
{...rest}
|
||||
onClick={e => handleSelection(e, column)}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
{column.verbose_name || column.column_name}
|
||||
</MenuItemWithTruncation>
|
||||
|
|
@ -289,6 +281,7 @@ export const DrillByMenuItems = ({
|
|||
return (
|
||||
<>
|
||||
<Menu.SubMenu
|
||||
key="drill-by-submenu"
|
||||
title={t('Drill by')}
|
||||
popupClassName="chart-context-submenu"
|
||||
popupOffset={[0, submenuYOffset]}
|
||||
|
|
@ -349,16 +342,6 @@ export const DrillByMenuItems = ({
|
|||
)}
|
||||
</div>
|
||||
</Menu.SubMenu>
|
||||
{showModal && (
|
||||
<DrillByModal
|
||||
column={currentColumn}
|
||||
drillByConfig={drillByConfig}
|
||||
formData={formData}
|
||||
onHideModal={closeModal}
|
||||
dataset={{ ...dataset!, verbose_map: verboseMap }}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,8 +60,15 @@ const DISABLED_REASONS = {
|
|||
),
|
||||
};
|
||||
|
||||
const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
|
||||
<Menu.Item disabled {...props}>
|
||||
const DisabledMenuItem = ({
|
||||
children,
|
||||
menuKey,
|
||||
...rest
|
||||
}: {
|
||||
children: ReactNode;
|
||||
menuKey: string;
|
||||
}) => (
|
||||
<Menu.Item disabled key={menuKey} {...rest}>
|
||||
<div
|
||||
css={css`
|
||||
white-space: normal;
|
||||
|
|
@ -183,39 +190,34 @@ const DrillDetailMenuItems = ({
|
|||
}
|
||||
|
||||
const drillToDetailMenuItem = drillDisabled ? (
|
||||
<DisabledMenuItem {...props} key="drill-to-detail-disabled">
|
||||
<DisabledMenuItem menuKey="drill-to-detail-disabled" {...props}>
|
||||
{DRILL_TO_DETAIL}
|
||||
<MenuItemTooltip title={drillDisabled} />
|
||||
</DisabledMenuItem>
|
||||
) : (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
key="drill-to-detail"
|
||||
onClick={openModal.bind(null, [])}
|
||||
>
|
||||
<Menu.Item key="drill-to-detail" onClick={openModal.bind(null, [])}>
|
||||
{DRILL_TO_DETAIL}
|
||||
</Menu.Item>
|
||||
);
|
||||
|
||||
const drillToDetailByMenuItem = drillByDisabled ? (
|
||||
<DisabledMenuItem {...props} key="drill-to-detail-by-disabled">
|
||||
<DisabledMenuItem menuKey="drill-to-detail-by-disabled" {...props}>
|
||||
{DRILL_TO_DETAIL_BY}
|
||||
<MenuItemTooltip title={drillByDisabled} />
|
||||
</DisabledMenuItem>
|
||||
) : (
|
||||
<Menu.SubMenu
|
||||
{...props}
|
||||
popupOffset={[0, submenuYOffset]}
|
||||
popupClassName="chart-context-submenu"
|
||||
title={DRILL_TO_DETAIL_BY}
|
||||
key={key}
|
||||
{...props}
|
||||
>
|
||||
<div data-test="drill-to-detail-by-submenu">
|
||||
{filters.map((filter, i) => (
|
||||
<MenuItemWithTruncation
|
||||
{...props}
|
||||
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
||||
key={`drill-detail-filter-${i}`}
|
||||
menuKey={`drill-detail-filter-${i}`}
|
||||
onClick={openModal.bind(null, [filter])}
|
||||
>
|
||||
{`${DRILL_TO_DETAIL_BY} `}
|
||||
|
|
@ -224,7 +226,6 @@ const DrillDetailMenuItems = ({
|
|||
))}
|
||||
{filters.length > 1 && (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
key="drill-detail-filter-all"
|
||||
onClick={openModal.bind(null, filters)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ export type MenuItemWithTruncationProps = {
|
|||
children: ReactNode;
|
||||
onClick?: MenuItemProps['onClick'];
|
||||
style?: CSSProperties;
|
||||
menuKey?: string;
|
||||
};
|
||||
|
||||
export const MenuItemWithTruncation = ({
|
||||
tooltipText,
|
||||
children,
|
||||
...props
|
||||
onClick,
|
||||
style,
|
||||
menuKey,
|
||||
}: MenuItemWithTruncationProps) => {
|
||||
const [itemRef, itemIsTruncated] = useCSSTextTruncation<HTMLDivElement>();
|
||||
|
||||
|
|
@ -43,7 +46,9 @@ export const MenuItemWithTruncation = ({
|
|||
display: flex;
|
||||
line-height: 1.5em;
|
||||
`}
|
||||
{...props}
|
||||
eventKey={menuKey}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<Tooltip title={itemIsTruncated ? tooltipText : null}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { Dropdown, DropdownProps } from '.';
|
||||
import { MenuDotsDropdown, MenuDotsDropdownProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Dropdown',
|
||||
|
|
@ -50,8 +50,8 @@ const customOverlay = (
|
|||
export const InteractiveDropdown = ({
|
||||
overlayType,
|
||||
...rest
|
||||
}: DropdownProps & { overlayType: string }) => (
|
||||
<Dropdown
|
||||
}: MenuDotsDropdownProps & { overlayType: string }) => (
|
||||
<MenuDotsDropdown
|
||||
{...rest}
|
||||
overlay={overlayType === 'custom' ? customOverlay : menu}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -24,13 +24,10 @@ import {
|
|||
cloneElement,
|
||||
} from 'react';
|
||||
|
||||
import { AntdDropdown } from 'src/components';
|
||||
// TODO: @geido - Remove these after dropdown is fully migrated to Antd v5
|
||||
import {
|
||||
Dropdown as Antd5Dropdown,
|
||||
DropDownProps as Antd5DropdownProps,
|
||||
Dropdown as AntdDropdown,
|
||||
DropdownProps as AntdDropdownProps,
|
||||
} from 'antd-v5';
|
||||
import { DropDownProps } from 'antd/lib/dropdown';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
||||
|
|
@ -83,7 +80,8 @@ export enum IconOrientation {
|
|||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
export interface DropdownProps extends DropDownProps {
|
||||
|
||||
export interface MenuDotsDropdownProps extends AntdDropdownProps {
|
||||
overlay: ReactElement;
|
||||
iconOrientation?: IconOrientation;
|
||||
}
|
||||
|
|
@ -100,19 +98,19 @@ const RenderIcon = (
|
|||
return component;
|
||||
};
|
||||
|
||||
export const Dropdown = ({
|
||||
export const MenuDotsDropdown = ({
|
||||
overlay,
|
||||
iconOrientation = IconOrientation.Vertical,
|
||||
...rest
|
||||
}: DropdownProps) => (
|
||||
<AntdDropdown overlay={overlay} {...rest}>
|
||||
}: MenuDotsDropdownProps) => (
|
||||
<AntdDropdown dropdownRender={() => overlay} {...rest}>
|
||||
<MenuDotsWrapper data-test="dropdown-trigger">
|
||||
{RenderIcon(iconOrientation)}
|
||||
</MenuDotsWrapper>
|
||||
</AntdDropdown>
|
||||
);
|
||||
|
||||
export interface NoAnimationDropdownProps extends Antd5DropdownProps {
|
||||
export interface NoAnimationDropdownProps extends AntdDropdownProps {
|
||||
children: ReactNode;
|
||||
onBlur?: (e: FocusEvent<HTMLDivElement>) => void;
|
||||
onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;
|
||||
|
|
@ -126,8 +124,13 @@ export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<Antd5Dropdown overlayStyle={props.overlayStyle} {...rest}>
|
||||
<AntdDropdown autoFocus overlayStyle={props.overlayStyle} {...rest}>
|
||||
{childrenWithProps}
|
||||
</Antd5Dropdown>
|
||||
</AntdDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export type DropdownProps = AntdDropdownProps;
|
||||
export const Dropdown = (props: DropdownProps) => (
|
||||
<AntdDropdown autoFocus {...props} />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,90 +16,39 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
import { AntdDropdown, AntdTooltip } from 'src/components';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Dropdown } from 'antd-v5';
|
||||
import { Tooltip, TooltipPlacement } from 'src/components/Tooltip';
|
||||
import { kebabCase } from 'lodash';
|
||||
|
||||
const StyledDropdownButton = styled.div`
|
||||
.ant-btn-group {
|
||||
button.ant-btn {
|
||||
background-color: ${({ theme }) => theme.colors.primary.dark1};
|
||||
border-color: transparent;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
outline: none;
|
||||
&:first-of-type {
|
||||
border-radius: ${({ theme }) =>
|
||||
`${theme.gridUnit}px 0 0 ${theme.gridUnit}px`};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light2};
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
margin: 0;
|
||||
border-radius: ${({ theme }) =>
|
||||
`0 ${theme.gridUnit}px ${theme.gridUnit}px 0`};
|
||||
width: ${({ theme }) => theme.gridUnit * 9}px;
|
||||
&:before,
|
||||
&:hover:before {
|
||||
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5};
|
||||
content: '';
|
||||
display: block;
|
||||
height: ${({ theme }) => theme.gridUnit * 8}px;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
width: ${({ theme }) => theme.gridUnit * 0.25}px;
|
||||
}
|
||||
|
||||
&:disabled:before {
|
||||
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.base};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DropdownButtonProps {
|
||||
overlay: ReactElement;
|
||||
export type DropdownButtonProps = ComponentProps<typeof Dropdown.Button> & {
|
||||
tooltip?: string;
|
||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
buttonsRender?: ((buttons: ReactNode[]) => ReactNode[]) | undefined;
|
||||
}
|
||||
tooltipPlacement?: TooltipPlacement;
|
||||
};
|
||||
|
||||
export const DropdownButton = ({
|
||||
overlay,
|
||||
dropdownRender,
|
||||
tooltip,
|
||||
placement,
|
||||
tooltipPlacement,
|
||||
children,
|
||||
...rest
|
||||
}: DropdownButtonProps) => {
|
||||
const buildButton = (
|
||||
props: {
|
||||
buttonsRender?: DropdownButtonProps['buttonsRender'];
|
||||
} = {},
|
||||
) => (
|
||||
<StyledDropdownButton>
|
||||
<AntdDropdown.Button overlay={overlay} {...rest} {...props} />
|
||||
</StyledDropdownButton>
|
||||
const button = (
|
||||
<Dropdown.Button dropdownRender={dropdownRender} {...rest}>
|
||||
{children}
|
||||
</Dropdown.Button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return buildButton({
|
||||
buttonsRender: ([leftButton, rightButton]) => [
|
||||
<AntdTooltip
|
||||
placement={placement}
|
||||
return (
|
||||
<Tooltip
|
||||
placement={tooltipPlacement}
|
||||
id={`${kebabCase(tooltip)}-tooltip`}
|
||||
title={tooltip}
|
||||
>
|
||||
{leftButton}
|
||||
</AntdTooltip>,
|
||||
rightButton,
|
||||
],
|
||||
});
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return buildButton();
|
||||
return button;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import Icons from 'src/components/Icons';
|
||||
import DropdownSelectableIcon, { DropDownSelectableProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'DropdownSelectableIcon',
|
||||
component: DropdownSelectableIcon,
|
||||
};
|
||||
|
||||
export const Component = (props: DropDownSelectableProps) => (
|
||||
<DropdownSelectableIcon
|
||||
{...props}
|
||||
icon={<Icons.Gear name="gear" iconColor="#000000" />}
|
||||
/>
|
||||
);
|
||||
|
||||
Component.args = {
|
||||
info: 'Info go here',
|
||||
selectedKeys: ['vertical'],
|
||||
menuItems: [
|
||||
{
|
||||
key: 'vertical',
|
||||
label: 'Vertical',
|
||||
},
|
||||
{
|
||||
key: 'horizontal',
|
||||
label: 'Horizontal',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Component.argTypes = {
|
||||
onSelect: {
|
||||
action: 'onSelect',
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import Icons from 'src/components/Icons';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import DropdownSelectableIcon, { DropDownSelectableProps } from '.';
|
||||
|
||||
const mockedProps = {
|
||||
menuItems: [
|
||||
{
|
||||
key: 'vertical',
|
||||
label: 'vertical',
|
||||
},
|
||||
{
|
||||
key: 'horizontal',
|
||||
label: 'horizontal',
|
||||
},
|
||||
],
|
||||
selectedKeys: [],
|
||||
icon: <Icons.Gear name="gear" />,
|
||||
};
|
||||
|
||||
const asyncRender = (props: DropDownSelectableProps) =>
|
||||
waitFor(() => render(<DropdownSelectableIcon {...props} />));
|
||||
|
||||
const openMenu = () => {
|
||||
userEvent.click(screen.getByRole('img', { name: 'gear' }));
|
||||
};
|
||||
|
||||
test('should render', async () => {
|
||||
const { container } = await asyncRender(mockedProps);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the icon', async () => {
|
||||
await asyncRender(mockedProps);
|
||||
expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render the info', async () => {
|
||||
await asyncRender(mockedProps);
|
||||
openMenu();
|
||||
expect(
|
||||
screen.queryByTestId('dropdown-selectable-info'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the info', async () => {
|
||||
const infoProps = {
|
||||
...mockedProps,
|
||||
info: 'Test',
|
||||
};
|
||||
await asyncRender(infoProps);
|
||||
openMenu();
|
||||
expect(screen.getByTestId('dropdown-selectable-info')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the menu items', async () => {
|
||||
await asyncRender(mockedProps);
|
||||
openMenu();
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(2);
|
||||
expect(screen.getByText('vertical')).toBeInTheDocument();
|
||||
expect(screen.getByText('horizontal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render any selected menu item', async () => {
|
||||
await asyncRender(mockedProps);
|
||||
openMenu();
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(2);
|
||||
expect(screen.queryByRole('img', { name: 'check' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the selected menu items', async () => {
|
||||
const selectedProps = {
|
||||
...mockedProps,
|
||||
selectedKeys: ['vertical'],
|
||||
};
|
||||
await asyncRender(selectedProps);
|
||||
openMenu();
|
||||
expect(screen.getByRole('img', { name: 'check' })).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { addAlpha, styled, useTheme } from '@superset-ui/core';
|
||||
import { FC, RefObject, useMemo, ReactNode, useState } from 'react';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { DropdownButton } from 'src/components/DropdownButton';
|
||||
import { DropdownButtonProps } from 'antd/lib/dropdown';
|
||||
import { Menu, MenuProps } from 'src/components/Menu';
|
||||
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
type SubMenuItemProps = { key: string; label: string | ReactNode };
|
||||
|
||||
export interface DropDownSelectableProps extends Pick<MenuProps, 'onSelect'> {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
icon: ReactNode;
|
||||
info?: string;
|
||||
menuItems: {
|
||||
key: string;
|
||||
label: string | ReactNode;
|
||||
children?: SubMenuItemProps[];
|
||||
divider?: boolean;
|
||||
}[];
|
||||
selectedKeys?: string[];
|
||||
}
|
||||
|
||||
const StyledDropdownButton = styled(DropdownButton as FC<DropdownButtonProps>)`
|
||||
button.ant-btn:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
> button.ant-btn:nth-of-type(2) {
|
||||
display: inline-flex;
|
||||
background-color: transparent !important;
|
||||
height: unset;
|
||||
padding: 0;
|
||||
border: none;
|
||||
width: auto !important;
|
||||
|
||||
.anticon {
|
||||
line-height: 0;
|
||||
}
|
||||
&:after {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
${({ theme }) => `
|
||||
box-shadow:
|
||||
0 3px 6px -4px ${addAlpha(theme.colors.grayscale.dark2, 0.12)},
|
||||
0 6px 16px 0
|
||||
${addAlpha(theme.colors.grayscale.dark2, 0.08)},
|
||||
0 9px 28px 8px
|
||||
${addAlpha(theme.colors.grayscale.dark2, 0.05)};
|
||||
.info {
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
color: ${theme.colors.grayscale.base};
|
||||
padding: ${theme.gridUnit}px ${theme.gridUnit * 3}px ${
|
||||
theme.gridUnit
|
||||
}px ${theme.gridUnit * 3}px;
|
||||
}
|
||||
.ant-dropdown-menu-item-selected {
|
||||
color: ${theme.colors.grayscale.dark1};
|
||||
background-color: ${theme.colors.primary.light5};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyleMenuItem = styled(Menu.Item)<{ divider?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
> span {
|
||||
width: 100%;
|
||||
}
|
||||
border-bottom: ${({ divider, theme }) =>
|
||||
divider ? `1px solid ${theme.colors.grayscale.light3};` : 'none;'};
|
||||
`;
|
||||
|
||||
const StyleSubmenuItem = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default (props: DropDownSelectableProps) => {
|
||||
const theme = useTheme();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { icon, info, menuItems, selectedKeys, onSelect } = props;
|
||||
|
||||
const handleVisibleChange = setVisible;
|
||||
|
||||
const handleMenuSelect: MenuProps['onSelect'] = info => {
|
||||
if (onSelect) {
|
||||
onSelect(info);
|
||||
}
|
||||
setVisible(false);
|
||||
};
|
||||
const menuItem = useMemo(
|
||||
() => (label: string | ReactNode, key: string, divider?: boolean) => (
|
||||
<StyleMenuItem key={key} divider={divider}>
|
||||
<StyleSubmenuItem>
|
||||
{label}
|
||||
{selectedKeys?.includes(key) && (
|
||||
<Icons.Check
|
||||
iconColor={theme.colors.primary.base}
|
||||
className="tick-menu-item"
|
||||
iconSize="xl"
|
||||
/>
|
||||
)}
|
||||
</StyleSubmenuItem>
|
||||
</StyleMenuItem>
|
||||
),
|
||||
[selectedKeys, theme.colors.primary.base],
|
||||
);
|
||||
|
||||
const overlayMenu = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{info && (
|
||||
<div className="info" data-test="dropdown-selectable-info">
|
||||
{info}
|
||||
</div>
|
||||
)}
|
||||
<StyledMenu
|
||||
selectedKeys={selectedKeys}
|
||||
onSelect={handleMenuSelect}
|
||||
selectable
|
||||
>
|
||||
{menuItems.map(m =>
|
||||
m.children?.length ? (
|
||||
<SubMenu
|
||||
title={m.label}
|
||||
key={m.key}
|
||||
data-test="dropdown-selectable-icon-submenu"
|
||||
>
|
||||
{m.children.map(s => menuItem(s.label, s.key))}
|
||||
</SubMenu>
|
||||
) : (
|
||||
menuItem(m.label, m.key, m.divider)
|
||||
),
|
||||
)}
|
||||
</StyledMenu>
|
||||
</>
|
||||
),
|
||||
[selectedKeys, onSelect, info, menuItems, menuItem, handleMenuSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDropdownButton
|
||||
overlay={overlayMenu}
|
||||
trigger={['click']}
|
||||
icon={icon}
|
||||
visible={visible}
|
||||
onVisibleChange={handleVisibleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import Icons from 'src/components/Icons';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
|
|
@ -70,8 +70,8 @@ export const SupersetListViewCard = ({
|
|||
saveFaveStar={action('saveFaveStar')}
|
||||
isStarred={isStarred}
|
||||
/>
|
||||
<AntdDropdown
|
||||
overlay={
|
||||
<Dropdown
|
||||
dropdownRender={() => (
|
||||
<Menu>
|
||||
<Menu.Item role="button" tabIndex={0} onClick={action('Delete')}>
|
||||
<Icons.Trash /> Delete
|
||||
|
|
@ -80,10 +80,10 @@ export const SupersetListViewCard = ({
|
|||
<Icons.EditAlt /> Edit
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icons.MoreHoriz />
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
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'];
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
||||
import { AntdDropdown, AntdDropdownProps } from 'src/components';
|
||||
import { Dropdown, DropdownProps } from 'src/components/Dropdown';
|
||||
import { TooltipPlacement } from 'src/components/Tooltip';
|
||||
import {
|
||||
DynamicEditableTitle,
|
||||
|
|
@ -116,7 +116,7 @@ export type PageHeaderWithActionsProps = {
|
|||
titlePanelAdditionalItems: ReactNode;
|
||||
rightPanelAdditionalItems: ReactNode;
|
||||
additionalActionsMenu: ReactElement;
|
||||
menuDropdownProps: Omit<AntdDropdownProps, 'overlay'>;
|
||||
menuDropdownProps: Omit<DropdownProps, 'overlay'>;
|
||||
tooltipProps?: {
|
||||
text?: string;
|
||||
placement?: TooltipPlacement;
|
||||
|
|
@ -155,9 +155,9 @@ export const PageHeaderWithActions = ({
|
|||
{rightPanelAdditionalItems}
|
||||
<div css={additionalActionsContainerStyles}>
|
||||
{showMenuDropdown && (
|
||||
<AntdDropdown
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={additionalActionsMenu}
|
||||
dropdownRender={() => additionalActionsMenu}
|
||||
{...menuDropdownProps}
|
||||
>
|
||||
<Button
|
||||
|
|
@ -173,7 +173,7 @@ export const PageHeaderWithActions = ({
|
|||
iconSize="l"
|
||||
/>
|
||||
</Button>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import { Key } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { styled, useTheme } from '@superset-ui/core';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
||||
|
|
@ -89,10 +89,10 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
|
|||
const theme = useTheme();
|
||||
const selected = options.find(opt => opt.value === value);
|
||||
return (
|
||||
<AntdDropdown
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlayStyle={{ zIndex: theme.zIndex.max }}
|
||||
overlay={
|
||||
dropdownRender={() => (
|
||||
<Menu onClick={({ key }: HandleSelectProps) => onChange(key)}>
|
||||
{options.map(option => (
|
||||
<MenuItem
|
||||
|
|
@ -106,7 +106,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
|
|||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div role="button" css={{ display: 'flex', alignItems: 'center' }}>
|
||||
{selected && renderButton(selected)}
|
||||
|
|
@ -115,7 +115,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
|
|||
css={{ marginTop: theme.gridUnit * 0.5 }}
|
||||
/>
|
||||
</div>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Dropdown, IconOrientation } from 'src/components/Dropdown';
|
||||
import { MenuDotsDropdown, IconOrientation } from 'src/components/Dropdown';
|
||||
import { Menu, MenuProps } from 'src/components/Menu';
|
||||
|
||||
/**
|
||||
|
|
@ -126,7 +126,7 @@ export function ActionCell(props: ActionCellProps) {
|
|||
setVisible(flag);
|
||||
};
|
||||
return (
|
||||
<Dropdown
|
||||
<MenuDotsDropdown
|
||||
iconOrientation={IconOrientation.Horizontal}
|
||||
onVisibleChange={handleVisibleChange}
|
||||
trigger={['click']}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { TooltipProps, TooltipPlacement } from 'antd-v5/lib/tooltip';
|
|||
export { TooltipProps, TooltipPlacement };
|
||||
|
||||
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
|
||||
<>
|
||||
<AntdTooltip
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
|
|
@ -32,5 +31,4 @@ export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
|
|||
color={`${supersetTheme.colors.grayscale.dark2}e6`}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Key, ReactNode, PureComponent } from 'react';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import rison from 'rison';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import Button from 'src/components/Button';
|
||||
import { t, styled, SupersetClient } from '@superset-ui/core';
|
||||
|
|
@ -115,9 +115,9 @@ class CssEditor extends PureComponent<CssEditorProps, CssEditorState> {
|
|||
</Menu>
|
||||
);
|
||||
return (
|
||||
<AntdDropdown overlay={menu} placement="bottomRight">
|
||||
<Dropdown dropdownRender={() => menu} placement="bottomRight">
|
||||
<Button>{t('Load a CSS template')}</Button>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -53,9 +53,6 @@ jest.mock('src/components/Select/Select', () => () => (
|
|||
jest.mock('src/components/Select/AsyncSelect', () => () => (
|
||||
<div data-test="mock-async-select" />
|
||||
));
|
||||
jest.mock('src/dashboard/components/Header/HeaderActionsDropdown', () => () => (
|
||||
<div data-test="mock-header-actions-dropdown" />
|
||||
));
|
||||
jest.mock('src/components/PageHeaderWithActions', () => ({
|
||||
PageHeaderWithActions: () => (
|
||||
<div data-test="mock-page-header-with-actions" />
|
||||
|
|
|
|||
|
|
@ -1,260 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { HeaderDropdownProps } from 'src/dashboard/components/Header/types';
|
||||
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
||||
import { HeaderActionsDropdown } from '.';
|
||||
|
||||
const createProps = (): HeaderDropdownProps => ({
|
||||
addSuccessToast: jest.fn(),
|
||||
addDangerToast: jest.fn(),
|
||||
customCss: '.ant-menu {margin-left: 100px;}',
|
||||
dashboardId: 1,
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
dash_save_perm: true,
|
||||
userId: '1',
|
||||
metadata: {},
|
||||
common: {
|
||||
conf: {
|
||||
DASHBOARD_AUTO_REFRESH_INTERVALS: [
|
||||
[0, "Don't refresh"],
|
||||
[10, '10 seconds'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
dashboardTitle: 'Title',
|
||||
editMode: false,
|
||||
expandedSlices: {},
|
||||
forceRefreshAllCharts: jest.fn(),
|
||||
hasUnsavedChanges: false,
|
||||
isLoading: false,
|
||||
layout: {},
|
||||
onChange: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
refreshFrequency: 200,
|
||||
setRefreshFrequency: jest.fn(),
|
||||
shouldPersistRefreshFrequency: false,
|
||||
showPropertiesModal: jest.fn(),
|
||||
startPeriodicRender: jest.fn(),
|
||||
updateCss: jest.fn(),
|
||||
userCanEdit: false,
|
||||
userCanSave: false,
|
||||
userCanShare: false,
|
||||
userCanCurate: false,
|
||||
lastModifiedTime: 0,
|
||||
isDropdownVisible: true,
|
||||
setIsDropdownVisible: jest.fn(),
|
||||
directPathToChild: [],
|
||||
manageEmbedded: jest.fn(),
|
||||
dataMask: {},
|
||||
logEvent: jest.fn(),
|
||||
refreshLimit: 0,
|
||||
refreshWarning: '',
|
||||
});
|
||||
|
||||
const editModeOnProps = {
|
||||
...createProps(),
|
||||
editMode: true,
|
||||
};
|
||||
|
||||
const editModeOnWithFilterScopesProps = {
|
||||
...editModeOnProps,
|
||||
dashboardInfo: {
|
||||
...editModeOnProps.dashboardInfo,
|
||||
metadata: {
|
||||
filter_scopes: {
|
||||
'1': { scopes: ['ROOT_ID'], immune: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const guestUserProps = {
|
||||
...createProps(),
|
||||
dashboardInfo: {
|
||||
...createProps().dashboardInfo,
|
||||
userId: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
function setup(props: HeaderDropdownProps) {
|
||||
return render(
|
||||
<div className="dashboard-header">
|
||||
<HeaderActionsDropdown {...props} />
|
||||
</div>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
}
|
||||
|
||||
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
|
||||
|
||||
test('should render', () => {
|
||||
const mockedProps = createProps();
|
||||
const { container } = setup(mockedProps);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Download dropdown button when not in edit mode', () => {
|
||||
const mockedProps = createProps();
|
||||
setup(mockedProps);
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Download' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the menu items', async () => {
|
||||
const mockedProps = createProps();
|
||||
setup(mockedProps);
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(4);
|
||||
expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enter fullscreen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the menu items in edit mode', async () => {
|
||||
setup(editModeOnProps);
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(4);
|
||||
expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit properties')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit CSS')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the menu items in Embedded mode', async () => {
|
||||
setup(guestUserProps);
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
|
||||
expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download')).toBeInTheDocument();
|
||||
expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render filter mapping in edit mode if explicit filter scopes undefined', async () => {
|
||||
setup(editModeOnProps);
|
||||
expect(screen.queryByText('Set filter mapping')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render filter mapping in edit mode if explicit filter scopes defined', async () => {
|
||||
setup(editModeOnWithFilterScopesProps);
|
||||
expect(screen.getByText('Set filter mapping')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the share actions', async () => {
|
||||
const mockedProps = createProps();
|
||||
const canShareProps = {
|
||||
...mockedProps,
|
||||
userCanShare: true,
|
||||
};
|
||||
setup(canShareProps);
|
||||
|
||||
expect(screen.getByText('Share')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the "Save as" menu item when user can save', async () => {
|
||||
const mockedProps = createProps();
|
||||
const canSaveProps = {
|
||||
...mockedProps,
|
||||
userCanSave: true,
|
||||
};
|
||||
setup(canSaveProps);
|
||||
expect(screen.getByText('Save as')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render the "Save as" menu item when user cannot save', async () => {
|
||||
const mockedProps = createProps();
|
||||
setup(mockedProps);
|
||||
expect(screen.queryByText('Save as')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the "Refresh dashboard" menu item as disabled when loading', async () => {
|
||||
const mockedProps = createProps();
|
||||
const loadingProps = {
|
||||
...mockedProps,
|
||||
isLoading: true,
|
||||
};
|
||||
setup(loadingProps);
|
||||
expect(screen.getByText('Refresh dashboard').parentElement).toHaveClass(
|
||||
'ant-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
|
||||
test('should NOT render the "Refresh dashboard" menu item as disabled', async () => {
|
||||
const mockedProps = createProps();
|
||||
setup(mockedProps);
|
||||
expect(screen.getByText('Refresh dashboard')).not.toHaveClass(
|
||||
'ant-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render with custom css', () => {
|
||||
const mockedProps = createProps();
|
||||
const { customCss } = mockedProps;
|
||||
setup(mockedProps);
|
||||
injectCustomCss(customCss);
|
||||
expect(screen.getByTestId('header-actions-menu')).toHaveStyle(
|
||||
'margin-left: 100px',
|
||||
);
|
||||
});
|
||||
|
||||
test('should refresh the charts', async () => {
|
||||
const mockedProps = createProps();
|
||||
setup(mockedProps);
|
||||
userEvent.click(screen.getByText('Refresh dashboard'));
|
||||
expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1);
|
||||
expect(mockedProps.addSuccessToast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show the properties modal', async () => {
|
||||
setup(editModeOnProps);
|
||||
userEvent.click(screen.getByText('Edit properties'));
|
||||
expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('UNSAFE_componentWillReceiveProps', () => {
|
||||
let wrapper: any;
|
||||
|
||||
const mockedProps = createProps();
|
||||
const props = { ...mockedProps, customCss: '' };
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<HeaderActionsDropdown {...props} />);
|
||||
wrapper.setState({ css: props.customCss });
|
||||
sinon.spy(wrapper.instance(), 'setState');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.instance().setState.restore();
|
||||
});
|
||||
|
||||
it('css should update state and inject custom css', () => {
|
||||
wrapper.instance().UNSAFE_componentWillReceiveProps({
|
||||
...props,
|
||||
customCss: mockedProps.customCss,
|
||||
});
|
||||
expect(wrapper.instance().setState.calledOnce).toBe(true);
|
||||
const stateKeys = Object.keys(wrapper.instance().setState.lastCall.args[0]);
|
||||
expect(stateKeys).toContain('css');
|
||||
});
|
||||
});
|
||||
|
|
@ -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 handleReportDelete = async report => {
|
||||
await dispatch(deleteActiveReport(report));
|
||||
setCurrentReportDeleting(null);
|
||||
};
|
||||
|
||||
const additionalActionsMenu = useMemo(
|
||||
() => (
|
||||
<ConnectedHeaderActionsDropdown
|
||||
addSuccessToast={boundActionCreators.addSuccessToast}
|
||||
addDangerToast={boundActionCreators.addDangerToast}
|
||||
dashboardId={dashboardInfo.id}
|
||||
dashboardTitle={dashboardTitle}
|
||||
dashboardInfo={dashboardInfo}
|
||||
dataMask={dataMask}
|
||||
layout={layout}
|
||||
expandedSlices={expandedSlices}
|
||||
customCss={customCss}
|
||||
colorNamespace={colorNamespace}
|
||||
colorScheme={colorScheme}
|
||||
onSave={boundActionCreators.onSave}
|
||||
onChange={boundActionCreators.onChange}
|
||||
forceRefreshAllCharts={forceRefresh}
|
||||
startPeriodicRender={startPeriodicRender}
|
||||
refreshFrequency={refreshFrequency}
|
||||
shouldPersistRefreshFrequency={shouldPersistRefreshFrequency}
|
||||
setRefreshFrequency={boundActionCreators.setRefreshFrequency}
|
||||
updateCss={boundActionCreators.updateCss}
|
||||
editMode={editMode}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
userCanEdit={userCanEdit}
|
||||
userCanShare={userCanShare}
|
||||
userCanSave={userCanSaveAs}
|
||||
userCanCurate={userCanCurate}
|
||||
isLoading={isLoading}
|
||||
showPropertiesModal={showPropertiesModal}
|
||||
manageEmbedded={showEmbedModal}
|
||||
refreshLimit={refreshLimit}
|
||||
refreshWarning={refreshWarning}
|
||||
lastModifiedTime={actualLastModifiedTime}
|
||||
isDropdownVisible={isDropdownVisible}
|
||||
setIsDropdownVisible={setDropdownVisible}
|
||||
logEvent={boundActionCreators.logEvent}
|
||||
/>
|
||||
),
|
||||
[
|
||||
actualLastModifiedTime,
|
||||
boundActionCreators.addDangerToast,
|
||||
boundActionCreators.addSuccessToast,
|
||||
boundActionCreators.logEvent,
|
||||
boundActionCreators.onChange,
|
||||
boundActionCreators.onSave,
|
||||
boundActionCreators.setRefreshFrequency,
|
||||
boundActionCreators.updateCss,
|
||||
colorNamespace,
|
||||
colorScheme,
|
||||
customCss,
|
||||
const [menu, isDropdownVisible, setIsDropdownVisible] = useHeaderActionsMenu({
|
||||
addSuccessToast: boundActionCreators.addSuccessToast,
|
||||
addDangerToast: boundActionCreators.addDangerToast,
|
||||
dashboardInfo,
|
||||
dashboardId: dashboardInfo.id,
|
||||
dashboardTitle,
|
||||
dataMask,
|
||||
editMode,
|
||||
expandedSlices,
|
||||
forceRefresh,
|
||||
hasUnsavedChanges,
|
||||
isDropdownVisible,
|
||||
isLoading,
|
||||
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,
|
||||
setDropdownVisible,
|
||||
shouldPersistRefreshFrequency,
|
||||
showEmbedModal,
|
||||
showPropertiesModal,
|
||||
startPeriodicRender,
|
||||
userCanCurate,
|
||||
userCanEdit,
|
||||
userCanSaveAs,
|
||||
userCanShare,
|
||||
],
|
||||
);
|
||||
|
||||
lastModifiedTime: actualLastModifiedTime,
|
||||
logEvent: boundActionCreators.logEvent,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
css={headerContainerStyle}
|
||||
|
|
@ -788,8 +751,11 @@ const Header = () => {
|
|||
faveStarProps={faveStarProps}
|
||||
titlePanelAdditionalItems={titlePanelAdditionalItems}
|
||||
rightPanelAdditionalItems={rightPanelAdditionalItems}
|
||||
menuDropdownProps={menuDropdownProps}
|
||||
additionalActionsMenu={additionalActionsMenu}
|
||||
menuDropdownProps={{
|
||||
open: isDropdownVisible,
|
||||
onOpenChange: setIsDropdownVisible,
|
||||
}}
|
||||
additionalActionsMenu={menu}
|
||||
showFaveStar={user?.userId && dashboardInfo?.id}
|
||||
showTitlePanelItems
|
||||
/>
|
||||
|
|
@ -806,6 +772,32 @@ const Header = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
<ReportModal
|
||||
userId={user.userId}
|
||||
show={showingReportModal}
|
||||
onHide={hideReportModal}
|
||||
userEmail={user.email}
|
||||
dashboardId={dashboardInfo.id}
|
||||
creationMethod="dashboards"
|
||||
/>
|
||||
|
||||
{currentReportDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
'This action will permanently delete %s.',
|
||||
currentReportDeleting?.name,
|
||||
)}
|
||||
onConfirm={() => {
|
||||
if (currentReportDeleting) {
|
||||
handleReportDelete(currentReportDeleting);
|
||||
}
|
||||
}}
|
||||
onHide={() => setCurrentReportDeleting(null)}
|
||||
open
|
||||
title={t('Delete Report?')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OverwriteConfirm />
|
||||
|
||||
{userCanCurate && (
|
||||
|
|
@ -817,7 +809,7 @@ const Header = () => {
|
|||
)}
|
||||
<Global
|
||||
styles={css`
|
||||
.ant-menu-vertical {
|
||||
.antd5-menu-vertical {
|
||||
border-right: none;
|
||||
}
|
||||
`}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { Layout } from 'src/dashboard/types';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { AlertObject } from 'src/features/alerts/types';
|
||||
|
||||
interface DashboardInfo {
|
||||
id: number;
|
||||
|
|
@ -60,11 +61,11 @@ export interface HeaderDropdownProps {
|
|||
dataMask: any;
|
||||
lastModifiedTime: number;
|
||||
logEvent: () => void;
|
||||
setIsDropdownVisible: (visible: boolean) => void;
|
||||
isDropdownVisible: boolean;
|
||||
refreshLimit: number;
|
||||
refreshWarning: string;
|
||||
directPathToChild: string[];
|
||||
showReportModal: () => void;
|
||||
setCurrentReportDeleting: (alert: AlertObject | null) => void;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
|
|
|
|||
|
|
@ -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,93 +37,13 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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<string, any>) => {
|
||||
switch (key) {
|
||||
case MenuKeys.RefreshDashboard:
|
||||
this.props.forceRefreshAllCharts();
|
||||
this.props.addSuccessToast(t('Refreshing charts'));
|
||||
break;
|
||||
case MenuKeys.EditProperties:
|
||||
this.props.showPropertiesModal();
|
||||
break;
|
||||
case MenuKeys.ToggleFullscreen: {
|
||||
const url = getDashboardUrl({
|
||||
pathname: window.location.pathname,
|
||||
filters: getActiveFilters(),
|
||||
hash: window.location.hash,
|
||||
standalone: getUrlParam(URL_PARAMS.standalone),
|
||||
});
|
||||
window.location.replace(url);
|
||||
break;
|
||||
}
|
||||
case MenuKeys.ManageEmbedded: {
|
||||
this.props.manageEmbedded();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dashboardTitle,
|
||||
export const useHeaderActionsMenu = ({
|
||||
customCss,
|
||||
dashboardId,
|
||||
dashboardInfo,
|
||||
refreshFrequency,
|
||||
shouldPersistRefreshFrequency,
|
||||
editMode,
|
||||
customCss,
|
||||
colorNamespace,
|
||||
colorScheme,
|
||||
layout,
|
||||
|
|
@ -139,56 +59,134 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
lastModifiedTime,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
setIsDropdownVisible,
|
||||
isDropdownVisible,
|
||||
directPathToChild,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const emailTitle = t('Superset dashboard');
|
||||
const emailSubject = `${emailTitle} ${dashboardTitle}`;
|
||||
const emailBody = t('Check out this dashboard: ');
|
||||
|
||||
const isEmbedded = !dashboardInfo?.userId;
|
||||
forceRefreshAllCharts,
|
||||
showPropertiesModal,
|
||||
showReportModal,
|
||||
manageEmbedded,
|
||||
onChange,
|
||||
updateCss,
|
||||
startPeriodicRender,
|
||||
setRefreshFrequency,
|
||||
dashboardTitle,
|
||||
logEvent,
|
||||
setCurrentReportDeleting,
|
||||
}: HeaderDropdownProps) => {
|
||||
const [css, setCss] = useState(customCss || '');
|
||||
const [showReportSubMenu, setShowReportSubMenu] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const directPathToChild = useSelector(
|
||||
(state: RootState) => state.dashboardState.directPathToChild,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (customCss !== css) {
|
||||
setCss(customCss || '');
|
||||
injectCustomCss(customCss);
|
||||
}
|
||||
}, [css, customCss]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
({ key }: { key: string }) => {
|
||||
switch (key) {
|
||||
case MenuKeys.RefreshDashboard:
|
||||
forceRefreshAllCharts();
|
||||
addSuccessToast(t('Refreshing charts'));
|
||||
break;
|
||||
case MenuKeys.EditProperties:
|
||||
showPropertiesModal();
|
||||
break;
|
||||
case MenuKeys.ToggleFullscreen: {
|
||||
const url = getDashboardUrl({
|
||||
pathname: window.location.pathname,
|
||||
filters: getActiveFilters(),
|
||||
hash: window.location.hash,
|
||||
standalone: getUrlParam(URL_PARAMS.standalone),
|
||||
});
|
||||
window.location.replace(url);
|
||||
break;
|
||||
}
|
||||
case MenuKeys.ManageEmbedded:
|
||||
manageEmbedded();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setIsDropdownVisible(false);
|
||||
},
|
||||
[
|
||||
forceRefreshAllCharts,
|
||||
addSuccessToast,
|
||||
showPropertiesModal,
|
||||
manageEmbedded,
|
||||
],
|
||||
);
|
||||
|
||||
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 refreshIntervalOptions =
|
||||
dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS;
|
||||
|
||||
const dashboardComponentId = [...(directPathToChild || [])].pop();
|
||||
|
||||
return (
|
||||
<Menu selectable={false} data-test="header-actions-menu" {...rest}>
|
||||
<Menu
|
||||
selectable={false}
|
||||
data-test="header-actions-menu"
|
||||
onClick={handleMenuClick}
|
||||
>
|
||||
{!editMode && (
|
||||
<Menu.Item
|
||||
key={MenuKeys.RefreshDashboard}
|
||||
data-test="refresh-dashboard-menu-item"
|
||||
disabled={isLoading}
|
||||
onClick={this.handleMenuClick}
|
||||
>
|
||||
{t('Refresh dashboard')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!editMode && !isEmbedded && (
|
||||
<Menu.Item
|
||||
key={MenuKeys.ToggleFullscreen}
|
||||
onClick={this.handleMenuClick}
|
||||
>
|
||||
<Menu.Item key={MenuKeys.ToggleFullscreen}>
|
||||
{getUrlParam(URL_PARAMS.standalone)
|
||||
? t('Exit fullscreen')
|
||||
: t('Enter fullscreen')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{editMode && (
|
||||
<Menu.Item
|
||||
key={MenuKeys.EditProperties}
|
||||
onClick={this.handleMenuClick}
|
||||
>
|
||||
<Menu.Item key={MenuKeys.EditProperties}>
|
||||
{t('Edit properties')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
|
@ -196,8 +194,8 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
<Menu.Item key={MenuKeys.EditCss}>
|
||||
<CssEditor
|
||||
triggerNode={<div>{t('Edit CSS')}</div>}
|
||||
initialCss={this.state.css}
|
||||
onChange={this.changeCss}
|
||||
initialCss={css}
|
||||
onChange={changeCss}
|
||||
addDangerToast={addDangerToast}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
@ -228,29 +226,26 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.SubMenu
|
||||
key={MenuKeys.Download}
|
||||
<DownloadMenuItems
|
||||
submenuKey={MenuKeys.Download}
|
||||
disabled={isLoading}
|
||||
title={t('Download')}
|
||||
>
|
||||
<DownloadMenuItems
|
||||
pdfMenuItemTitle={t('Export to PDF')}
|
||||
imageMenuItemTitle={t('Download as Image')}
|
||||
dashboardTitle={dashboardTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={this.props.logEvent}
|
||||
logEvent={logEvent}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
{userCanShare && (
|
||||
<ShareMenuItems
|
||||
key={MenuKeys.Share}
|
||||
disabled={isLoading}
|
||||
data-test="share-dashboard-menu-item"
|
||||
title={t('Share')}
|
||||
url={url}
|
||||
copyMenuItemTitle={t('Copy permalink to clipboard')}
|
||||
emailMenuItemTitle={t('Share permalink by email')}
|
||||
emailSubject={emailSubject}
|
||||
emailBody={emailBody}
|
||||
emailBody={t('Check out this dashboard: ')}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
dashboardId={dashboardId}
|
||||
|
|
@ -258,39 +253,33 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
/>
|
||||
)}
|
||||
{!editMode && userCanCurate && (
|
||||
<Menu.Item
|
||||
key={MenuKeys.ManageEmbedded}
|
||||
onClick={this.handleMenuClick}
|
||||
>
|
||||
<Menu.Item key={MenuKeys.ManageEmbedded}>
|
||||
{t('Embed dashboard')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
{!editMode ? (
|
||||
this.state.showReportSubMenu ? (
|
||||
showReportSubMenu ? (
|
||||
<>
|
||||
<Menu.SubMenu title={t('Manage email report')}>
|
||||
<HeaderReportDropdown
|
||||
submenuTitle={t('Manage email report')}
|
||||
dashboardId={dashboardInfo.id}
|
||||
setShowReportSubMenu={this.setShowReportSubMenu}
|
||||
showReportSubMenu={this.state.showReportSubMenu}
|
||||
setIsDropdownVisible={setIsDropdownVisible}
|
||||
isDropdownVisible={isDropdownVisible}
|
||||
setShowReportSubMenu={setShowReportSubMenu}
|
||||
showReportModal={showReportModal}
|
||||
showReportSubMenu={showReportSubMenu}
|
||||
setCurrentReportDeleting={setCurrentReportDeleting}
|
||||
useTextMenu
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
) : (
|
||||
<Menu>
|
||||
<HeaderReportDropdown
|
||||
dashboardId={dashboardInfo.id}
|
||||
setShowReportSubMenu={this.setShowReportSubMenu}
|
||||
setIsDropdownVisible={setIsDropdownVisible}
|
||||
isDropdownVisible={isDropdownVisible}
|
||||
setShowReportSubMenu={setShowReportSubMenu}
|
||||
showReportModal={showReportModal}
|
||||
setCurrentReportDeleting={setCurrentReportDeleting}
|
||||
useTextMenu
|
||||
/>
|
||||
</Menu>
|
||||
)
|
||||
) : null}
|
||||
{editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && (
|
||||
|
|
@ -302,14 +291,13 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item key={MenuKeys.AutorefreshModal}>
|
||||
<RefreshIntervalModal
|
||||
addSuccessToast={addSuccessToast}
|
||||
refreshFrequency={refreshFrequency}
|
||||
refreshLimit={refreshLimit}
|
||||
refreshWarning={refreshWarning}
|
||||
onChange={this.changeRefreshInterval}
|
||||
onChange={changeRefreshInterval}
|
||||
editMode={editMode}
|
||||
refreshIntervalOptions={refreshIntervalOptions}
|
||||
triggerNode={<div>{t('Set auto-refresh interval')}</div>}
|
||||
|
|
@ -317,7 +305,18 @@ export class HeaderActionsDropdown extends PureComponent<
|
|||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
css,
|
||||
showReportSubMenu,
|
||||
isDropdownVisible,
|
||||
directPathToChild,
|
||||
handleMenuClick,
|
||||
changeCss,
|
||||
changeRefreshInterval,
|
||||
emailSubject,
|
||||
url,
|
||||
dashboardComponentId,
|
||||
]);
|
||||
|
||||
export default connect(mapStateToProps)(HeaderActionsDropdown);
|
||||
return [menu, isDropdownVisible, setIsDropdownVisible];
|
||||
};
|
||||
|
|
@ -22,16 +22,16 @@ import userEvent from '@testing-library/user-event';
|
|||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
|
||||
import { HeaderActionsDropdown } from 'src/dashboard/components/Header/HeaderActionsDropdown';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { useHeaderActionsMenu } from './Header/useHeaderActionsDropdownMenu';
|
||||
|
||||
const createProps = () => ({
|
||||
addSuccessToast: jest.fn(),
|
||||
addDangerToast: jest.fn(),
|
||||
customCss:
|
||||
'.header-with-actions .right-button-panel .ant-dropdown-trigger{margin-left: 100px;}',
|
||||
'.header-with-actions .right-button-panel .antd5-dropdown-trigger{margin-left: 100px;}',
|
||||
dashboardId: 1,
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
|
|
@ -85,12 +85,22 @@ const editModeOnProps = {
|
|||
};
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
const store = mockStore({
|
||||
dashboardState: {
|
||||
dashboardInfo: createProps().dashboardInfo,
|
||||
},
|
||||
});
|
||||
|
||||
const HeaderActionsMenu = (props: any) => {
|
||||
const [menu] = useHeaderActionsMenu(props);
|
||||
|
||||
return <>{menu}</>;
|
||||
};
|
||||
|
||||
const setup = (overrides?: any) => (
|
||||
<Provider store={store}>
|
||||
<div className="dashboard-header">
|
||||
<HeaderActionsDropdown {...editModeOnProps} {...overrides} />
|
||||
<HeaderActionsMenu {...editModeOnProps} {...overrides} />
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
|||
import { MenuKeys, RootState } from 'src/dashboard/types';
|
||||
import DrillDetailModal from 'src/components/Chart/DrillDetail/DrillDetailModal';
|
||||
import { usePermissions } from 'src/hooks/usePermissions';
|
||||
import Button from 'src/components/Button';
|
||||
import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
|
||||
import { ViewResultsModalTrigger } from './ViewResultsModalTrigger';
|
||||
|
||||
|
|
@ -158,9 +159,8 @@ const SliceHeaderControls = (
|
|||
props: SliceHeaderControlsPropsWithRouter | SliceHeaderControlsProps,
|
||||
) => {
|
||||
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
// setting openKeys undefined falls back to uncontrolled behaviour
|
||||
const [openKeys, setOpenKeys] = useState<string[] | undefined>(undefined);
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal(
|
||||
props.slice.slice_id,
|
||||
);
|
||||
|
|
@ -241,7 +241,7 @@ const SliceHeaderControls = (
|
|||
// menu closes with a delay, we need to hide it manually,
|
||||
// so that we don't capture it on the screenshot
|
||||
const menu = document.querySelector(
|
||||
'.ant-dropdown:not(.ant-dropdown-hidden)',
|
||||
'.antd5-dropdown:not(.antd5-dropdown-hidden)',
|
||||
) as HTMLElement;
|
||||
if (menu) {
|
||||
menu.style.visibility = 'hidden';
|
||||
|
|
@ -284,6 +284,7 @@ const SliceHeaderControls = (
|
|||
default:
|
||||
break;
|
||||
}
|
||||
setIsDropdownVisible(false);
|
||||
};
|
||||
|
||||
const {
|
||||
|
|
@ -334,24 +335,12 @@ const SliceHeaderControls = (
|
|||
animationDuration: '0s',
|
||||
};
|
||||
|
||||
// controlled/uncontrolled behaviour for submenus
|
||||
const openKeysProps: Record<string, string[]> = {};
|
||||
if (openKeys) {
|
||||
openKeysProps.openKeys = openKeys;
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
selectable={false}
|
||||
data-test={`slice_${slice.slice_id}-menu`}
|
||||
selectedKeys={selectedKeys}
|
||||
onSelect={({ selectedKeys: keys }) => setSelectedKeys(keys)}
|
||||
openKeys={openKeys}
|
||||
id={`slice_${slice.slice_id}-menu`}
|
||||
// submenus must be rendered for handleDropdownNavigation
|
||||
forceSubMenuRender
|
||||
{...openKeysProps}
|
||||
selectable={false}
|
||||
>
|
||||
<Menu.Item
|
||||
key={MenuKeys.ForceRefresh}
|
||||
|
|
@ -458,9 +447,7 @@ const SliceHeaderControls = (
|
|||
emailBody={t('Check out this chart: ')}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
setOpenKeys={setOpenKeys}
|
||||
title={t('Share')}
|
||||
key={MenuKeys.Share}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -532,22 +519,17 @@ const SliceHeaderControls = (
|
|||
overlayStyle={dropdownOverlayStyle}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
autoFocus
|
||||
forceRender
|
||||
open={isDropdownVisible}
|
||||
onOpenChange={visible => setIsDropdownVisible(visible)}
|
||||
>
|
||||
<span
|
||||
css={() => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
<Button
|
||||
type="link"
|
||||
id={`slice_${slice.slice_id}-controls`}
|
||||
role="button"
|
||||
aria-label="More Options"
|
||||
aria-haspopup="true"
|
||||
tabIndex={0}
|
||||
>
|
||||
<VerticalDotsTrigger />
|
||||
</span>
|
||||
</Button>
|
||||
</NoAnimationDropdown>
|
||||
<DrillDetailModal
|
||||
formData={props.formData}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ test('Should call download image on click', async () => {
|
|||
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
|
||||
userEvent.click(screen.getByRole('menuitem', { name: 'Download as Image' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(downloadAsImage).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -68,8 +68,8 @@ test('Should call download image on click', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('Component is rendered with role="button"', async () => {
|
||||
test('Component is rendered with role="menuitem"', async () => {
|
||||
renderComponent();
|
||||
const button = screen.getByRole('button', { name: 'Download as Image' });
|
||||
const button = screen.getByRole('menuitem', { name: 'Download as Image' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export default function DownloadAsImage({
|
|||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
...rest
|
||||
}: {
|
||||
text: string;
|
||||
dashboardTitle: string;
|
||||
|
|
@ -46,10 +45,13 @@ export default function DownloadAsImage({
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu.Item key="download-image" {...rest}>
|
||||
<div onClick={onDownloadImage} role="button" tabIndex={0}>
|
||||
<Menu.Item
|
||||
key="download-image"
|
||||
onClick={e => {
|
||||
onDownloadImage(e.domEvent);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export default function DownloadAsPdf({
|
|||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
...rest
|
||||
}: {
|
||||
text: string;
|
||||
dashboardTitle: string;
|
||||
|
|
@ -46,10 +45,13 @@ export default function DownloadAsPdf({
|
|||
};
|
||||
|
||||
return (
|
||||
<Menu.Item key="download-pdf" {...rest}>
|
||||
<div onClick={onDownloadPdf} role="button" tabIndex={0}>
|
||||
<Menu.Item
|
||||
key="download-pdf"
|
||||
onClick={e => {
|
||||
onDownloadPdf(e.domEvent);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@ const createProps = () => ({
|
|||
dashboardTitle: 'Test Dashboard',
|
||||
logEvent: jest.fn(),
|
||||
dashboardId: 123,
|
||||
title: 'Download',
|
||||
submenuKey: 'download',
|
||||
});
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<Menu>
|
||||
<Menu forceSubMenuRender>
|
||||
<DownloadMenuItems {...createProps()} />
|
||||
</Menu>,
|
||||
{
|
||||
|
|
@ -41,10 +43,6 @@ const renderComponent = () => {
|
|||
|
||||
test('Should render menu items', () => {
|
||||
renderComponent();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Export to PDF' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Download as Image' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Export to PDF')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download as Image')).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,17 +17,23 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import DownloadScreenshot from './DownloadScreenshot';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { useDownloadScreenshot } from 'src/dashboard/hooks/useDownloadScreenshot';
|
||||
import { ComponentProps } from 'react';
|
||||
import { DownloadScreenshotFormat } from './types';
|
||||
import DownloadAsPdf from './DownloadAsPdf';
|
||||
import DownloadAsImage from './DownloadAsImage';
|
||||
|
||||
export interface DownloadMenuItemProps {
|
||||
export interface DownloadMenuItemProps
|
||||
extends ComponentProps<typeof Menu.SubMenu> {
|
||||
pdfMenuItemTitle: string;
|
||||
imageMenuItemTitle: string;
|
||||
dashboardTitle: string;
|
||||
logEvent?: Function;
|
||||
dashboardId: number;
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
submenuKey: string;
|
||||
}
|
||||
|
||||
const DownloadMenuItems = (props: DownloadMenuItemProps) => {
|
||||
|
|
@ -37,44 +43,45 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => {
|
|||
logEvent,
|
||||
dashboardId,
|
||||
dashboardTitle,
|
||||
submenuKey,
|
||||
disabled,
|
||||
title,
|
||||
...rest
|
||||
} = props;
|
||||
const isWebDriverScreenshotEnabled =
|
||||
isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) &&
|
||||
isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot);
|
||||
|
||||
const downloadScreenshot = useDownloadScreenshot(dashboardId, logEvent);
|
||||
|
||||
return isWebDriverScreenshotEnabled ? (
|
||||
<>
|
||||
<DownloadScreenshot
|
||||
text={pdfMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PDF}
|
||||
{...rest}
|
||||
/>
|
||||
<DownloadScreenshot
|
||||
text={imageMenuItemTitle}
|
||||
dashboardId={dashboardId}
|
||||
logEvent={logEvent}
|
||||
format={DownloadScreenshotFormat.PNG}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
<Menu.SubMenu key={submenuKey} title={title} disabled={disabled} {...rest}>
|
||||
<Menu.Item
|
||||
key={DownloadScreenshotFormat.PDF}
|
||||
onClick={() => downloadScreenshot(DownloadScreenshotFormat.PDF)}
|
||||
>
|
||||
{pdfMenuItemTitle}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={DownloadScreenshotFormat.PNG}
|
||||
onClick={() => downloadScreenshot(DownloadScreenshotFormat.PNG)}
|
||||
>
|
||||
{imageMenuItemTitle}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
) : (
|
||||
<>
|
||||
<Menu.SubMenu key={submenuKey} title={title} disabled={disabled} {...rest}>
|
||||
<DownloadAsPdf
|
||||
text={pdfMenuItemTitle}
|
||||
dashboardTitle={dashboardTitle}
|
||||
logEvent={logEvent}
|
||||
{...rest}
|
||||
/>
|
||||
<DownloadAsImage
|
||||
text={imageMenuItemTitle}
|
||||
dashboardTitle={dashboardTitle}
|
||||
logEvent={logEvent}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const createProps = () => ({
|
|||
emailBody: 'Check out this dashboard: ',
|
||||
dashboardId: DASHBOARD_ID,
|
||||
title: 'Test Dashboard',
|
||||
submenuKey: 'share',
|
||||
});
|
||||
|
||||
const { location } = window;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { RefObject } from 'react';
|
||||
import { ComponentProps, RefObject } from 'react';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import { t, logging } from '@superset-ui/core';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
|
|
@ -24,7 +24,7 @@ import { getDashboardPermalink } from 'src/utils/urlUtils';
|
|||
import { MenuKeys, RootState } from 'src/dashboard/types';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
interface ShareMenuItemProps {
|
||||
interface ShareMenuItemProps extends ComponentProps<typeof Menu.SubMenu> {
|
||||
url?: string;
|
||||
copyMenuItemTitle: string;
|
||||
emailMenuItemTitle: string;
|
||||
|
|
@ -38,8 +38,8 @@ interface ShareMenuItemProps {
|
|||
shareByEmailMenuItemRef?: RefObject<any>;
|
||||
selectedKeys?: string[];
|
||||
setOpenKeys?: Function;
|
||||
key?: string;
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ShareMenuItems = (props: ShareMenuItemProps) => {
|
||||
|
|
@ -52,8 +52,9 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
|
|||
addSuccessToast,
|
||||
dashboardId,
|
||||
dashboardComponentId,
|
||||
key,
|
||||
title,
|
||||
disabled,
|
||||
...rest
|
||||
} = props;
|
||||
const { dataMask, activeTabs } = useSelector(
|
||||
(state: RootState) => ({
|
||||
|
|
@ -96,7 +97,12 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Menu.SubMenu title={title} key={key}>
|
||||
<Menu.SubMenu
|
||||
title={title}
|
||||
key={MenuKeys.Share}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
<Menu.Item key={MenuKeys.CopyLink} onClick={() => onCopyLink()}>
|
||||
{copyMenuItemTitle}
|
||||
</Menu.Item>
|
||||
|
|
|
|||
|
|
@ -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,24 +203,19 @@ test('On selection change, send request and update checked value', async () => {
|
|||
}),
|
||||
),
|
||||
);
|
||||
await waitFor(() => {
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
expect(menuitems.length).toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
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
|
||||
const updatedHorizontalItem = screen.getByText('Horizontal (Top)');
|
||||
expect(
|
||||
await within(screen.getAllByRole('menuitem')[5]).findByLabelText('check'),
|
||||
within(updatedHorizontalItem.closest('li')!).getByLabelText('check'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(screen.getAllByRole('menuitem')[4]).queryByLabelText('check'),
|
||||
within(verticalItem.closest('li')!).queryByLabelText('check'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('On failed request, restore previous selection', async () => {
|
||||
// @ts-ignore
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ import {
|
|||
saveCrossFiltersSetting,
|
||||
} from 'src/dashboard/actions/dashboardInfo';
|
||||
import Icons from 'src/components/Icons';
|
||||
import DropdownSelectableIcon, {
|
||||
DropDownSelectableProps,
|
||||
} from 'src/components/DropdownSelectableIcon';
|
||||
import Checkbox from 'src/components/Checkbox';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Button } from 'src/components';
|
||||
import { Space } from 'src/components/Space';
|
||||
import { clearDataMaskState } from 'src/dataMask/actions';
|
||||
import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
|
||||
import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal';
|
||||
import { useCrossFiltersScopingModal } from '../CrossFilters/ScopingModal/useCrossFiltersScopingModal';
|
||||
import FilterConfigurationLink from '../FilterConfigurationLink';
|
||||
|
||||
|
|
@ -100,6 +101,12 @@ const FilterBarSettings = () => {
|
|||
|
||||
const [openScopingModal, scopingModal] = useCrossFiltersScopingModal();
|
||||
|
||||
const { openFilterConfigModal, FilterConfigModalComponent } =
|
||||
useFilterConfigModal({
|
||||
createNewOnOpen: filterValues.length === 0,
|
||||
dashboardId,
|
||||
});
|
||||
|
||||
const updateCrossFiltersSetting = useCallback(
|
||||
async isEnabled => {
|
||||
if (!isEnabled) {
|
||||
|
|
@ -133,7 +140,7 @@ const FilterBarSettings = () => {
|
|||
[dispatch, filterBarOrientation],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
const handleClick = useCallback(
|
||||
(
|
||||
selection: Parameters<
|
||||
Required<Pick<MenuProps, 'onSelect'>>['onSelect']
|
||||
|
|
@ -146,9 +153,16 @@ const FilterBarSettings = () => {
|
|||
toggleFilterBarOrientation(selectedKey);
|
||||
} else if (selectedKey === CROSS_FILTERS_SCOPING_MENU_KEY) {
|
||||
openScopingModal();
|
||||
} else if (selectedKey === ADD_EDIT_FILTERS_MENU_KEY) {
|
||||
openFilterConfigModal();
|
||||
}
|
||||
},
|
||||
[openScopingModal, toggleCrossFiltering, toggleFilterBarOrientation],
|
||||
[
|
||||
openScopingModal,
|
||||
toggleCrossFiltering,
|
||||
toggleFilterBarOrientation,
|
||||
openFilterConfigModal,
|
||||
],
|
||||
);
|
||||
|
||||
const crossFiltersMenuItem = useMemo(
|
||||
|
|
@ -168,21 +182,20 @@ const FilterBarSettings = () => {
|
|||
);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const items: DropDownSelectableProps['menuItems'] = [];
|
||||
const items: MenuProps['items'] = [];
|
||||
|
||||
if (canEdit) {
|
||||
items.push({
|
||||
key: ADD_EDIT_FILTERS_MENU_KEY,
|
||||
label: (
|
||||
<FilterConfigurationLink
|
||||
dashboardId={dashboardId}
|
||||
createNewOnOpen={filterValues.length === 0}
|
||||
>
|
||||
<FilterConfigurationLink>
|
||||
{t('Add or edit filters')}
|
||||
</FilterConfigurationLink>
|
||||
),
|
||||
divider: canSetHorizontalFilterBar,
|
||||
});
|
||||
if (canSetHorizontalFilterBar) {
|
||||
items.push({ type: 'divider' });
|
||||
}
|
||||
}
|
||||
if (canEdit) {
|
||||
items.push({
|
||||
|
|
@ -192,8 +205,10 @@ const FilterBarSettings = () => {
|
|||
items.push({
|
||||
key: CROSS_FILTERS_SCOPING_MENU_KEY,
|
||||
label: t('Cross-filtering scoping'),
|
||||
divider: canSetHorizontalFilterBar,
|
||||
});
|
||||
if (canSetHorizontalFilterBar) {
|
||||
items.push({ type: 'divider' });
|
||||
}
|
||||
}
|
||||
if (canSetHorizontalFilterBar) {
|
||||
items.push({
|
||||
|
|
@ -202,17 +217,31 @@ const FilterBarSettings = () => {
|
|||
children: [
|
||||
{
|
||||
key: FilterBarOrientation.Vertical,
|
||||
label: t('Vertical (Left)'),
|
||||
label: (
|
||||
<Space>
|
||||
{t('Vertical (Left)')}
|
||||
{selectedFilterBarOrientation ===
|
||||
FilterBarOrientation.Vertical && <Icons.Check />}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: FilterBarOrientation.Horizontal,
|
||||
label: t('Horizontal (Top)'),
|
||||
label: (
|
||||
<Space>
|
||||
{t('Horizontal (Top)')}
|
||||
{selectedFilterBarOrientation ===
|
||||
FilterBarOrientation.Horizontal && <Icons.Check />}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
...{ 'data-test': 'dropdown-selectable-icon-submenu' },
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [
|
||||
selectedFilterBarOrientation,
|
||||
canEdit,
|
||||
canSetHorizontalFilterBar,
|
||||
crossFiltersMenuItem,
|
||||
|
|
@ -226,19 +255,24 @@ const FilterBarSettings = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<DropdownSelectableIcon
|
||||
onSelect={handleSelect}
|
||||
icon={
|
||||
<Dropdown
|
||||
menu={{
|
||||
onClick: handleClick,
|
||||
items: menuItems,
|
||||
selectedKeys: [selectedFilterBarOrientation],
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button type="link">
|
||||
<Icons.Gear
|
||||
name="gear"
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
data-test="filterbar-orientation-icon"
|
||||
/>
|
||||
}
|
||||
menuItems={menuItems}
|
||||
selectedKeys={[selectedFilterBarOrientation]}
|
||||
/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{scopingModal}
|
||||
{FilterConfigModalComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,11 +38,16 @@ test('should render the config link text', () => {
|
|||
});
|
||||
|
||||
test('should render the modal on click', () => {
|
||||
render(<FilterConfigurationLink>Config link</FilterConfigurationLink>, {
|
||||
const showModal = jest.fn();
|
||||
render(
|
||||
<FilterConfigurationLink onClick={showModal}>
|
||||
Config link
|
||||
</FilterConfigurationLink>,
|
||||
{
|
||||
useRedux: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
const configLink = screen.getByText('Config link');
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
userEvent.click(configLink);
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(showModal).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,70 +16,27 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, FC, useCallback, useState, memo } from 'react';
|
||||
import { ReactNode, FC, memo } from 'react';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
|
||||
import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
|
||||
import { getFilterBarTestId } from '../utils';
|
||||
import { SaveFilterChangesType } from '../../FiltersConfigModal/types';
|
||||
|
||||
export interface FCBProps {
|
||||
createNewOnOpen?: boolean;
|
||||
dashboardId?: number;
|
||||
initialFilterId?: string;
|
||||
onClick?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const FilterConfigurationLink: FC<FCBProps> = ({
|
||||
createNewOnOpen,
|
||||
dashboardId,
|
||||
initialFilterId,
|
||||
onClick,
|
||||
children,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const close = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const submit = useCallback(
|
||||
async (filterChanges: SaveFilterChangesType) => {
|
||||
dispatch(await setFilterConfiguration(filterChanges));
|
||||
close();
|
||||
},
|
||||
[dispatch, close],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpen(true);
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
}, [setOpen, onClick]);
|
||||
|
||||
return (
|
||||
<>
|
||||
}) => (
|
||||
<div
|
||||
{...getFilterBarTestId('create-filter')}
|
||||
onClick={handleClick}
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<FiltersConfigModal
|
||||
isOpen={isOpen}
|
||||
onSave={submit}
|
||||
onCancel={close}
|
||||
initialFilterId={initialFilterId}
|
||||
createNewOnOpen={createNewOnOpen}
|
||||
key={`filters-for-${dashboardId}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FilterConfigurationLink);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
|
||||
import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
|
||||
import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
|
||||
|
||||
interface UseFilterConfigModalProps {
|
||||
createNewOnOpen?: boolean;
|
||||
dashboardId: number;
|
||||
initialFilterId?: string;
|
||||
}
|
||||
|
||||
interface UseFilterConfigModalReturn {
|
||||
isFilterConfigModalOpen: boolean;
|
||||
openFilterConfigModal: () => void;
|
||||
closeFilterConfigModal: () => void;
|
||||
handleFilterSave: (filterChanges: SaveFilterChangesType) => Promise<void>;
|
||||
FilterConfigModalComponent: JSX.Element | null;
|
||||
}
|
||||
|
||||
export const useFilterConfigModal = ({
|
||||
createNewOnOpen = false,
|
||||
dashboardId,
|
||||
initialFilterId,
|
||||
}: UseFilterConfigModalProps): UseFilterConfigModalReturn => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFilterConfigModalOpen, setIsFilterConfigModalOpen] = useState(false);
|
||||
|
||||
const openFilterConfigModal = useCallback(() => {
|
||||
setIsFilterConfigModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeFilterConfigModal = useCallback(() => {
|
||||
setIsFilterConfigModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFilterSave = useCallback(
|
||||
async (filterChanges: SaveFilterChangesType) => {
|
||||
dispatch(await setFilterConfiguration(filterChanges));
|
||||
closeFilterConfigModal();
|
||||
},
|
||||
[dispatch, closeFilterConfigModal],
|
||||
);
|
||||
|
||||
const FilterConfigModalComponent = isFilterConfigModalOpen ? (
|
||||
<FiltersConfigModal
|
||||
isOpen={isFilterConfigModalOpen}
|
||||
onSave={handleFilterSave}
|
||||
onCancel={closeFilterConfigModal}
|
||||
key={`filters-for-${dashboardId}`}
|
||||
createNewOnOpen={createNewOnOpen}
|
||||
initialFilterId={initialFilterId}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return {
|
||||
isFilterConfigModalOpen,
|
||||
openFilterConfigModal,
|
||||
closeFilterConfigModal,
|
||||
handleFilterSave,
|
||||
FilterConfigModalComponent,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { css, SupersetTheme, useTheme, useTruncation } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { Row, FilterName, InternalRow } from './Styles';
|
||||
import { FilterCardRowProps } from './types';
|
||||
|
|
@ -39,6 +40,12 @@ export const NameRow = ({
|
|||
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
||||
);
|
||||
|
||||
const { FilterConfigModalComponent, openFilterConfigModal } =
|
||||
useFilterConfigModal({
|
||||
dashboardId,
|
||||
initialFilterId: filter.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Row
|
||||
css={(theme: SupersetTheme) => css`
|
||||
|
|
@ -58,9 +65,10 @@ export const NameRow = ({
|
|||
</InternalRow>
|
||||
{canEdit && (
|
||||
<FilterConfigurationLink
|
||||
dashboardId={dashboardId}
|
||||
onClick={hidePopover}
|
||||
initialFilterId={filter.id}
|
||||
onClick={() => {
|
||||
openFilterConfigModal();
|
||||
hidePopover();
|
||||
}}
|
||||
>
|
||||
<Icons.Edit
|
||||
iconSize="l"
|
||||
|
|
@ -71,6 +79,7 @@ export const NameRow = ({
|
|||
/>
|
||||
</FilterConfigurationLink>
|
||||
)}
|
||||
{FilterConfigModalComponent}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { last } from 'lodash';
|
||||
import {
|
||||
logging,
|
||||
t,
|
||||
SupersetClient,
|
||||
SupersetApiError,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE,
|
||||
LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
|
||||
} from 'src/logger/LogUtils';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { getDashboardUrlParams } from 'src/utils/urlUtils';
|
||||
import { DownloadScreenshotFormat } from '../components/menu/DownloadMenuItems/types';
|
||||
|
||||
const RETRY_INTERVAL = 3000;
|
||||
const MAX_RETRIES = 30;
|
||||
|
||||
export const useDownloadScreenshot = (
|
||||
dashboardId: number,
|
||||
logEvent?: Function,
|
||||
) => {
|
||||
const activeTabs = useSelector(
|
||||
(state: RootState) => state.dashboardState.activeTabs || undefined,
|
||||
);
|
||||
const anchor = useSelector(
|
||||
(state: RootState) =>
|
||||
last(state.dashboardState.directPathToChild) || undefined,
|
||||
);
|
||||
const dataMask = useSelector(
|
||||
(state: RootState) => state.dataMask || undefined,
|
||||
);
|
||||
|
||||
const { addDangerToast, addSuccessToast, addInfoToast } = useToasts();
|
||||
|
||||
const currentIntervalIds = useRef<NodeJS.Timeout[]>([]);
|
||||
|
||||
const stopIntervals = useCallback(
|
||||
(message?: 'success' | 'failure') => {
|
||||
currentIntervalIds.current.forEach(clearInterval);
|
||||
|
||||
if (message === 'failure') {
|
||||
addDangerToast(
|
||||
t('The screenshot could not be downloaded. Please, try again later.'),
|
||||
);
|
||||
}
|
||||
if (message === 'success') {
|
||||
addSuccessToast(t('The screenshot has been downloaded.'));
|
||||
}
|
||||
},
|
||||
[addDangerToast, addSuccessToast],
|
||||
);
|
||||
|
||||
const downloadScreenshot = useCallback(
|
||||
(format: DownloadScreenshotFormat) => {
|
||||
let retries = 0;
|
||||
|
||||
const toastIntervalId = setInterval(
|
||||
() =>
|
||||
addInfoToast(
|
||||
t(
|
||||
'The screenshot is being generated. Please, do not leave the page.',
|
||||
),
|
||||
{ noDuplicate: true },
|
||||
),
|
||||
RETRY_INTERVAL,
|
||||
);
|
||||
|
||||
currentIntervalIds.current = [
|
||||
...(currentIntervalIds.current || []),
|
||||
toastIntervalId,
|
||||
];
|
||||
|
||||
const checkImageReady = (cacheKey: string) =>
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/screenshot/${cacheKey}/?download_format=${format}`,
|
||||
headers: { Accept: 'application/pdf, image/png' },
|
||||
parseMethod: 'raw',
|
||||
})
|
||||
.then((response: Response) => response.blob())
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `screenshot.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
stopIntervals('success');
|
||||
})
|
||||
.catch(err => {
|
||||
if ((err as SupersetApiError).status === 404) {
|
||||
throw new Error('Image not ready');
|
||||
}
|
||||
});
|
||||
|
||||
const fetchImageWithRetry = (cacheKey: string) => {
|
||||
if (retries >= MAX_RETRIES) {
|
||||
stopIntervals('failure');
|
||||
logging.error('Max retries reached');
|
||||
return;
|
||||
}
|
||||
checkImageReady(cacheKey).catch(() => {
|
||||
retries += 1;
|
||||
});
|
||||
};
|
||||
|
||||
SupersetClient.post({
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/cache_dashboard_screenshot/`,
|
||||
jsonPayload: {
|
||||
anchor,
|
||||
activeTabs,
|
||||
dataMask,
|
||||
urlParams: getDashboardUrlParams(['edit']),
|
||||
},
|
||||
})
|
||||
.then(({ json }) => {
|
||||
const cacheKey = json?.cache_key;
|
||||
if (!cacheKey) {
|
||||
throw new Error('No image URL in response');
|
||||
}
|
||||
const retryIntervalId = setInterval(() => {
|
||||
fetchImageWithRetry(cacheKey);
|
||||
}, RETRY_INTERVAL);
|
||||
currentIntervalIds.current.push(retryIntervalId);
|
||||
fetchImageWithRetry(cacheKey);
|
||||
})
|
||||
.catch(error => {
|
||||
logging.error(error);
|
||||
stopIntervals('failure');
|
||||
})
|
||||
.finally(() => {
|
||||
logEvent?.(
|
||||
format === DownloadScreenshotFormat.PNG
|
||||
? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE
|
||||
: LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
dashboardId,
|
||||
anchor,
|
||||
activeTabs,
|
||||
dataMask,
|
||||
addInfoToast,
|
||||
stopIntervals,
|
||||
logEvent,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (currentIntervalIds.current.length > 0) {
|
||||
stopIntervals();
|
||||
}
|
||||
currentIntervalIds.current = [];
|
||||
},
|
||||
[stopIntervals],
|
||||
);
|
||||
|
||||
return downloadScreenshot;
|
||||
};
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
|||
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
|
||||
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
|
||||
import { applyColors, resetColors } from 'src/utils/colorScheme';
|
||||
import ReportModal from 'src/features/reports/ReportModal';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import { deleteActiveReport } from 'src/features/reports/ReportModal/actions';
|
||||
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
|
||||
import { useExploreMetadataBar } from './useExploreMetadataBar';
|
||||
|
||||
|
|
@ -86,6 +89,8 @@ export const ExploreChartHeader = ({
|
|||
const dispatch = useDispatch();
|
||||
const { latestQueryFormData, sliceFormData } = chart;
|
||||
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [currentReportDeleting, setCurrentReportDeleting] = useState(null);
|
||||
const updateCategoricalNamespace = async () => {
|
||||
const { dashboards } = metadata || {};
|
||||
const dashboard =
|
||||
|
|
@ -128,6 +133,14 @@ export const ExploreChartHeader = ({
|
|||
setIsPropertiesModalOpen(false);
|
||||
};
|
||||
|
||||
const showReportModal = () => {
|
||||
setIsReportModalOpen(true);
|
||||
};
|
||||
|
||||
const closeReportModal = () => {
|
||||
setIsReportModalOpen(false);
|
||||
};
|
||||
|
||||
const showModal = useCallback(() => {
|
||||
dispatch(setSaveChartModalVisibility(true));
|
||||
}, [dispatch]);
|
||||
|
|
@ -139,6 +152,11 @@ export const ExploreChartHeader = ({
|
|||
[dispatch],
|
||||
);
|
||||
|
||||
const handleReportDelete = async report => {
|
||||
await dispatch(deleteActiveReport(report));
|
||||
setCurrentReportDeleting(null);
|
||||
};
|
||||
|
||||
const history = useHistory();
|
||||
const { redirectSQLLab } = actions;
|
||||
|
||||
|
|
@ -158,6 +176,8 @@ export const ExploreChartHeader = ({
|
|||
openPropertiesModal,
|
||||
ownState,
|
||||
metadata?.dashboards,
|
||||
showReportModal,
|
||||
setCurrentReportDeleting,
|
||||
);
|
||||
|
||||
const metadataBar = useExploreMetadataBar(metadata, slice);
|
||||
|
|
@ -229,8 +249,8 @@ export const ExploreChartHeader = ({
|
|||
}
|
||||
additionalActionsMenu={menu}
|
||||
menuDropdownProps={{
|
||||
visible: isDropdownVisible,
|
||||
onVisibleChange: setIsDropdownVisible,
|
||||
open: isDropdownVisible,
|
||||
onOpenChange: setIsDropdownVisible,
|
||||
}}
|
||||
/>
|
||||
{isPropertiesModalOpen && (
|
||||
|
|
@ -241,6 +261,33 @@ export const ExploreChartHeader = ({
|
|||
slice={slice}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ReportModal
|
||||
userId={user.userId}
|
||||
show={isReportModalOpen}
|
||||
onHide={closeReportModal}
|
||||
userEmail={user.email}
|
||||
dashboardId={dashboardId}
|
||||
chart={chart}
|
||||
creationMethod="charts"
|
||||
/>
|
||||
|
||||
{currentReportDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
'This action will permanently delete %s.',
|
||||
currentReportDeleting?.name,
|
||||
)}
|
||||
onConfirm={() => {
|
||||
if (currentReportDeleting) {
|
||||
handleReportDelete(currentReportDeleting);
|
||||
}
|
||||
}}
|
||||
onHide={() => setCurrentReportDeleting(null)}
|
||||
open
|
||||
title={t('Delete Report?')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { ReactChild, useCallback, Key } from 'react';
|
|||
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
|
||||
enum MenuKeys {
|
||||
|
|
@ -67,9 +67,9 @@ export const ExportToCSVDropdown = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<AntdDropdown
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
dropdownRender={() => (
|
||||
<Menu onClick={handleMenuClick} selectable={false}>
|
||||
<Menu.Item key={MenuKeys.ExportOriginal}>
|
||||
<MenuItemContent>
|
||||
|
|
@ -84,9 +84,9 @@ export const ExportToCSVDropdown = ({
|
|||
</MenuItemContent>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -107,6 +107,12 @@ describe('DatasourceControl', () => {
|
|||
expect(screen.queryAllByRole('menuitem')).toHaveLength(3);
|
||||
});
|
||||
|
||||
// Close the menu
|
||||
userEvent.click(document.body);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole('menuitem')).toHaveLength(0);
|
||||
});
|
||||
|
||||
rerender(<DatasourceControl {...{ ...props, isEditable: false }} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
} from '@superset-ui/core';
|
||||
import { getTemporalColumns } from '@superset-ui/chart-controls';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
|
@ -82,12 +82,8 @@ const Styles = styled.div`
|
|||
.error-alert {
|
||||
margin: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||
}
|
||||
.ant-dropdown-trigger {
|
||||
.antd5-dropdown-trigger {
|
||||
margin-left: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||
box-shadow: none;
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.btn-group .open .dropdown-toggle {
|
||||
box-shadow: none;
|
||||
|
|
@ -410,8 +406,8 @@ class DatasourceControl extends PureComponent {
|
|||
{extra?.warning_markdown && (
|
||||
<WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
|
||||
)}
|
||||
<AntdDropdown
|
||||
overlay={
|
||||
<Dropdown
|
||||
dropdownRender={() =>
|
||||
datasource.type === DatasourceType.Query
|
||||
? queryDatasourceMenu
|
||||
: defaultDatasourceMenu
|
||||
|
|
@ -423,7 +419,7 @@ class DatasourceControl extends PureComponent {
|
|||
className="datasource-modal-trigger"
|
||||
data-test="datasource-menu-trigger"
|
||||
/>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{/* missing dataset */}
|
||||
{isMissingDatasource && isMissingParams && (
|
||||
|
|
|
|||
|
|
@ -108,13 +108,6 @@ export const MenuTrigger = styled(Button)`
|
|||
`}
|
||||
`;
|
||||
|
||||
const iconReset = css`
|
||||
.ant-dropdown-menu-item > & > .anticon:first-child {
|
||||
margin-right: 0;
|
||||
vertical-align: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const useExploreAdditionalActionsMenu = (
|
||||
latestQueryFormData,
|
||||
canDownloadCSV,
|
||||
|
|
@ -123,6 +116,8 @@ export const useExploreAdditionalActionsMenu = (
|
|||
onOpenPropertiesModal,
|
||||
ownState,
|
||||
dashboards,
|
||||
showReportModal,
|
||||
setCurrentReportDeleting,
|
||||
...rest
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -330,14 +325,14 @@ export const useExploreAdditionalActionsMenu = (
|
|||
<>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV}
|
||||
icon={<Icons.FileOutlined css={iconReset} />}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to original .CSV')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
|
||||
icon={<Icons.FileOutlined css={iconReset} />}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to pivoted .CSV')}
|
||||
|
|
@ -346,7 +341,7 @@ export const useExploreAdditionalActionsMenu = (
|
|||
) : (
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV}
|
||||
icon={<Icons.FileOutlined css={iconReset} />}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to .CSV')}
|
||||
|
|
@ -354,20 +349,20 @@ export const useExploreAdditionalActionsMenu = (
|
|||
)}
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_JSON}
|
||||
icon={<Icons.FileOutlined css={iconReset} />}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to .JSON')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
|
||||
icon={<Icons.FileImageOutlined css={iconReset} />}
|
||||
icon={<Icons.FileImageOutlined />}
|
||||
>
|
||||
{t('Download as image')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_XLSX}
|
||||
icon={<Icons.FileOutlined css={iconReset} />}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to Excel')}
|
||||
|
|
@ -403,28 +398,25 @@ export const useExploreAdditionalActionsMenu = (
|
|||
<Menu.Divider />
|
||||
{showReportSubMenu ? (
|
||||
<>
|
||||
<Menu.SubMenu title={t('Manage email report')}>
|
||||
<HeaderReportDropDown
|
||||
submenuTitle={t('Manage email report')}
|
||||
chart={chart}
|
||||
setShowReportSubMenu={setShowReportSubMenu}
|
||||
showReportSubMenu={showReportSubMenu}
|
||||
setIsDropdownVisible={setIsDropdownVisible}
|
||||
isDropdownVisible={isDropdownVisible}
|
||||
showReportModal={showReportModal}
|
||||
setCurrentReportDeleting={setCurrentReportDeleting}
|
||||
useTextMenu
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
) : (
|
||||
<Menu>
|
||||
<HeaderReportDropDown
|
||||
chart={chart}
|
||||
setShowReportSubMenu={setShowReportSubMenu}
|
||||
setIsDropdownVisible={setIsDropdownVisible}
|
||||
isDropdownVisible={isDropdownVisible}
|
||||
showReportModal={showReportModal}
|
||||
setCurrentReportDeleting={setCurrentReportDeleting}
|
||||
useTextMenu
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
|
||||
<ModalTrigger
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import Chart from 'src/types/Chart';
|
|||
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
import Label from 'src/components/Label';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import FacePile from 'src/components/FacePile';
|
||||
|
|
@ -172,9 +172,9 @@ export default function ChartCard({
|
|||
isStarred={favoriteStatus}
|
||||
/>
|
||||
)}
|
||||
<AntdDropdown overlay={menu}>
|
||||
<Dropdown dropdownRender={() => menu}>
|
||||
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
SupersetClient,
|
||||
} from '@superset-ui/core';
|
||||
import { CardStyles } from 'src/views/CRUD/utils';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
|
@ -179,9 +179,9 @@ function DashboardCard({
|
|||
isStarred={favoriteStatus}
|
||||
/>
|
||||
)}
|
||||
<AntdDropdown overlay={menu}>
|
||||
<Dropdown dropdownRender={() => menu}>
|
||||
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -409,6 +409,7 @@ const RightMenu = ({
|
|||
{RightMenuExtension && <RightMenuExtension />}
|
||||
{!navbarRight.user_is_anonymous && showActionDropdown && (
|
||||
<StyledSubMenu
|
||||
key="sub1"
|
||||
data-test="new-dropdown"
|
||||
title={
|
||||
<StyledI data-test="new-dropdown-icon" className="fa fa-plus" />
|
||||
|
|
@ -474,6 +475,7 @@ const RightMenu = ({
|
|||
</StyledSubMenu>
|
||||
)}
|
||||
<StyledSubMenu
|
||||
key="sub3_settings"
|
||||
title={t('Settings')}
|
||||
icon={<Icons.TriangleDown iconSize="xl" />}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
|||
import { LoadingCards } from 'src/pages/Home';
|
||||
import { TableTab } from 'src/views/CRUD/types';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { copyQueryLink, useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
|
|
@ -315,11 +315,11 @@ const SavedQueries = ({
|
|||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<AntdDropdown overlay={renderMenu(q)}>
|
||||
<Dropdown dropdownRender={() => renderMenu(q)}>
|
||||
<Icons.MoreVert
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
/>
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
</QueryData>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,15 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, act } from 'spec/helpers/testing-library';
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import HeaderReportDropdown, { HeaderReportProps } from '.';
|
||||
|
||||
const createProps = () => ({
|
||||
dashboardId: 1,
|
||||
useTextMenu: false,
|
||||
isDropdownVisible: true,
|
||||
setIsDropdownVisible: jest.fn,
|
||||
setShowReportSubMenu: jest.fn,
|
||||
showReportModal: jest.fn,
|
||||
setCurrentReportDeleting: jest.fn,
|
||||
});
|
||||
|
||||
const stateWithOnlyUser = {
|
||||
|
|
@ -117,9 +118,9 @@ const stateWithUserAndReport = {
|
|||
|
||||
function setup(props: HeaderReportProps, initialState = {}) {
|
||||
render(
|
||||
<div>
|
||||
<Menu>
|
||||
<HeaderReportDropdown {...props} />
|
||||
</div>,
|
||||
</Menu>,
|
||||
{ useRedux: true, initialState },
|
||||
);
|
||||
}
|
||||
|
|
@ -147,95 +148,90 @@ describe('Header Report Dropdown', () => {
|
|||
act(() => {
|
||||
setup(mockedProps, stateWithUserAndReport);
|
||||
});
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the dropdown correctly', () => {
|
||||
it('renders the dropdown correctly', async () => {
|
||||
const mockedProps = createProps();
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithUserAndReport);
|
||||
});
|
||||
const emailReportModalButton = screen.getByRole('button');
|
||||
userEvent.click(emailReportModalButton);
|
||||
expect(screen.getByText('Email reports active')).toBeInTheDocument();
|
||||
const emailReportModalButton = screen.getByRole('menuitem');
|
||||
userEvent.hover(emailReportModalButton);
|
||||
expect(await screen.findByText('Email reports active')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit email report')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete email report')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens an edit modal', async () => {
|
||||
const mockedProps = createProps();
|
||||
mockedProps.showReportModal = jest.fn();
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithUserAndReport);
|
||||
});
|
||||
const emailReportModalButton = screen.getByRole('button');
|
||||
const emailReportModalButton = screen.getByRole('menuitem');
|
||||
userEvent.click(emailReportModalButton);
|
||||
const editModal = screen.getByText('Edit email report');
|
||||
const editModal = await screen.findByText('Edit email report');
|
||||
userEvent.click(editModal);
|
||||
const textBoxes = await screen.findAllByText('Edit email report');
|
||||
expect(textBoxes).toHaveLength(2);
|
||||
expect(mockedProps.showReportModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens a delete modal', () => {
|
||||
it('opens a delete modal', async () => {
|
||||
const mockedProps = createProps();
|
||||
mockedProps.setCurrentReportDeleting = jest.fn();
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithUserAndReport);
|
||||
});
|
||||
const emailReportModalButton = screen.getByRole('button');
|
||||
const emailReportModalButton = screen.getByRole('menuitem');
|
||||
userEvent.click(emailReportModalButton);
|
||||
const deleteModal = screen.getByText('Delete email report');
|
||||
const deleteModal = await screen.findByText('Delete email report');
|
||||
userEvent.click(deleteModal);
|
||||
expect(screen.getByText('Delete Report?')).toBeInTheDocument();
|
||||
expect(mockedProps.setCurrentReportDeleting).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a new report modal if there is no report', () => {
|
||||
const mockedProps = createProps();
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithOnlyUser);
|
||||
});
|
||||
const emailReportModalButton = screen.getByRole('button');
|
||||
userEvent.click(emailReportModalButton);
|
||||
expect(screen.getByText('Schedule a new email report')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Manage Email Reports Menu if textMenu is set to true and there is a report', () => {
|
||||
it('renders Manage Email Reports Menu if textMenu is set to true and there is a report', async () => {
|
||||
let mockedProps = createProps();
|
||||
mockedProps = {
|
||||
...mockedProps,
|
||||
useTextMenu: true,
|
||||
isDropdownVisible: true,
|
||||
};
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithUserAndReport);
|
||||
});
|
||||
expect(screen.getByText('Email reports active')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('menuitem'));
|
||||
expect(await screen.findByText('Email reports active')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit email report')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete email report')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Schedule Email Reports if textMenu is set to true and there is a report', () => {
|
||||
it('renders Schedule Email Reports if textMenu is set to true and there is a report', async () => {
|
||||
let mockedProps = createProps();
|
||||
mockedProps = {
|
||||
...mockedProps,
|
||||
useTextMenu: true,
|
||||
isDropdownVisible: true,
|
||||
};
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithOnlyUser);
|
||||
});
|
||||
expect(screen.getByText('Set up an email report')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('menuitem'));
|
||||
expect(
|
||||
await screen.findByText('Set up an email report'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Schedule Email Reports as long as user has permission through any role', () => {
|
||||
it('renders Schedule Email Reports as long as user has permission through any role', async () => {
|
||||
let mockedProps = createProps();
|
||||
mockedProps = {
|
||||
...mockedProps,
|
||||
useTextMenu: true,
|
||||
isDropdownVisible: true,
|
||||
};
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithNonAdminUser);
|
||||
});
|
||||
expect(screen.getByText('Set up an email report')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('menuitem'));
|
||||
expect(
|
||||
await screen.findByText('Set up an email report'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('do not render Schedule Email Reports if user no permission', () => {
|
||||
|
|
@ -243,11 +239,12 @@ describe('Header Report Dropdown', () => {
|
|||
mockedProps = {
|
||||
...mockedProps,
|
||||
useTextMenu: true,
|
||||
isDropdownVisible: true,
|
||||
};
|
||||
act(() => {
|
||||
setup(mockedProps, stateWithNonMenuAccessOnManage);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('menu'));
|
||||
expect(
|
||||
screen.queryByText('Set up an email report'),
|
||||
).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
|
|
@ -24,7 +24,6 @@ import {
|
|||
SupersetTheme,
|
||||
css,
|
||||
styled,
|
||||
useTheme,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
getExtensionsRegistry,
|
||||
|
|
@ -36,15 +35,11 @@ import { AlertObject } from 'src/features/alerts/types';
|
|||
import { Menu } from 'src/components/Menu';
|
||||
import Checkbox from 'src/components/Checkbox';
|
||||
import { noOp } from 'src/utils/common';
|
||||
import { NoAnimationDropdown } from 'src/components/Dropdown';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import ReportModal from 'src/features/reports/ReportModal';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import {
|
||||
fetchUISpecificReport,
|
||||
toggleActive,
|
||||
deleteActiveReport,
|
||||
} from 'src/features/reports/ReportModal/actions';
|
||||
import { reportSelector } from 'src/views/CRUD/hooks';
|
||||
import { MenuItemWithCheckboxContainer } from 'src/explore/components/useExploreAdditionalActionsMenu/index';
|
||||
|
|
@ -99,9 +94,10 @@ export interface HeaderReportProps {
|
|||
chart?: ChartState;
|
||||
useTextMenu?: boolean;
|
||||
setShowReportSubMenu?: (show: boolean) => void;
|
||||
setIsDropdownVisible?: (visible: boolean) => void;
|
||||
isDropdownVisible?: boolean;
|
||||
showReportSubMenu?: boolean;
|
||||
submenuTitle?: string;
|
||||
showReportModal: () => void;
|
||||
setCurrentReportDeleting: (report: AlertObject | null) => void;
|
||||
}
|
||||
|
||||
// Same instance to be used in useEffects
|
||||
|
|
@ -112,9 +108,9 @@ export default function HeaderReportDropDown({
|
|||
chart,
|
||||
useTextMenu = false,
|
||||
setShowReportSubMenu,
|
||||
setIsDropdownVisible,
|
||||
isDropdownVisible,
|
||||
...rest
|
||||
submenuTitle,
|
||||
showReportModal,
|
||||
setCurrentReportDeleting,
|
||||
}: HeaderReportProps) {
|
||||
const dispatch = useDispatch();
|
||||
const report = useSelector<any, AlertObject>(state => {
|
||||
|
|
@ -156,22 +152,13 @@ export default function HeaderReportDropDown({
|
|||
return permissions.some(permission => permission.length > 0);
|
||||
};
|
||||
|
||||
const [currentReportDeleting, setCurrentReportDeleting] =
|
||||
useState<AlertObject | null>(null);
|
||||
const theme = useTheme();
|
||||
const prevDashboard = usePrevious(dashboardId);
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const toggleActiveKey = async (data: AlertObject, checked: boolean) => {
|
||||
if (data?.id) {
|
||||
dispatch(toggleActive(data, checked));
|
||||
}
|
||||
};
|
||||
|
||||
const handleReportDelete = async (report: AlertObject) => {
|
||||
await dispatch(deleteActiveReport(report));
|
||||
setCurrentReportDeleting(null);
|
||||
};
|
||||
|
||||
const shouldFetch =
|
||||
canAddReports() &&
|
||||
!!((dashboardId && prevDashboard !== dashboardId) || chart?.id);
|
||||
|
|
@ -191,12 +178,6 @@ export default function HeaderReportDropDown({
|
|||
|
||||
const showReportSubMenu = report && setShowReportSubMenu && canAddReports();
|
||||
|
||||
// @z-index-below-dashboard-header (100) - 1 = 99
|
||||
const dropdownOverlayStyle = {
|
||||
zIndex: 99,
|
||||
animationDuration: '0s',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showReportSubMenu) {
|
||||
setShowReportSubMenu(true);
|
||||
|
|
@ -206,22 +187,16 @@ export default function HeaderReportDropDown({
|
|||
}, [report]);
|
||||
|
||||
const handleShowMenu = () => {
|
||||
if (setIsDropdownVisible) {
|
||||
setIsDropdownVisible(false);
|
||||
setShowModal(true);
|
||||
}
|
||||
showReportModal();
|
||||
};
|
||||
|
||||
const handleDeleteMenuClick = () => {
|
||||
if (setIsDropdownVisible) {
|
||||
setIsDropdownVisible(false);
|
||||
setCurrentReportDeleting(report);
|
||||
}
|
||||
};
|
||||
|
||||
const textMenu = () =>
|
||||
isEmpty(report) ? (
|
||||
<Menu selectable={false} {...rest} css={onMenuHover}>
|
||||
<Menu.SubMenu title={submenuTitle} css={onMenuHover}>
|
||||
<Menu.Item onClick={handleShowMenu}>
|
||||
{DropdownItemExtension ? (
|
||||
<StyledDropdownItemWithIcon>
|
||||
|
|
@ -233,9 +208,14 @@ export default function HeaderReportDropDown({
|
|||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</Menu>
|
||||
</Menu.SubMenu>
|
||||
) : (
|
||||
<Menu selectable={false} css={{ border: 'none' }}>
|
||||
<Menu.SubMenu
|
||||
title={submenuTitle}
|
||||
css={css`
|
||||
border: none;
|
||||
`}
|
||||
>
|
||||
<Menu.Item
|
||||
css={onMenuItemHover}
|
||||
onClick={() => toggleActiveKey(report, !isReportActive)}
|
||||
|
|
@ -251,10 +231,15 @@ export default function HeaderReportDropDown({
|
|||
<Menu.Item css={onMenuItemHover} onClick={handleDeleteMenuClick}>
|
||||
{t('Delete email report')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
const menu = () => (
|
||||
<Menu selectable={false} css={{ width: '200px' }}>
|
||||
const menu = (title: ReactNode) => (
|
||||
<Menu.SubMenu
|
||||
title={title}
|
||||
css={css`
|
||||
width: 200px;
|
||||
`}
|
||||
>
|
||||
<Menu.Item>
|
||||
{t('Email reports active')}
|
||||
<Switch
|
||||
|
|
@ -262,10 +247,12 @@ export default function HeaderReportDropDown({
|
|||
checked={isReportActive}
|
||||
onClick={(checked: boolean) => toggleActiveKey(report, checked)}
|
||||
size="small"
|
||||
css={{ marginLeft: theme.gridUnit * 2 }}
|
||||
css={theme => css`
|
||||
margin-left: ${theme.gridUnit * 2}px;
|
||||
`}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => setShowModal(true)}>
|
||||
<Menu.Item onClick={() => showReportModal()}>
|
||||
{t('Edit email report')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
|
|
@ -274,7 +261,7 @@ export default function HeaderReportDropDown({
|
|||
>
|
||||
{t('Delete email report')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
|
||||
const iconMenu = () =>
|
||||
|
|
@ -284,65 +271,12 @@ export default function HeaderReportDropDown({
|
|||
title={t('Schedule email report')}
|
||||
tabIndex={0}
|
||||
className="action-button action-schedule-report"
|
||||
onClick={() => setShowModal(true)}
|
||||
onClick={() => showReportModal()}
|
||||
>
|
||||
<Icons.Calendar />
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<NoAnimationDropdown
|
||||
overlay={menu()}
|
||||
overlayStyle={dropdownOverlayStyle}
|
||||
trigger={['click']}
|
||||
getPopupContainer={(triggerNode: any) =>
|
||||
triggerNode.closest('.action-button')
|
||||
}
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
className="action-button action-schedule-report"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icons.Calendar />
|
||||
</span>
|
||||
</NoAnimationDropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canAddReports() && (
|
||||
<>
|
||||
<ReportModal
|
||||
userId={user.userId}
|
||||
show={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
userEmail={user.email}
|
||||
dashboardId={dashboardId}
|
||||
chart={chart}
|
||||
creationMethod={
|
||||
dashboardId ? CreationMethod.Dashboards : CreationMethod.Charts
|
||||
}
|
||||
/>
|
||||
{isDropdownVisible ? (useTextMenu ? textMenu() : iconMenu()) : null}
|
||||
{currentReportDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
'This action will permanently delete %s.',
|
||||
currentReportDeleting?.name,
|
||||
)}
|
||||
onConfirm={() => {
|
||||
if (currentReportDeleting) {
|
||||
handleReportDelete(currentReportDeleting);
|
||||
}
|
||||
}}
|
||||
onHide={() => setCurrentReportDeleting(null)}
|
||||
open
|
||||
title={t('Delete Report?')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
menu(<Icons.Calendar />)
|
||||
);
|
||||
return <>{canAddReports() && (useTextMenu ? textMenu() : iconMenu())}</>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { isFeatureEnabled, FeatureFlag, t, useTheme } from '@superset-ui/core';
|
||||
import { CardStyles } from 'src/views/CRUD/utils';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Dropdown } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
|
|
@ -108,9 +108,9 @@ function TagCard({
|
|||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<AntdDropdown overlay={menu}>
|
||||
<Dropdown dropdownRender={() => menu}>
|
||||
<Icons.MoreVert iconColor={theme.colors.grayscale.base} />
|
||||
</AntdDropdown>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue