LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_result - query_result_download_button.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 79 79
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : import * as React from 'react';
       3              : import * as styles from './query_result_download_button.module.css';
       4              : 
       5              : import { QueryExecutionStatus, type QueryExecutionState } from '../../query/query_execution_state.js';
       6              : import { ButtonSize, ButtonVariant, IconButton } from '../../ui/foundations/button.js';
       7              : import * as ActionList from '../../ui/foundations/action_list.js';
       8              : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
       9              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
      10              : import { DownloadIcon } from '../../ui/foundations/symbol_icon.js';
      11              : import { useComputeDatabase } from '../../compute/compute_connection_provider.js';
      12              : import { exportArrowTableAsParquet, supportsParquetExport } from '../../platform/hyperdb/hyperdb_parquet_export.js';
      13              : import { useFileDownloader } from '../../platform/file/file_downloader_hook.js';
      14              : import { useLogger } from '../../platform/logger/logger_provider.js';
      15              : import { stringifyError } from '../../platform/logger/logger.js';
      16              : 
      17            1 : const enum DownloadFormat {
      18            1 :     Arrow = 'arrow',
      19            1 :     Parquet = 'parquet',
      20            1 : }
      21              : 
      22              : interface Props {
      23              :     query: QueryExecutionState;
      24              :     dependencies?: {
      25              :         useFileDownloader: () => Pick<ReturnType<typeof useFileDownloader>, 'downloadBufferAsFile'>;
      26              :         useLogger: () => Pick<ReturnType<typeof useLogger>, 'error'>;
      27              :         useComputeDatabase: typeof useComputeDatabase;
      28              :         exportArrowTableAsParquet: typeof exportArrowTableAsParquet;
      29              :     };
      30              : }
      31              : 
      32            1 : const DEFAULT_DEPENDENCIES: NonNullable<Props['dependencies']> = {
      33            1 :     useFileDownloader,
      34            1 :     useLogger,
      35            1 :     useComputeDatabase,
      36            1 :     exportArrowTableAsParquet,
      37            1 : };
      38              : 
      39            1 : export function QueryResultDownloadButton({ query, dependencies = DEFAULT_DEPENDENCIES }: Props) {
      40            1 :     const fileDownloader = dependencies.useFileDownloader();
      41            1 :     const logger = dependencies.useLogger();
      42            1 :     const computeDatabase = dependencies.useComputeDatabase();
      43            1 :     const parquetDatabase = supportsParquetExport(computeDatabase) ? computeDatabase : null;
      44            1 :     const [open, setOpen] = React.useState(false);
      45            1 :     const [downloading, setDownloading] = React.useState(false);
      46            1 :     const triggerRef = React.useRef<HTMLButtonElement>(null);
      47            1 :     const hasResult = query.status === QueryExecutionStatus.SUCCEEDED && query.resultTable != null;
      48              : 
      49            1 :     const downloadResult = async (format: DownloadFormat) => {
      50            1 :         setOpen(false);
      51            1 :         if (!hasResult || downloading) return;
      52            1 :         setDownloading(true);
      53            1 :         try {
      54            1 :             switch (format) {
      55            1 :                 case DownloadFormat.Arrow: {
      56            1 :                     const bytes = arrow.tableToIPC(query.resultTable!, 'file');
      57            1 :                     await fileDownloader.downloadBufferAsFile(bytes, `query-result-${query.queryId}.arrow`);
      58            1 :                     break;
      59            1 :                 }
      60            1 :                 case DownloadFormat.Parquet: {
      61            1 :                     if (parquetDatabase == null) throw new Error('compute database cannot export Parquet');
      62            1 :                     const bytes = await dependencies.exportArrowTableAsParquet(parquetDatabase, query.resultTable!);
      63            1 :                     await fileDownloader.downloadBufferAsFile(bytes, `query-result-${query.queryId}.parquet`);
      64            1 :                     break;
      65            1 :                 }
      66            1 :             }
      67            1 :         } catch (error) {
      68            1 :             logger.error('Failed to download query result', { format, error: stringifyError(error) }, 'query_result_download');
      69            1 :         } finally {
      70            1 :             setDownloading(false);
      71            1 :         }
      72            1 :     };
      73              : 
      74            1 :     return (
      75            1 :         <AnchoredOverlay
      76            1 :             open={open}
      77            1 :             onOpen={() => setOpen(true)}
      78            1 :             onClose={() => setOpen(false)}
      79            1 :             side={AnchorSide.OutsideBottom}
      80            1 :             align={AnchorAlignment.End}
      81            1 :             anchorOffset={4}
      82            1 :             anchorRef={triggerRef}
      83            1 :             returnFocusRef={triggerRef}
      84            1 :             renderAnchor={(anchorProps) => (
      85            1 :                 <IconButton
      86            1 :                     {...anchorProps}
      87            1 :                     ref={triggerRef}
      88            1 :                     variant={ButtonVariant.Invisible}
      89            1 :                     size={ButtonSize.Small}
      90            1 :                     aria-label="Download query result"
      91            1 :                     data-testid="download-query-result"
      92            1 :                     disabled={!hasResult || downloading}
      93              :                 >
      94            1 :                     <DownloadIcon size={16} />
      95            1 :                 </IconButton>
      96              :             )}
      97              :         >
      98            1 :             <div className={styles.menu} role="dialog" aria-label="Download query result">
      99            1 :                 <ActionList.List aria-label="Download formats">
     100            1 :                     <ActionList.ListItem data-testid="download-format-arrow" onClick={() => downloadResult(DownloadFormat.Arrow)}>
     101            1 :                         <ActionList.ItemText>Download as Arrow</ActionList.ItemText>
     102            1 :                     </ActionList.ListItem>
     103            1 :                     <ActionList.ListItem
     104            1 :                         data-testid="download-format-parquet"
     105            1 :                         disabled={parquetDatabase == null}
     106            1 :                         onClick={() => downloadResult(DownloadFormat.Parquet)}
     107              :                     >
     108            1 :                         <ActionList.ItemText>Download as Parquet</ActionList.ItemText>
     109            1 :                     </ActionList.ListItem>
     110            1 :                 </ActionList.List>
     111            1 :             </div>
     112            1 :         </AnchoredOverlay>
     113              :     );
     114            1 : }
        

Generated by: LCOV version 2.0-1