blob: 19278a747b9d25c9853e001d8098c3d7707b60a8 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import {
css,
isFeatureEnabled,
FeatureFlag,
styled,
t,
useTheme,
VizType,
} from '@superset-ui/core';
import {
Icons,
ModalTrigger,
Button,
Input,
} from '@superset-ui/core/components';
import { Menu } from '@superset-ui/core/components/Menu';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { exportChart, getChartKey } from 'src/explore/exploreUtils';
import downloadAsImage from 'src/utils/downloadAsImage';
import { getChartPermalink } from 'src/utils/urlUtils';
import copyTextToClipboard from 'src/utils/copy';
import { useHeaderReportMenuItems } from 'src/features/reports/ReportModal/HeaderReportDropdown';
import { logEvent } from 'src/logger/actions';
import {
LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE,
LOG_ACTIONS_CHART_DOWNLOAD_AS_JSON,
LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV,
LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED,
LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS,
} from 'src/logger/LogUtils';
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
import ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import { useDashboardsMenuItems } from './DashboardsSubMenu';
export const SEARCH_THRESHOLD = 10;
const MENU_KEYS = {
EDIT_PROPERTIES: 'edit_properties',
DASHBOARDS_ADDED_TO: 'dashboards_added_to',
DOWNLOAD_SUBMENU: 'download_submenu',
EXPORT_TO_CSV: 'export_to_csv',
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
EXPORT_TO_JSON: 'export_to_json',
EXPORT_TO_XLSX: 'export_to_xlsx',
DOWNLOAD_AS_IMAGE: 'download_as_image',
SHARE_SUBMENU: 'share_submenu',
COPY_PERMALINK: 'copy_permalink',
EMBED_CODE: 'embed_code',
SHARE_BY_EMAIL: 'share_by_email',
REPORT_SUBMENU: 'report_submenu',
SET_UP_REPORT: 'set_up_report',
SET_REPORT_ACTIVE: 'set_report_active',
EDIT_REPORT: 'edit_report',
DELETE_REPORT: 'delete_report',
VIEW_QUERY: 'view_query',
RUN_IN_SQL_LAB: 'run_in_sql_lab',
EXPORT_TO_PIVOT_XLSX: 'export_to_pivot_xlsx',
};
const VIZ_TYPES_PIVOTABLE = [VizType.PivotTable];
export const MenuItemWithCheckboxContainer = styled.div`
${({ theme }) => css`
display: flex;
align-items: center;
& svg {
width: ${theme.sizeUnit * 3}px;
height: ${theme.sizeUnit * 3}px;
}
& span[role='checkbox'] {
display: inline-flex;
margin-right: ${theme.sizeUnit}px;
}
`}
`;
export const MenuTrigger = styled(Button)`
${({ theme }) => css`
width: ${theme.sizeUnit * 8}px;
height: ${theme.sizeUnit * 8}px;
padding: 0;
border: 1px solid ${theme.colorPrimary};
&.ant-btn > span.anticon {
line-height: 0;
transition: inherit;
}
&:hover:not(:focus) > span.anticon {
color: ${theme.colorPrimary};
}
`}
`;
export const useExploreAdditionalActionsMenu = (
latestQueryFormData,
canDownloadCSV,
slice,
onOpenInEditor,
onOpenPropertiesModal,
ownState,
dashboards,
showReportModal,
setCurrentReportDeleting,
...rest
) => {
const theme = useTheme();
const { addDangerToast, addSuccessToast } = useToasts();
const dispatch = useDispatch();
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [dashboardSearchTerm, setDashboardSearchTerm] = useState('');
const debouncedDashboardSearchTerm = useDebounceValue(
dashboardSearchTerm,
300,
);
const chart = useSelector(
state => state.charts?.[getChartKey(state.explore)],
);
// Use the updated report menu items hook
const reportMenuItem = useHeaderReportMenuItems({
chart,
showReportModal,
setCurrentReportDeleting,
});
const { datasource } = latestQueryFormData;
// Get dashboard menu items using the hook
const dashboardMenuItems = useDashboardsMenuItems({
chartId: slice?.slice_id,
dashboards,
searchTerm: debouncedDashboardSearchTerm,
});
const showDashboardSearch = dashboards?.length > SEARCH_THRESHOLD;
const shareByEmail = useCallback(async () => {
try {
const subject = t('Superset Chart');
const url = await getChartPermalink(latestQueryFormData);
const body = encodeURIComponent(t('%s%s', 'Check out this chart: ', url));
window.location.href = `mailto:?Subject=${subject}%20&Body=${body}`;
} catch (error) {
addDangerToast(t('Sorry, something went wrong. Try again later.'));
}
}, [addDangerToast, latestQueryFormData]);
const exportCSV = useCallback(
() =>
canDownloadCSV
? exportChart({
formData: latestQueryFormData,
ownState,
resultType: 'full',
resultFormat: 'csv',
})
: null,
[canDownloadCSV, latestQueryFormData],
);
const exportCSVPivoted = useCallback(
() =>
canDownloadCSV
? exportChart({
formData: latestQueryFormData,
resultType: 'post_processed',
resultFormat: 'csv',
})
: null,
[canDownloadCSV, latestQueryFormData],
);
const exportJson = useCallback(
() =>
canDownloadCSV
? exportChart({
formData: latestQueryFormData,
resultType: 'results',
resultFormat: 'json',
})
: null,
[canDownloadCSV, latestQueryFormData],
);
const exportExcel = useCallback(
() =>
canDownloadCSV
? exportChart({
formData: latestQueryFormData,
resultType: 'results',
resultFormat: 'xlsx',
})
: null,
[canDownloadCSV, latestQueryFormData],
);
const copyLink = useCallback(async () => {
try {
if (!latestQueryFormData) {
throw new Error();
}
await copyTextToClipboard(() => getChartPermalink(latestQueryFormData));
addSuccessToast(t('Copied to clipboard!'));
} catch (error) {
addDangerToast(t('Sorry, something went wrong. Try again later.'));
}
}, [addDangerToast, addSuccessToast, latestQueryFormData]);
const menu = useMemo(() => {
const menuItems = [];
// Edit chart properties
if (slice) {
menuItems.push({
key: MENU_KEYS.EDIT_PROPERTIES,
label: t('Edit chart properties'),
onClick: () => {
onOpenPropertiesModal();
setIsDropdownVisible(false);
},
});
}
// On dashboards submenu
const dashboardsChildren = [];
// Add search input if needed
if (showDashboardSearch) {
dashboardsChildren.push({
key: 'dashboard-search',
label: (
<Input
allowClear
placeholder={t('Search')}
prefix={<Icons.StarOutlined iconSize="l" />}
css={css`
width: 220px;
margin: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 3}px;
`}
value={dashboardSearchTerm}
onChange={e => setDashboardSearchTerm(e.currentTarget.value)}
onClick={e => e.stopPropagation()}
/>
),
disabled: true, // Prevent clicks on the search input from closing menu
});
}
// Add dashboard items
dashboardMenuItems.forEach(item => {
dashboardsChildren.push(item);
});
menuItems.push({
key: MENU_KEYS.DASHBOARDS_ADDED_TO,
type: 'submenu',
label: t('On dashboards'),
children: dashboardsChildren,
popupStyle: {
maxHeight: '300px',
overflow: 'auto',
},
});
// Divider
menuItems.push({ type: 'divider' });
// Download submenu
const downloadChildren = [];
if (VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type)) {
downloadChildren.push(
{
key: MENU_KEYS.EXPORT_TO_CSV,
label: t('Export to original .CSV'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
exportCSV();
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
{
key: MENU_KEYS.EXPORT_TO_CSV_PIVOTED,
label: t('Export to pivoted .CSV'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
exportCSVPivoted();
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
{
key: MENU_KEYS.EXPORT_TO_PIVOT_XLSX,
label: t('Export to Pivoted Excel'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
const sliceSelector = `#chart-id-${slice?.slice_id}`;
exportPivotExcel(
`${sliceSelector} .pvtTable`,
slice?.slice_name ?? t('pivoted_xlsx'),
);
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
);
} else {
downloadChildren.push({
key: MENU_KEYS.EXPORT_TO_CSV,
label: t('Export to .CSV'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
exportCSV();
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
});
}
downloadChildren.push(
{
key: MENU_KEYS.EXPORT_TO_JSON,
label: t('Export to .JSON'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
exportJson();
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_JSON, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
{
key: MENU_KEYS.DOWNLOAD_AS_IMAGE,
label: t('Download as image'),
icon: <Icons.FileImageOutlined />,
onClick: e => {
downloadAsImage(
'.panel-body .chart-container',
slice?.slice_name ?? t('New chart'),
true,
)(e.domEvent);
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
{
key: MENU_KEYS.EXPORT_TO_XLSX,
label: t('Export to Excel'),
icon: <Icons.FileOutlined />,
disabled: !canDownloadCSV,
onClick: () => {
exportExcel();
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
},
},
);
menuItems.push({
key: MENU_KEYS.DOWNLOAD_SUBMENU,
type: 'submenu',
label: t('Download'),
children: downloadChildren,
});
// Share submenu
const shareChildren = [
{
key: MENU_KEYS.COPY_PERMALINK,
label: t('Copy permalink to clipboard'),
onClick: () => {
copyLink();
setIsDropdownVisible(false);
},
},
{
key: MENU_KEYS.SHARE_BY_EMAIL,
label: t('Share chart by email'),
onClick: () => {
shareByEmail();
setIsDropdownVisible(false);
},
},
];
if (isFeatureEnabled(FeatureFlag.EmbeddableCharts)) {
shareChildren.push({
key: MENU_KEYS.EMBED_CODE,
label: (
<ModalTrigger
triggerNode={
<div data-test="embed-code-button">{t('Embed code')}</div>
}
modalTitle={t('Embed code')}
modalBody={
<EmbedCodeContent
formData={latestQueryFormData}
addDangerToast={addDangerToast}
/>
}
maxWidth={`${theme.sizeUnit * 100}px`}
destroyOnHidden
responsive
/>
),
onClick: () => setIsDropdownVisible(false),
});
}
menuItems.push({
key: MENU_KEYS.SHARE_SUBMENU,
type: 'submenu',
label: t('Share'),
children: shareChildren,
});
// Divider
menuItems.push({ type: 'divider' });
// Report menu item
if (reportMenuItem) {
menuItems.push(reportMenuItem);
}
// View query
menuItems.push({
key: MENU_KEYS.VIEW_QUERY,
label: (
<ModalTrigger
triggerNode={
<div data-test="view-query-menu-item">{t('View query')}</div>
}
modalTitle={t('View query')}
modalBody={
<ViewQueryModal latestQueryFormData={latestQueryFormData} />
}
draggable
resizable
responsive
/>
),
onClick: () => setIsDropdownVisible(false),
});
// Run in SQL Lab
if (datasource) {
menuItems.push({
key: MENU_KEYS.RUN_IN_SQL_LAB,
label: t('Run in SQL Lab'),
onClick: e => {
onOpenInEditor(latestQueryFormData, e.domEvent.metaKey);
setIsDropdownVisible(false);
},
});
}
return <Menu selectable={false} items={menuItems} {...rest} />;
}, [
addDangerToast,
canDownloadCSV,
copyLink,
dashboards,
dashboardMenuItems,
dashboardSearchTerm,
debouncedDashboardSearchTerm,
datasource,
dispatch,
exportCSV,
exportCSVPivoted,
exportExcel,
exportJson,
latestQueryFormData,
onOpenInEditor,
onOpenPropertiesModal,
reportMenuItem,
shareByEmail,
showDashboardSearch,
slice,
theme.sizeUnit,
]);
return [menu, isDropdownVisible, setIsDropdownVisible];
};