airbnb/caravel

View on GitHub
superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx

Summary

Maintainability
F
4 days
Test Coverage
/**
 * 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 {
  css,
  isFeatureEnabled,
  FeatureFlag,
  styled,
  t,
  useTheme,
} from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Menu } from 'src/components/Menu';
import ModalTrigger from 'src/components/ModalTrigger';
import Button from 'src/components/Button';
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 HeaderReportDropDown 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 ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import DashboardsSubMenu from './DashboardsSubMenu';

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',
};

const VIZ_TYPES_PIVOTABLE = ['pivot_table_v2'];

export const MenuItemWithCheckboxContainer = styled.div`
  ${({ theme }) => css`
    display: flex;
    align-items: center;

    & svg {
      width: ${theme.gridUnit * 3}px;
      height: ${theme.gridUnit * 3}px;
    }

    & span[role='checkbox'] {
      display: inline-flex;
      margin-right: ${theme.gridUnit}px;
    }
  `}
`;

export const MenuTrigger = styled(Button)`
  ${({ theme }) => 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 iconReset = css`
  .ant-dropdown-menu-item > & > .anticon:first-child {
    margin-right: 0;
    vertical-align: 0;
  }
`;

export const useExploreAdditionalActionsMenu = (
  latestQueryFormData,
  canDownloadCSV,
  slice,
  onOpenInEditor,
  onOpenPropertiesModal,
  ownState,
  dashboards,
  ...rest
) => {
  const theme = useTheme();
  const { addDangerToast, addSuccessToast } = useToasts();
  const dispatch = useDispatch();
  const [showReportSubMenu, setShowReportSubMenu] = useState(null);
  const [isDropdownVisible, setIsDropdownVisible] = useState(false);
  const chart = useSelector(
    state => state.charts?.[getChartKey(state.explore)],
  );

  const { datasource } = latestQueryFormData;

  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 handleMenuClick = useCallback(
    ({ key, domEvent }) => {
      switch (key) {
        case MENU_KEYS.EDIT_PROPERTIES:
          onOpenPropertiesModal();
          setIsDropdownVisible(false);
          break;
        case MENU_KEYS.EXPORT_TO_CSV:
          exportCSV();
          setIsDropdownVisible(false);
          dispatch(
            logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV, {
              chartId: slice?.slice_id,
              chartName: slice?.slice_name,
            }),
          );
          break;
        case MENU_KEYS.EXPORT_TO_CSV_PIVOTED:
          exportCSVPivoted();
          setIsDropdownVisible(false);
          dispatch(
            logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED, {
              chartId: slice?.slice_id,
              chartName: slice?.slice_name,
            }),
          );
          break;
        case MENU_KEYS.EXPORT_TO_JSON:
          exportJson();
          setIsDropdownVisible(false);
          dispatch(
            logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_JSON, {
              chartId: slice?.slice_id,
              chartName: slice?.slice_name,
            }),
          );
          break;
        case MENU_KEYS.EXPORT_TO_XLSX:
          exportExcel();
          setIsDropdownVisible(false);
          dispatch(
            logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, {
              chartId: slice?.slice_id,
              chartName: slice?.slice_name,
            }),
          );
          break;
        case MENU_KEYS.DOWNLOAD_AS_IMAGE:
          downloadAsImage(
            '.panel-body .chart-container',
            // eslint-disable-next-line camelcase
            slice?.slice_name ?? t('New chart'),
            true,
          )(domEvent);
          setIsDropdownVisible(false);
          dispatch(
            logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE, {
              chartId: slice?.slice_id,
              chartName: slice?.slice_name,
            }),
          );
          break;
        case MENU_KEYS.COPY_PERMALINK:
          copyLink();
          setIsDropdownVisible(false);
          break;
        case MENU_KEYS.EMBED_CODE:
          setIsDropdownVisible(false);
          break;
        case MENU_KEYS.SHARE_BY_EMAIL:
          shareByEmail();
          setIsDropdownVisible(false);
          break;
        case MENU_KEYS.VIEW_QUERY:
          setIsDropdownVisible(false);
          break;
        case MENU_KEYS.RUN_IN_SQL_LAB:
          onOpenInEditor(latestQueryFormData, domEvent.metaKey);
          setIsDropdownVisible(false);
          break;
        default:
          break;
      }
    },
    [
      copyLink,
      exportCSV,
      exportCSVPivoted,
      exportJson,
      latestQueryFormData,
      onOpenInEditor,
      onOpenPropertiesModal,
      shareByEmail,
      slice?.slice_name,
    ],
  );

  const menu = useMemo(
    () => (
      <Menu onClick={handleMenuClick} selectable={false} {...rest}>
        <>
          {slice && (
            <Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
              {t('Edit chart properties')}
            </Menu.Item>
          )}
          <Menu.SubMenu
            title={t('On dashboards')}
            key={MENU_KEYS.DASHBOARDS_ADDED_TO}
          >
            <DashboardsSubMenu
              chartId={slice?.slice_id}
              dashboards={dashboards}
            />
          </Menu.SubMenu>
          <Menu.Divider />
        </>
        <Menu.SubMenu title={t('Download')} key={MENU_KEYS.DOWNLOAD_SUBMENU}>
          {VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
            <>
              <Menu.Item
                key={MENU_KEYS.EXPORT_TO_CSV}
                icon={<Icons.FileOutlined css={iconReset} />}
                disabled={!canDownloadCSV}
              >
                {t('Export to original .CSV')}
              </Menu.Item>
              <Menu.Item
                key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
                icon={<Icons.FileOutlined css={iconReset} />}
                disabled={!canDownloadCSV}
              >
                {t('Export to pivoted .CSV')}
              </Menu.Item>
            </>
          ) : (
            <Menu.Item
              key={MENU_KEYS.EXPORT_TO_CSV}
              icon={<Icons.FileOutlined css={iconReset} />}
              disabled={!canDownloadCSV}
            >
              {t('Export to .CSV')}
            </Menu.Item>
          )}
          <Menu.Item
            key={MENU_KEYS.EXPORT_TO_JSON}
            icon={<Icons.FileOutlined css={iconReset} />}
            disabled={!canDownloadCSV}
          >
            {t('Export to .JSON')}
          </Menu.Item>
          <Menu.Item
            key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
            icon={<Icons.FileImageOutlined css={iconReset} />}
          >
            {t('Download as image')}
          </Menu.Item>
          <Menu.Item
            key={MENU_KEYS.EXPORT_TO_XLSX}
            icon={<Icons.FileOutlined css={iconReset} />}
            disabled={!canDownloadCSV}
          >
            {t('Export to Excel')}
          </Menu.Item>
        </Menu.SubMenu>
        <Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
          <Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
            {t('Copy permalink to clipboard')}
          </Menu.Item>
          <Menu.Item key={MENU_KEYS.SHARE_BY_EMAIL}>
            {t('Share chart by email')}
          </Menu.Item>
          {isFeatureEnabled(FeatureFlag.EmbeddableCharts) ? (
            <Menu.Item key={MENU_KEYS.EMBED_CODE}>
              <ModalTrigger
                triggerNode={
                  <span data-test="embed-code-button">{t('Embed code')}</span>
                }
                modalTitle={t('Embed code')}
                modalBody={
                  <EmbedCodeContent
                    formData={latestQueryFormData}
                    addDangerToast={addDangerToast}
                  />
                }
                maxWidth={`${theme.gridUnit * 100}px`}
                destroyOnClose
                responsive
              />
            </Menu.Item>
          ) : null}
        </Menu.SubMenu>
        <Menu.Divider />
        {showReportSubMenu ? (
          <>
            <Menu.SubMenu title={t('Manage email report')}>
              <HeaderReportDropDown
                chart={chart}
                setShowReportSubMenu={setShowReportSubMenu}
                showReportSubMenu={showReportSubMenu}
                setIsDropdownVisible={setIsDropdownVisible}
                isDropdownVisible={isDropdownVisible}
                useTextMenu
              />
            </Menu.SubMenu>
            <Menu.Divider />
          </>
        ) : (
          <Menu>
            <HeaderReportDropDown
              chart={chart}
              setShowReportSubMenu={setShowReportSubMenu}
              setIsDropdownVisible={setIsDropdownVisible}
              isDropdownVisible={isDropdownVisible}
              useTextMenu
            />
          </Menu>
        )}
        <Menu.Item key={MENU_KEYS.VIEW_QUERY}>
          <ModalTrigger
            triggerNode={
              <span data-test="view-query-menu-item">{t('View query')}</span>
            }
            modalTitle={t('View query')}
            modalBody={
              <ViewQueryModal latestQueryFormData={latestQueryFormData} />
            }
            draggable
            resizable
            responsive
          />
        </Menu.Item>
        {datasource && (
          <Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
            {t('Run in SQL Lab')}
          </Menu.Item>
        )}
      </Menu>
    ),
    [
      addDangerToast,
      canDownloadCSV,
      chart,
      dashboards,
      handleMenuClick,
      isDropdownVisible,
      latestQueryFormData,
      showReportSubMenu,
      slice,
      theme.gridUnit,
    ],
  );
  return [menu, isDropdownVisible, setIsDropdownVisible];
};