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.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(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
cy.get( cy.get(
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)', '.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',

View File

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

View File

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

View File

@ -73,4 +73,5 @@ export {
export type { FormInstance } from 'antd/lib/form'; export type { FormInstance } from 'antd/lib/form';
export type { ListItemProps } from 'antd/lib/list'; export type { ListItemProps } from 'antd/lib/list';
export type { ModalProps as AntdModalProps } from 'antd/lib/modal'; 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'; 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 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 userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock'; 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 '.'; import ExploreHeader from '.';
const chartEndpoint = 'glob:*api/v1/chart/*'; const chartEndpoint = 'glob:*api/v1/chart/*';
@ -30,6 +33,7 @@ fetchMock.get(chartEndpoint, { json: 'foo' });
const createProps = () => ({ const createProps = () => ({
chart: { chart: {
id: 1,
latestQueryFormData: { latestQueryFormData: {
viz_type: 'histogram', viz_type: 'histogram',
datasource: '49__table', datasource: '49__table',
@ -88,17 +92,29 @@ const createProps = () => ({
}, },
slice_name: 'Age distribution of respondents', slice_name: 'Age distribution of respondents',
actions: { actions: {
postChartFormData: () => null, postChartFormData: jest.fn(),
updateChartTitle: () => null, updateChartTitle: jest.fn(),
fetchFaveStar: () => null, fetchFaveStar: jest.fn(),
saveFaveStar: () => null, saveFaveStar: jest.fn(),
redirectSQLLab: jest.fn(),
}, },
user: { user: {
userId: 1, userId: 1,
}, },
onSaveChart: jest.fn(), 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', () => { test('Cancelling changes to the properties should reset previous properties', () => {
const props = createProps(); const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true }); render(<ExploreHeader {...props} />, { useRedux: true });
@ -136,3 +152,208 @@ test('Save disabled', () => {
userEvent.click(screen.getByText('Save')); userEvent.click(screen.getByText('Save'));
expect(props.onSaveChart).not.toHaveBeenCalled(); 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 * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React from 'react'; import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -30,14 +30,12 @@ import {
import { toggleActive, deleteActiveReport } from 'src/reports/actions/reports'; import { toggleActive, deleteActiveReport } from 'src/reports/actions/reports';
import { chartPropShape } from 'src/dashboard/util/propShapes'; import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag'; import AlteredSliceTag from 'src/components/AlteredSliceTag';
import FaveStar from 'src/components/FaveStar';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal'; import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions'; import { sliceUpdated } from 'src/explore/actions/exploreActions';
import CertifiedBadge from 'src/components/CertifiedBadge'; import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu'; import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
import { ChartEditableTitle } from './ChartEditableTitle';
const propTypes = { const propTypes = {
actions: PropTypes.object.isRequired, actions: PropTypes.object.isRequired,
@ -48,7 +46,7 @@ const propTypes = {
slice: PropTypes.object, slice: PropTypes.object,
sliceName: PropTypes.string, sliceName: PropTypes.string,
table_name: PropTypes.string, table_name: PropTypes.string,
form_data: PropTypes.object, formData: PropTypes.object,
ownState: PropTypes.object, ownState: PropTypes.object,
timeout: PropTypes.number, timeout: PropTypes.number,
chart: chartPropShape, chart: chartPropShape,
@ -62,70 +60,25 @@ const saveButtonStyles = theme => css`
} }
`; `;
const headerStyles = theme => css` export const ExploreChartHeader = ({
display: flex; dashboardId,
flex-direction: row; slice,
align-items: center; actions,
flex-wrap: nowrap; formData,
justify-content: space-between; chart,
height: 100%; user,
canOverwrite,
canDownload,
isStarred,
sliceUpdated,
sliceName,
onSaveChart,
saveDisabled,
}) => {
const { latestQueryFormData, sliceFormData } = chart;
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
span[role='button'] { const fetchChartDashboardData = async () => {
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;
await SupersetClient.get({ await SupersetClient.get({
endpoint: `/api/v1/chart/${slice.slice_id}`, endpoint: `/api/v1/chart/${slice.slice_id}`,
}) })
@ -162,96 +115,71 @@ export class ExploreChartHeader extends React.PureComponent {
} }
}) })
.catch(() => {}); .catch(() => {});
} };
postChartFormData() { useEffect(() => {
this.props.actions.postChartFormData( if (dashboardId) {
this.props.form_data, fetchChartDashboardData();
true, }
this.props.timeout, }, []);
this.props.chart.id,
this.props.ownState,
);
}
openPropertiesModal() { const openPropertiesModal = () => {
this.setState({ setIsPropertiesModalOpen(true);
isPropertiesModalOpen: true, };
});
}
closePropertiesModal() { const closePropertiesModal = () => {
this.setState({ setIsPropertiesModalOpen(false);
isPropertiesModalOpen: false, };
});
}
render() { const [menu, isDropdownVisible, setIsDropdownVisible] =
const { useExploreAdditionalActionsMenu(
actions, latestQueryFormData,
chart,
user,
formData,
slice,
canOverwrite,
canDownload, canDownload,
isStarred, slice,
sliceUpdated, actions.redirectSQLLab,
sliceName, openPropertiesModal,
onSaveChart, );
saveDisabled,
} = this.props; const oldSliceName = slice?.slice_name;
const { latestQueryFormData, sliceFormData } = chart; return (
const oldSliceName = slice?.slice_name; <>
return ( <PageHeaderWithActions
<div id="slice-header" css={headerStyles}> editableTitleProps={{
<div className="title-panel"> title: sliceName,
<ChartEditableTitle canEdit:
title={sliceName} !slice ||
canEdit={ canOverwrite ||
!slice || (slice?.owners || []).includes(user?.userId),
canOverwrite || onSave: actions.updateChartTitle,
(slice?.owners || []).includes(user?.userId) placeholder: t('Add the name of the chart'),
} label: t('Chart title'),
onSave={actions.updateChartTitle} }}
placeholder={t('Add the name of the chart')} showTitlePanelItems={!!slice}
/> certificatiedBadgeProps={{
{slice && ( certifiedBy: slice?.certified_by,
<span css={buttonsStyles}> details: slice?.certification_details,
{slice.certified_by && ( }}
<CertifiedBadge showFaveStar={!!user?.userId}
certifiedBy={slice.certified_by} faveStarProps={{
details={slice.certification_details} itemId: slice?.slice_id,
/> fetchFaveStar: actions.fetchFaveStar,
)} saveFaveStar: actions.saveFaveStar,
{user.userId && ( isStarred,
<FaveStar showTooltip: true,
itemId={slice.slice_id} }}
fetchFaveStar={actions.fetchFaveStar} titlePanelAdditionalItems={
saveFaveStar={actions.saveFaveStar} sliceFormData ? (
isStarred={isStarred} <AlteredSliceTag
showTooltip className="altered"
/> origFormData={{
)} ...sliceFormData,
{this.state.isPropertiesModalOpen && ( chartTitle: oldSliceName,
<PropertiesModal }}
show={this.state.isPropertiesModalOpen} currentFormData={{ ...formData, chartTitle: sliceName }}
onHide={this.closePropertiesModal} />
onSave={sliceUpdated} ) : null
slice={slice} }
/> rightPanelAdditionalItems={
)}
{sliceFormData && (
<AlteredSliceTag
className="altered"
origFormData={{ ...sliceFormData, chartTitle: oldSliceName }}
currentFormData={{ ...formData, chartTitle: sliceName }}
/>
)}
</span>
)}
</div>
<div className="right-button-panel">
<Tooltip <Tooltip
title={ title={
saveDisabled 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 */} {/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
<div css={saveButtonContainerStyles}> <div>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
onClick={onSaveChart} onClick={onSaveChart}
@ -273,18 +201,24 @@ export class ExploreChartHeader extends React.PureComponent {
</Button> </Button>
</div> </div>
</Tooltip> </Tooltip>
<ExploreAdditionalActionsMenu }
onOpenInEditor={actions.redirectSQLLab} additionalActionsMenu={menu}
onOpenPropertiesModal={this.openPropertiesModal} menuDropdownProps={{
slice={slice} visible: isDropdownVisible,
canDownloadCSV={canDownload} onVisibleChange: setIsDropdownVisible,
latestQueryFormData={latestQueryFormData} }}
/> />
</div> {isPropertiesModalOpen && (
</div> <PropertiesModal
); show={isPropertiesModalOpen}
} onHide={closePropertiesModal}
} onSave={sliceUpdated}
slice={slice}
/>
)}
</>
);
};
ExploreChartHeader.propTypes = propTypes; ExploreChartHeader.propTypes = propTypes;

View File

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

View File

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