chore: Create a generic header component for Explore and Dashboard (#20044)

* chore: Create a generic header component for Explore and Dashboard

* Add tests

* Fix undefined error

* Remove duplicate code

* Fix cypress test
This commit is contained in:
Kamil Gabryjelski 2022-05-13 15:36:18 +02:00 committed by GitHub
parent b53daa91ec
commit 1cd002e801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 730 additions and 685 deletions

View File

@ -34,7 +34,7 @@ describe('Download Chart > Distribution bar chart', () => {
};
cy.visitChartByParams(JSON.stringify(formData));
cy.get('.right-button-panel > .ant-dropdown-trigger').click();
cy.get('.right-button-panel .ant-dropdown-trigger').click();
cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
cy.get(
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',

View File

@ -19,20 +19,21 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from 'spec/helpers/testing-library';
import { ChartEditableTitle } from './index';
import { DynamicEditableTitle } from '.';
const createProps = (overrides: Record<string, any> = {}) => ({
title: 'Chart title',
placeholder: 'Add the name of the chart',
canEdit: true,
onSave: jest.fn(),
label: 'Chart title',
...overrides,
});
describe('Chart editable title', () => {
it('renders chart title', () => {
const props = createProps();
render(<ChartEditableTitle {...props} />);
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Chart title')).toBeVisible();
});
@ -40,13 +41,13 @@ describe('Chart editable title', () => {
const props = createProps({
title: '',
});
render(<ChartEditableTitle {...props} />);
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Add the name of the chart')).toBeVisible();
});
it('click, edit and save title', () => {
const props = createProps();
render(<ChartEditableTitle {...props} />);
render(<DynamicEditableTitle {...props} />);
const textboxElement = screen.getByRole('textbox');
userEvent.click(textboxElement);
userEvent.type(textboxElement, ' edited');
@ -57,7 +58,7 @@ describe('Chart editable title', () => {
it('renders in non-editable mode', () => {
const props = createProps({ canEdit: false });
render(<ChartEditableTitle {...props} />);
render(<DynamicEditableTitle {...props} />);
const titleElement = screen.getByLabelText('Chart title');
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
expect(titleElement).toBeVisible();

View File

@ -26,62 +26,62 @@ import React, {
useRef,
useState,
} from 'react';
import { css, styled, t } from '@superset-ui/core';
import { css, SupersetTheme, t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { useResizeDetector } from 'react-resize-detector';
export type ChartEditableTitleProps = {
export type DynamicEditableTitleProps = {
title: string;
placeholder: string;
onSave: (title: string) => void;
canEdit: boolean;
label: string | undefined;
};
const Styles = styled.div`
${({ theme }) => css`
display: flex;
font-size: ${theme.typography.sizes.xl}px;
font-weight: ${theme.typography.weights.bold};
const titleStyles = (theme: SupersetTheme) => css`
display: flex;
font-size: ${theme.typography.sizes.xl}px;
font-weight: ${theme.typography.weights.bold};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& .dynamic-title,
& .dynamic-title-input {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& .chart-title,
& .chart-title-input {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& .dynamic-title {
cursor: default;
}
& .dynamic-title-input {
border: none;
padding: 0;
outline: none;
& .chart-title {
cursor: default;
&::placeholder {
color: ${theme.colors.grayscale.light1};
}
& .chart-title-input {
border: none;
padding: 0;
outline: none;
}
&::placeholder {
color: ${theme.colors.grayscale.light1};
}
}
& .input-sizer {
position: absolute;
left: -9999px;
display: inline-block;
}
`}
& .input-sizer {
position: absolute;
left: -9999px;
display: inline-block;
}
`;
export const ChartEditableTitle = ({
export const DynamicEditableTitle = ({
title,
placeholder,
onSave,
canEdit,
}: ChartEditableTitleProps) => {
label,
}: DynamicEditableTitleProps) => {
const [isEditing, setIsEditing] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
const contentRef = useRef<HTMLInputElement>(null);
@ -170,7 +170,7 @@ export const ChartEditableTitle = ({
);
return (
<Styles ref={containerRef}>
<div css={titleStyles} ref={containerRef}>
<Tooltip
id="title-tooltip"
title={showTooltip && currentTitle && !isEditing ? currentTitle : null}
@ -178,8 +178,8 @@ export const ChartEditableTitle = ({
{canEdit ? (
<input
data-test="editable-title-input"
className="chart-title-input"
aria-label={t('Chart title')}
className="dynamic-title-input"
aria-label={label ?? t('Title')}
ref={contentRef}
onChange={handleChange}
onBlur={handleBlur}
@ -199,8 +199,8 @@ export const ChartEditableTitle = ({
/>
) : (
<span
className="chart-title"
aria-label={t('Chart title')}
className="dynamic-title"
aria-label={label ?? t('Title')}
ref={contentRef}
>
{currentTitle}
@ -208,6 +208,6 @@ export const ChartEditableTitle = ({
)}
</Tooltip>
<span ref={sizerRef} className="input-sizer" aria-hidden tabIndex={-1} />
</Styles>
</div>
);
};

View File

@ -23,7 +23,7 @@ import { Tooltip } from 'src/components/Tooltip';
import { useComponentDidMount } from 'src/hooks/useComponentDidMount';
import Icons from 'src/components/Icons';
interface FaveStarProps {
export interface FaveStarProps {
itemId: number;
isStarred?: boolean;
showTooltip?: boolean;

View File

@ -0,0 +1,57 @@
/**
* 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 React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { PageHeaderWithActions, PageHeaderWithActionsProps } from './index';
import { Menu } from '../Menu';
const defaultProps: PageHeaderWithActionsProps = {
editableTitleProps: {
title: 'Test title',
placeholder: 'Test placeholder',
onSave: jest.fn(),
canEdit: true,
label: 'Title',
},
showTitlePanelItems: true,
certificatiedBadgeProps: {},
showFaveStar: true,
faveStarProps: { itemId: 1, saveFaveStar: jest.fn() },
titlePanelAdditionalItems: <button type="button">Title panel button</button>,
rightPanelAdditionalItems: <button type="button">Save</button>,
additionalActionsMenu: (
<Menu>
<Menu.Item>Test menu item</Menu.Item>
</Menu>
),
menuDropdownProps: { onVisibleChange: jest.fn(), visible: true },
};
test('Renders', async () => {
render(<PageHeaderWithActions {...defaultProps} />);
expect(screen.getByText('Test title')).toBeVisible();
expect(screen.getByTestId('fave-unfave-icon')).toBeVisible();
expect(screen.getByText('Title panel button')).toBeVisible();
expect(screen.getByText('Save')).toBeVisible();
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(defaultProps.menuDropdownProps.onVisibleChange).toHaveBeenCalled();
});

View File

@ -0,0 +1,152 @@
/**
* 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 React, { ReactNode, ReactElement } from 'react';
import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
import { AntdDropdown, AntdDropdownProps } from 'src/components';
import {
DynamicEditableTitle,
DynamicEditableTitleProps,
} from '../DynamicEditableTitle';
import CertifiedBadge, { CertifiedBadgeProps } from '../CertifiedBadge';
import FaveStar, { FaveStarProps } from '../FaveStar';
import Icons from '../Icons';
import Button from '../Button';
export const menuTriggerStyles = (theme: SupersetTheme) => css`
width: ${theme.gridUnit * 8}px;
height: ${theme.gridUnit * 8}px;
padding: 0;
border: 1px solid ${theme.colors.primary.dark2};
&.ant-btn > span.anticon {
line-height: 0;
transition: inherit;
}
&:hover:not(:focus) > span.anticon {
color: ${theme.colors.primary.light1};
}
`;
const headerStyles = (theme: SupersetTheme) => css`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: space-between;
height: 100%;
span[role='button'] {
display: flex;
height: 100%;
}
.title-panel {
display: flex;
align-items: center;
min-width: 0;
margin-right: ${theme.gridUnit * 12}px;
}
.right-button-panel {
display: flex;
align-items: center;
}
`;
const buttonsStyles = (theme: SupersetTheme) => css`
display: flex;
align-items: center;
padding-left: ${theme.gridUnit * 2}px;
& .fave-unfave-icon {
padding: 0 ${theme.gridUnit}px;
&:first-child {
padding-left: 0;
}
}
`;
const additionalActionsContainerStyles = (theme: SupersetTheme) => css`
margin-left: ${theme.gridUnit * 2}px;
`;
export type PageHeaderWithActionsProps = {
editableTitleProps: DynamicEditableTitleProps;
showTitlePanelItems: boolean;
certificatiedBadgeProps?: CertifiedBadgeProps;
showFaveStar: boolean;
faveStarProps: FaveStarProps;
titlePanelAdditionalItems: ReactNode;
rightPanelAdditionalItems: ReactNode;
additionalActionsMenu: ReactElement;
menuDropdownProps: Omit<AntdDropdownProps, 'overlay'>;
};
export const PageHeaderWithActions = ({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles}>
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
<AntdDropdown
trigger={['click']}
overlay={additionalActionsMenu}
{...menuDropdownProps}
>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
>
<Icons.MoreHoriz
iconColor={theme.colors.primary.dark2}
iconSize="l"
/>
</Button>
</AntdDropdown>
</div>
</div>
</div>
);
};

View File

@ -44,7 +44,7 @@ import {
deleteActiveReport,
} from 'src/reports/actions/reports';
import { reportSelector } from 'src/views/CRUD/hooks';
import { MenuItemWithCheckboxContainer } from 'src/explore/components/ExploreAdditionalActionsMenu/index';
import { MenuItemWithCheckboxContainer } from 'src/explore/components/useExploreAdditionalActionsMenu/index';
const deleteColor = (theme: SupersetTheme) => css`
color: ${theme.colors.error.base};

View File

@ -73,4 +73,5 @@ export {
export type { FormInstance } from 'antd/lib/form';
export type { ListItemProps } from 'antd/lib/list';
export type { ModalProps as AntdModalProps } from 'antd/lib/modal';
export type { DropDownProps as AntdDropdownProps } from 'antd/lib/dropdown';
export type { RadioChangeEvent } from 'antd/lib/radio';

View File

@ -1,294 +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 React from 'react';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import * as chartAction from 'src/components/Chart/chartAction';
import * as downloadAsImage from 'src/utils/downloadAsImage';
import * as exploreUtils from 'src/explore/exploreUtils';
import ExploreAdditionalActionsMenu from '.';
const createProps = () => ({
latestQueryFormData: {
viz_type: 'histogram',
datasource: '49__table',
slice_id: 318,
url_params: {},
granularity_sqla: 'time_start',
time_range: 'No filter',
all_columns_x: ['age'],
adhoc_filters: [],
row_limit: 10000,
groupby: null,
color_scheme: 'supersetColors',
label_colors: {},
link_length: '25',
x_axis_label: 'age',
y_axis_label: 'count',
},
slice: {
cache_timeout: null,
changed_on: '2021-03-19T16:30:56.750230',
changed_on_humanized: '3 days ago',
datasource: 'FCC 2018 Survey',
description: null,
description_markeddown: '',
edit_url: '/chart/edit/318',
form_data: {
adhoc_filters: [],
all_columns_x: ['age'],
color_scheme: 'supersetColors',
datasource: '49__table',
granularity_sqla: 'time_start',
groupby: null,
label_colors: {},
link_length: '25',
queryFields: { groupby: 'groupby' },
row_limit: 10000,
slice_id: 318,
time_range: 'No filter',
url_params: {},
viz_type: 'histogram',
x_axis_label: 'age',
y_axis_label: 'count',
},
modified: '<span class="no-wrap">3 days ago</span>',
owners: [],
slice_id: 318,
slice_name: 'Age distribution of respondents',
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20318%7D',
},
chartStatus: 'rendered',
onOpenPropertiesModal: jest.fn(),
onOpenInEditor: jest.fn(),
canDownloadCSV: false,
showReportSubMenu: false,
});
fetchMock.post(
'http://api/v1/chart/data?form_data=%7B%22slice_id%22%3A318%7D',
{ body: {} },
{
sendAsJson: false,
},
);
test('Should render a button', () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, { useRedux: true });
expect(screen.getByRole('button')).toBeInTheDocument();
});
test('Should open a menu', () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
expect(props.onOpenInEditor).toBeCalledTimes(0);
expect(props.onOpenPropertiesModal).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button'));
expect(props.onOpenInEditor).toBeCalledTimes(0);
expect(props.onOpenPropertiesModal).toBeCalledTimes(0);
expect(screen.getByText('Edit chart properties')).toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
expect(screen.getByText('Share')).toBeInTheDocument();
expect(screen.getByText('View query')).toBeInTheDocument();
expect(screen.getByText('Run in SQL Lab')).toBeInTheDocument();
expect(screen.queryByText('Set up an email report')).not.toBeInTheDocument();
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
});
test('Should open download submenu', async () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
expect(screen.queryByText('Export to .CSV')).not.toBeInTheDocument();
expect(screen.queryByText('Export to .JSON')).not.toBeInTheDocument();
expect(screen.queryByText('Download as image')).not.toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
expect(await screen.findByText('Export to .JSON')).toBeInTheDocument();
expect(await screen.findByText('Download as image')).toBeInTheDocument();
});
test('Should open share submenu', async () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
expect(
screen.queryByText('Copy permalink to clipboard'),
).not.toBeInTheDocument();
expect(screen.queryByText('Embed code')).not.toBeInTheDocument();
expect(screen.queryByText('Share chart by email')).not.toBeInTheDocument();
expect(screen.getByText('Share')).toBeInTheDocument();
userEvent.hover(screen.getByText('Share'));
expect(
await screen.findByText('Copy permalink to clipboard'),
).toBeInTheDocument();
expect(await screen.findByText('Embed code')).toBeInTheDocument();
expect(await screen.findByText('Share chart by email')).toBeInTheDocument();
});
test('Should call onOpenPropertiesModal when click on "Edit chart properties"', () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
expect(props.onOpenInEditor).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button'));
userEvent.click(
screen.getByRole('menuitem', { name: 'Edit chart properties' }),
);
expect(props.onOpenPropertiesModal).toBeCalledTimes(1);
});
test('Should call getChartDataRequest when click on "View query"', async () => {
const props = createProps();
const getChartDataRequest = jest.spyOn(chartAction, 'getChartDataRequest');
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
expect(getChartDataRequest).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button'));
expect(getChartDataRequest).toBeCalledTimes(0);
const menuItem = screen.getByText('View query').parentElement!;
userEvent.click(menuItem);
await waitFor(() => expect(getChartDataRequest).toBeCalledTimes(1));
});
test('Should call onOpenInEditor when click on "Run in SQL Lab"', () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
expect(props.onOpenInEditor).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button'));
expect(props.onOpenInEditor).toBeCalledTimes(0);
userEvent.click(screen.getByRole('menuitem', { name: 'Run in SQL Lab' }));
expect(props.onOpenInEditor).toBeCalledTimes(1);
});
describe('Download', () => {
let spyDownloadAsImage = sinon.spy();
let spyExportChart = sinon.spy();
beforeEach(() => {
spyDownloadAsImage = sinon.spy(downloadAsImage, 'default');
spyExportChart = sinon.spy(exploreUtils, 'exportChart');
});
afterEach(() => {
spyDownloadAsImage.restore();
spyExportChart.restore();
});
test('Should call downloadAsImage when click on "Download as image"', async () => {
const props = createProps();
const spy = jest.spyOn(downloadAsImage, 'default');
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
expect(spy).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button'));
expect(spy).toBeCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
const downloadAsImageElement = await screen.findByText('Download as image');
userEvent.click(downloadAsImageElement);
expect(spy).toBeCalledTimes(1);
});
test('Should not export to CSV if canDownloadCSV=false', async () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(0);
spyExportChart.restore();
});
test('Should export to CSV if canDownloadCSV=true', async () => {
const props = createProps();
props.canDownloadCSV = true;
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(1);
spyExportChart.restore();
});
test('Should export to JSON', async () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
userEvent.hover(screen.getByText('Download'));
const exportJsonElement = await screen.findByText('Export to .JSON');
userEvent.click(exportJsonElement);
expect(spyExportChart.callCount).toBe(1);
});
test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => {
const props = createProps();
props.canDownloadCSV = true;
props.latestQueryFormData.viz_type = 'pivot_table_v2';
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText('Export to pivoted .CSV');
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(1);
});
});

View File

@ -18,10 +18,13 @@
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import sinon from 'sinon';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import * as chartAction from 'src/components/Chart/chartAction';
import * as downloadAsImage from 'src/utils/downloadAsImage';
import * as exploreUtils from 'src/explore/exploreUtils';
import ExploreHeader from '.';
const chartEndpoint = 'glob:*api/v1/chart/*';
@ -30,6 +33,7 @@ fetchMock.get(chartEndpoint, { json: 'foo' });
const createProps = () => ({
chart: {
id: 1,
latestQueryFormData: {
viz_type: 'histogram',
datasource: '49__table',
@ -88,17 +92,29 @@ const createProps = () => ({
},
slice_name: 'Age distribution of respondents',
actions: {
postChartFormData: () => null,
updateChartTitle: () => null,
fetchFaveStar: () => null,
saveFaveStar: () => null,
postChartFormData: jest.fn(),
updateChartTitle: jest.fn(),
fetchFaveStar: jest.fn(),
saveFaveStar: jest.fn(),
redirectSQLLab: jest.fn(),
},
user: {
userId: 1,
},
onSaveChart: jest.fn(),
canOverwrite: false,
canDownload: false,
isStarred: false,
});
fetchMock.post(
'http://api/v1/chart/data?form_data=%7B%22slice_id%22%3A318%7D',
{ body: {} },
{
sendAsJson: false,
},
);
test('Cancelling changes to the properties should reset previous properties', () => {
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
@ -136,3 +152,208 @@ test('Save disabled', () => {
userEvent.click(screen.getByText('Save'));
expect(props.onSaveChart).not.toHaveBeenCalled();
});
describe('Additional actions tests', () => {
test('Should render a button', () => {
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
expect(screen.getByLabelText('Menu actions trigger')).toBeInTheDocument();
});
test('Should open a menu', () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(screen.getByText('Edit chart properties')).toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
expect(screen.getByText('Share')).toBeInTheDocument();
expect(screen.getByText('View query')).toBeInTheDocument();
expect(screen.getByText('Run in SQL Lab')).toBeInTheDocument();
expect(
screen.queryByText('Set up an email report'),
).not.toBeInTheDocument();
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
});
test('Should open download submenu', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(screen.queryByText('Export to .CSV')).not.toBeInTheDocument();
expect(screen.queryByText('Export to .JSON')).not.toBeInTheDocument();
expect(screen.queryByText('Download as image')).not.toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
userEvent.hover(screen.getByText('Download'));
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
expect(await screen.findByText('Export to .JSON')).toBeInTheDocument();
expect(await screen.findByText('Download as image')).toBeInTheDocument();
});
test('Should open share submenu', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(
screen.queryByText('Copy permalink to clipboard'),
).not.toBeInTheDocument();
expect(screen.queryByText('Embed code')).not.toBeInTheDocument();
expect(screen.queryByText('Share chart by email')).not.toBeInTheDocument();
expect(screen.getByText('Share')).toBeInTheDocument();
userEvent.hover(screen.getByText('Share'));
expect(
await screen.findByText('Copy permalink to clipboard'),
).toBeInTheDocument();
expect(await screen.findByText('Embed code')).toBeInTheDocument();
expect(await screen.findByText('Share chart by email')).toBeInTheDocument();
});
test('Should call onOpenPropertiesModal when click on "Edit chart properties"', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
expect(props.actions.redirectSQLLab).toBeCalledTimes(0);
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.click(
screen.getByRole('menuitem', { name: 'Edit chart properties' }),
);
expect(await screen.findByText('Edit Chart Properties')).toBeVisible();
});
test('Should call getChartDataRequest when click on "View query"', async () => {
const props = createProps();
const getChartDataRequest = jest.spyOn(chartAction, 'getChartDataRequest');
render(<ExploreHeader {...props} />, {
useRedux: true,
});
expect(getChartDataRequest).toBeCalledTimes(0);
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(getChartDataRequest).toBeCalledTimes(0);
const menuItem = screen.getByText('View query').parentElement!;
userEvent.click(menuItem);
await waitFor(() => expect(getChartDataRequest).toBeCalledTimes(1));
});
test('Should call onOpenInEditor when click on "Run in SQL Lab"', () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
expect(props.actions.redirectSQLLab).toBeCalledTimes(0);
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(props.actions.redirectSQLLab).toBeCalledTimes(0);
userEvent.click(screen.getByRole('menuitem', { name: 'Run in SQL Lab' }));
expect(props.actions.redirectSQLLab).toBeCalledTimes(1);
});
describe('Download', () => {
let spyDownloadAsImage = sinon.spy();
let spyExportChart = sinon.spy();
beforeEach(() => {
spyDownloadAsImage = sinon.spy(downloadAsImage, 'default');
spyExportChart = sinon.spy(exploreUtils, 'exportChart');
});
afterEach(() => {
spyDownloadAsImage.restore();
spyExportChart.restore();
});
test('Should call downloadAsImage when click on "Download as image"', async () => {
const props = createProps();
const spy = jest.spyOn(downloadAsImage, 'default');
render(<ExploreHeader {...props} />, {
useRedux: true,
});
expect(spy).toBeCalledTimes(0);
userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(spy).toBeCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
const downloadAsImageElement = await screen.findByText(
'Download as image',
);
userEvent.click(downloadAsImageElement);
expect(spy).toBeCalledTimes(1);
});
test('Should not export to CSV if canDownload=false', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(0);
spyExportChart.restore();
});
test('Should export to CSV if canDownload=true', async () => {
const props = createProps();
props.canDownload = true;
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText('Export to .CSV');
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(1);
spyExportChart.restore();
});
test('Should export to JSON', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Download'));
const exportJsonElement = await screen.findByText('Export to .JSON');
userEvent.click(exportJsonElement);
expect(spyExportChart.callCount).toBe(1);
});
test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => {
const props = createProps();
props.canDownload = true;
props.chart.latestQueryFormData.viz_type = 'pivot_table_v2';
render(<ExploreHeader {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByLabelText('Menu actions trigger'));
userEvent.hover(screen.getByText('Download'));
const exportCSVElement = await screen.findByText(
'Export to pivoted .CSV',
);
userEvent.click(exportCSVElement);
expect(spyExportChart.callCount).toBe(1);
});
});
});

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
@ -30,14 +30,12 @@ import {
import { toggleActive, deleteActiveReport } from 'src/reports/actions/reports';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import FaveStar from 'src/components/FaveStar';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions';
import CertifiedBadge from 'src/components/CertifiedBadge';
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
import { ChartEditableTitle } from './ChartEditableTitle';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
const propTypes = {
actions: PropTypes.object.isRequired,
@ -48,7 +46,7 @@ const propTypes = {
slice: PropTypes.object,
sliceName: PropTypes.string,
table_name: PropTypes.string,
form_data: PropTypes.object,
formData: PropTypes.object,
ownState: PropTypes.object,
timeout: PropTypes.number,
chart: chartPropShape,
@ -62,70 +60,25 @@ const saveButtonStyles = theme => css`
}
`;
const headerStyles = theme => css`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: space-between;
height: 100%;
export const ExploreChartHeader = ({
dashboardId,
slice,
actions,
formData,
chart,
user,
canOverwrite,
canDownload,
isStarred,
sliceUpdated,
sliceName,
onSaveChart,
saveDisabled,
}) => {
const { latestQueryFormData, sliceFormData } = chart;
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
span[role='button'] {
display: flex;
height: 100%;
}
.title-panel {
display: flex;
align-items: center;
min-width: 0;
margin-right: ${theme.gridUnit * 12}px;
}
.right-button-panel {
display: flex;
align-items: center;
}
`;
const buttonsStyles = theme => css`
display: flex;
align-items: center;
padding-left: ${theme.gridUnit * 2}px;
& .fave-unfave-icon {
padding: 0 ${theme.gridUnit}px;
&:first-child {
padding-left: 0;
}
}
`;
const saveButtonContainerStyles = theme => css`
margin-right: ${theme.gridUnit * 2}px;
`;
export class ExploreChartHeader extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isPropertiesModalOpen: false,
};
this.openPropertiesModal = this.openPropertiesModal.bind(this);
this.closePropertiesModal = this.closePropertiesModal.bind(this);
this.fetchChartDashboardData = this.fetchChartDashboardData.bind(this);
}
componentDidMount() {
const { dashboardId } = this.props;
if (dashboardId) {
this.fetchChartDashboardData();
}
}
async fetchChartDashboardData() {
const { dashboardId, slice } = this.props;
const fetchChartDashboardData = async () => {
await SupersetClient.get({
endpoint: `/api/v1/chart/${slice.slice_id}`,
})
@ -162,96 +115,71 @@ export class ExploreChartHeader extends React.PureComponent {
}
})
.catch(() => {});
}
};
postChartFormData() {
this.props.actions.postChartFormData(
this.props.form_data,
true,
this.props.timeout,
this.props.chart.id,
this.props.ownState,
);
}
useEffect(() => {
if (dashboardId) {
fetchChartDashboardData();
}
}, []);
openPropertiesModal() {
this.setState({
isPropertiesModalOpen: true,
});
}
const openPropertiesModal = () => {
setIsPropertiesModalOpen(true);
};
closePropertiesModal() {
this.setState({
isPropertiesModalOpen: false,
});
}
const closePropertiesModal = () => {
setIsPropertiesModalOpen(false);
};
render() {
const {
actions,
chart,
user,
formData,
slice,
canOverwrite,
const [menu, isDropdownVisible, setIsDropdownVisible] =
useExploreAdditionalActionsMenu(
latestQueryFormData,
canDownload,
isStarred,
sliceUpdated,
sliceName,
onSaveChart,
saveDisabled,
} = this.props;
const { latestQueryFormData, sliceFormData } = chart;
const oldSliceName = slice?.slice_name;
return (
<div id="slice-header" css={headerStyles}>
<div className="title-panel">
<ChartEditableTitle
title={sliceName}
canEdit={
!slice ||
canOverwrite ||
(slice?.owners || []).includes(user?.userId)
}
onSave={actions.updateChartTitle}
placeholder={t('Add the name of the chart')}
/>
{slice && (
<span css={buttonsStyles}>
{slice.certified_by && (
<CertifiedBadge
certifiedBy={slice.certified_by}
details={slice.certification_details}
/>
)}
{user.userId && (
<FaveStar
itemId={slice.slice_id}
fetchFaveStar={actions.fetchFaveStar}
saveFaveStar={actions.saveFaveStar}
isStarred={isStarred}
showTooltip
/>
)}
{this.state.isPropertiesModalOpen && (
<PropertiesModal
show={this.state.isPropertiesModalOpen}
onHide={this.closePropertiesModal}
onSave={sliceUpdated}
slice={slice}
/>
)}
{sliceFormData && (
<AlteredSliceTag
className="altered"
origFormData={{ ...sliceFormData, chartTitle: oldSliceName }}
currentFormData={{ ...formData, chartTitle: sliceName }}
/>
)}
</span>
)}
</div>
<div className="right-button-panel">
slice,
actions.redirectSQLLab,
openPropertiesModal,
);
const oldSliceName = slice?.slice_name;
return (
<>
<PageHeaderWithActions
editableTitleProps={{
title: sliceName,
canEdit:
!slice ||
canOverwrite ||
(slice?.owners || []).includes(user?.userId),
onSave: actions.updateChartTitle,
placeholder: t('Add the name of the chart'),
label: t('Chart title'),
}}
showTitlePanelItems={!!slice}
certificatiedBadgeProps={{
certifiedBy: slice?.certified_by,
details: slice?.certification_details,
}}
showFaveStar={!!user?.userId}
faveStarProps={{
itemId: slice?.slice_id,
fetchFaveStar: actions.fetchFaveStar,
saveFaveStar: actions.saveFaveStar,
isStarred,
showTooltip: true,
}}
titlePanelAdditionalItems={
sliceFormData ? (
<AlteredSliceTag
className="altered"
origFormData={{
...sliceFormData,
chartTitle: oldSliceName,
}}
currentFormData={{ ...formData, chartTitle: sliceName }}
/>
) : null
}
rightPanelAdditionalItems={
<Tooltip
title={
saveDisabled
@ -260,7 +188,7 @@ export class ExploreChartHeader extends React.PureComponent {
}
>
{/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
<div css={saveButtonContainerStyles}>
<div>
<Button
buttonStyle="secondary"
onClick={onSaveChart}
@ -273,18 +201,24 @@ export class ExploreChartHeader extends React.PureComponent {
</Button>
</div>
</Tooltip>
<ExploreAdditionalActionsMenu
onOpenInEditor={actions.redirectSQLLab}
onOpenPropertiesModal={this.openPropertiesModal}
slice={slice}
canDownloadCSV={canDownload}
latestQueryFormData={latestQueryFormData}
/>
</div>
</div>
);
}
}
}
additionalActionsMenu={menu}
menuDropdownProps={{
visible: isDropdownVisible,
onVisibleChange: setIsDropdownVisible,
}}
/>
{isPropertiesModalOpen && (
<PropertiesModal
show={isPropertiesModalOpen}
onHide={closePropertiesModal}
onSave={sliceUpdated}
slice={slice}
/>
)}
</>
);
};
ExploreChartHeader.propTypes = propTypes;

View File

@ -571,7 +571,6 @@ function ExploreViewContainer(props) {
<ExploreContainer>
<ExploreHeaderContainer>
<ConnectedExploreChartHeader
ownState={props.ownState}
actions={props.actions}
canOverwrite={props.can_overwrite}
canDownload={props.can_download}
@ -581,7 +580,6 @@ function ExploreViewContainer(props) {
sliceName={props.sliceName}
table_name={props.table_name}
formData={props.form_data}
timeout={props.timeout}
chart={props.chart}
user={props.user}
reports={props.reports}

View File

@ -16,33 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { FileOutlined, FileImageOutlined } from '@ant-design/icons';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { AntdDropdown } from 'src/components';
import { Menu } from 'src/components/Menu';
import Icons from 'src/components/Icons';
import ModalTrigger from 'src/components/ModalTrigger';
import Button from 'src/components/Button';
import withToasts from 'src/components/MessageToasts/withToasts';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { exportChart } from 'src/explore/exploreUtils';
import downloadAsImage from 'src/utils/downloadAsImage';
import { getChartPermalink } from 'src/utils/urlUtils';
import copyTextToClipboard from 'src/utils/copy';
import HeaderReportDropDown from 'src/components/ReportModal/HeaderReportDropdown';
import ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import copyTextToClipboard from '../../../utils/copy';
const propTypes = {
onOpenPropertiesModal: PropTypes.func,
onOpenInEditor: PropTypes.func,
latestQueryFormData: PropTypes.object.isRequired,
slice: PropTypes.object,
canDownloadCSV: PropTypes.bool,
canAddReports: PropTypes.bool,
};
const MENU_KEYS = {
EDIT_PROPERTIES: 'edit_properties',
@ -101,29 +89,29 @@ export const MenuTrigger = styled(Button)`
`}
`;
const ExploreAdditionalActionsMenu = ({
export const useExploreAdditionalActionsMenu = (
latestQueryFormData,
canDownloadCSV,
addDangerToast,
addSuccessToast,
slice,
onOpenInEditor,
onOpenPropertiesModal,
}) => {
) => {
const theme = useTheme();
const { addDangerToast, addSuccessToast } = useToasts();
const [showReportSubMenu, setShowReportSubMenu] = useState(null);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [openSubmenus, setOpenSubmenus] = useState([]);
const chart = useSelector(state => {
if (!state.charts) {
const charts = useSelector(state => state.charts);
const chart = useMemo(() => {
if (!charts) {
return undefined;
}
const charts = Object.values(state.charts);
if (charts.length > 0) {
return charts[0];
const chartsValues = Object.values(charts);
if (chartsValues.length > 0) {
return chartsValues[0];
}
return undefined;
});
}, [charts]);
const { datasource } = latestQueryFormData;
const sqlSupported = datasource && datasource.split('__')[1] === 'table';
@ -258,157 +246,144 @@ const ExploreAdditionalActionsMenu = ({
],
);
return (
<>
<AntdDropdown
trigger="click"
data-test="query-dropdown"
visible={isDropdownVisible}
onVisibleChange={setIsDropdownVisible}
overlay={
<Menu
onClick={handleMenuClick}
selectable={false}
openKeys={openSubmenus}
onOpenChange={setOpenSubmenus}
>
{slice && (
<>
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
{t('Edit chart properties')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.SubMenu
title={t('Download')}
key={MENU_KEYS.DOWNLOAD_SUBMENU}
>
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
<>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to original .CSV')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to pivoted .CSV')}
</Menu.Item>
</>
) : (
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to .CSV')}
</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.EXPORT_TO_JSON} icon={<FileOutlined />}>
{t('Export to .JSON')}
const menu = useMemo(
() => (
<Menu
onClick={handleMenuClick}
selectable={false}
openKeys={openSubmenus}
onOpenChange={setOpenSubmenus}
>
{slice && (
<>
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
{t('Edit chart properties')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.SubMenu title={t('Download')} key={MENU_KEYS.DOWNLOAD_SUBMENU}>
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
<>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to original .CSV')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
icon={<FileImageOutlined />}
key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Download as image')}
{t('Export to pivoted .CSV')}
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
<Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
{t('Copy permalink to clipboard')}
</Menu.Item>
<Menu.Item key={MENU_KEYS.SHARE_BY_EMAIL}>
{t('Share chart by email')}
</Menu.Item>
<Menu.Item key={MENU_KEYS.EMBED_CODE}>
<ModalTrigger
triggerNode={
<span data-test="embed-code-button">{t('Embed code')}</span>
}
modalTitle={t('Embed code')}
modalBody={
<EmbedCodeContent
formData={latestQueryFormData}
addDangerToast={addDangerToast}
/>
}
maxWidth={`${theme.gridUnit * 100}px`}
destroyOnClose
responsive
</>
) : (
<Menu.Item
key={MENU_KEYS.EXPORT_TO_CSV}
icon={<FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to .CSV')}
</Menu.Item>
)}
<Menu.Item key={MENU_KEYS.EXPORT_TO_JSON} icon={<FileOutlined />}>
{t('Export to .JSON')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
icon={<FileImageOutlined />}
>
{t('Download as image')}
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
<Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
{t('Copy permalink to clipboard')}
</Menu.Item>
<Menu.Item key={MENU_KEYS.SHARE_BY_EMAIL}>
{t('Share chart by email')}
</Menu.Item>
<Menu.Item key={MENU_KEYS.EMBED_CODE}>
<ModalTrigger
triggerNode={
<span data-test="embed-code-button">{t('Embed code')}</span>
}
modalTitle={t('Embed code')}
modalBody={
<EmbedCodeContent
formData={latestQueryFormData}
addDangerToast={addDangerToast}
/>
</Menu.Item>
}
maxWidth={`${theme.gridUnit * 100}px`}
destroyOnClose
responsive
/>
</Menu.Item>
</Menu.SubMenu>
<Menu.Divider />
{showReportSubMenu ? (
<>
<Menu.SubMenu title={t('Manage email report')}>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
showReportSubMenu={showReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu.SubMenu>
<Menu.Divider />
{showReportSubMenu ? (
<>
<Menu.SubMenu title={t('Manage email report')}>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
showReportSubMenu={showReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu.SubMenu>
<Menu.Divider />
</>
) : (
<Menu>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu>
)}
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
<ModalTrigger
triggerNode={
<span data-test="view-query-menu-item">
{t('View query')}
</span>
}
modalTitle={t('View query')}
modalBody={
<ViewQueryModal latestQueryFormData={latestQueryFormData} />
}
draggable
resizable
responsive
/>
</Menu.Item>
{sqlSupported && (
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
{t('Run in SQL Lab')}
</Menu.Item>
)}
</>
) : (
<Menu>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu>
}
>
<MenuTrigger
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
>
<Icons.MoreHoriz
iconColor={theme.colors.primary.dark2}
iconSize="l"
)}
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
<ModalTrigger
triggerNode={
<span data-test="view-query-menu-item">{t('View query')}</span>
}
modalTitle={t('View query')}
modalBody={
<ViewQueryModal latestQueryFormData={latestQueryFormData} />
}
draggable
resizable
responsive
/>
</MenuTrigger>
</AntdDropdown>
</>
</Menu.Item>
{sqlSupported && (
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
{t('Run in SQL Lab')}
</Menu.Item>
)}
</Menu>
),
[
addDangerToast,
canDownloadCSV,
chart,
handleMenuClick,
isDropdownVisible,
latestQueryFormData,
openSubmenus,
showReportSubMenu,
slice,
sqlSupported,
theme.gridUnit,
],
);
return [menu, isDropdownVisible, setIsDropdownVisible];
};
ExploreAdditionalActionsMenu.propTypes = propTypes;
export default withToasts(ExploreAdditionalActionsMenu);