mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
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:
parent
b53daa91ec
commit
1cd002e801
@ -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)',
|
||||||
|
@ -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();
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
});
|
152
superset-frontend/src/components/PageHeaderWithActions/index.tsx
Normal file
152
superset-frontend/src/components/PageHeaderWithActions/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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};
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
|
Loading…
Reference in New Issue
Block a user