LCOV - code coverage report
Current view: top level - dashql-app/src/session/persistence - session_file_save_overlay.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 99.0 % 104 103
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './session_file_save_overlay.module.css';
       3              : 
       4              : import { DownloadIcon, FileIcon } from '../../ui/foundations/symbol_icon.js';
       5              : 
       6              : import { AnchorAlignment } from '../../ui/foundations/anchored_position.js';
       7              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
       8              : import { ConnectionState } from '../../connections/connection_state.js';
       9              : import { SessionExportSettings, SessionExportSettingsView } from './session_export_settings_view.js';
      10              : import { SessionScripts } from '../scripts/session_scripts.js';
      11              : import { classNames } from '../../utils/classnames.js';
      12              : import { exportSessionAsSharedZip } from './session_export.js';
      13              : import { connectionParamsHaveLoginHint, getConnectionParamsFromStateDetails } from '../../connections/connection_params.js';
      14              : import { formatBytes } from '../../utils/format.js';
      15              : import { useFileDownloader } from '../../platform/file/file_downloader_hook.js';
      16              : import { useStorageReader } from '../../storage/storage_provider.js';
      17              : import { StorageBackend } from '../../storage/storage_backend.js';
      18              : import { IconButton } from '../../ui/foundations/button.js';
      19              : import { DASHQL_ARCHIVE_FILENAME_EXT } from '../../globals.js';
      20              : import { resolveSessionConnections, useConnectionReferences, useConnectionRegistry, useSessionConnections } from '../../connections/connection_registry.js';
      21              : 
      22            1 : async function packAndCompressFile(
      23            1 :     backend: StorageBackend,
      24            1 :     databases: ConnectionState[],
      25            1 :     sessionScripts: SessionScripts,
      26            1 :     settings: SessionExportSettings,
      27            1 : ): Promise<Uint8Array> {
      28            1 :     const connectionParams = new Map();
      29            1 :     const connectionNames = new Map<string, string>();
      30            1 :     for (const database of databases) {
      31            1 :         const params = database.params;
      32            1 :         if (params != null) connectionParams.set(database.connectionId, params);
      33            1 :         if (database.name != null) connectionNames.set(database.connectionId, database.name);
      34            1 :     }
      35            1 :     const zipBlob = await exportSessionAsSharedZip(backend, sessionScripts.sessionId, connectionParams, { ...settings, connectionNames });
      36            1 :     const arrayBuffer = await zipBlob.arrayBuffer();
      37            1 :     return new Uint8Array(arrayBuffer);
      38            1 : }
      39              : 
      40              : interface Props {
      41              :     className?: string;
      42              :     isOpen: boolean;
      43              :     setIsOpen: (v: boolean) => void;
      44              :     anchorRef?: React.RefObject<HTMLButtonElement | null>;
      45              :     sessionScripts: SessionScripts | null;
      46              :     fileName?: string;
      47              : }
      48              : 
      49            1 : export const SessionFileSaveOverlay: React.FC<Props> = (props: Props) => {
      50            1 :     const anchorRef = React.createRef<HTMLDivElement>();
      51            1 :     const buttonRef = React.createRef<HTMLButtonElement>();
      52            1 :     const fileDownloader = useFileDownloader();
      53            1 :     const storage = useStorageReader();
      54            1 :     const [databaseRegistry] = useConnectionRegistry();
      55            1 :     const references = useConnectionReferences();
      56            1 :     const fileName = `${props.fileName ?? props.sessionScripts?.sessionMetadata.originalFileName ?? "session"}.${DASHQL_ARCHIVE_FILENAME_EXT}`;
      57              : 
      58            1 :     const [settings, setSettings] = React.useState<SessionExportSettings>({
      59            1 :         withCatalog: true,
      60            1 :         withLoginHint: true,
      61            1 :         preserveUUIDs: false,
      62            1 :     });
      63              : 
      64            1 :     const [fileBytes, setFileBytes] = React.useState<Uint8Array>(new Uint8Array());
      65            1 :     React.useEffect(() => {
      66            1 :         if (!props.isOpen) {
      67            1 :             return;
      68              :         }
      69            1 :         const sessionScripts = props.sessionScripts;
      70            1 :         if (sessionScripts == null) {
      71            1 :             return;
      72              :         }
      73            1 :         const cancellation = new AbortController();
      74            1 :         setFileBytes(new Uint8Array());
      75            1 :         const pack = async () => {
      76            1 :             const attached = resolveSessionConnections(databaseRegistry, references, sessionScripts.sessionId);
      77            1 :             if (attached == null) return;
      78            1 :             const fileBytes = await packAndCompressFile(
      79            1 :                 storage.backend,
      80            1 :                 attached,
      81            1 :                 sessionScripts,
      82            1 :                 settings,
      83            1 :             );
      84            1 :             if (!cancellation.signal.aborted) {
      85            1 :                 setFileBytes(fileBytes);
      86              :             }
      87            1 :         };
      88            1 :         pack().catch(() => {
      89            0 :             if (!cancellation.signal.aborted) setFileBytes(new Uint8Array());
      90            1 :         });
      91            1 :         return () => cancellation.abort();
      92            1 :     }, [databaseRegistry, references, settings, props.sessionScripts, props.isOpen, storage.backend]);
      93              : 
      94            1 :     const connections = useSessionConnections(props.sessionScripts?.sessionId ?? null);
      95            1 :     const hasLoginHint = React.useMemo(
      96            1 :         () => (connections ?? []).some(connection => connectionParamsHaveLoginHint(getConnectionParamsFromStateDetails(connection.details))),
      97            1 :         [connections],
      98            1 :     );
      99              : 
     100            1 :     const downloadFile = React.useCallback(async () => {
     101            1 :         await fileDownloader.downloadBufferAsFile(fileBytes, fileName);
     102            1 :     }, [fileBytes, fileName]);
     103              : 
     104            1 :     return (
     105            1 :         <AnchoredOverlay
     106            1 :             renderAnchor={props.anchorRef ? null : () => <div ref={anchorRef} />}
     107            1 :             open={props.isOpen}
     108            1 :             onClose={() => props.setIsOpen(false)}
     109            1 :             anchorRef={props.anchorRef ?? anchorRef}
     110            1 :             returnFocusRef={props.anchorRef}
     111            1 :             align={AnchorAlignment.End}
     112            1 :             overlayProps={{
     113            1 :                 initialFocusRef: buttonRef,
     114            1 :             }}
     115              :         >
     116            1 :             <div className={classNames(styles.overlay, props.className)}>
     117            1 :                 <div className={styles.header}>
     118            1 :                     <div className={styles.file_icon_container}>
     119            1 :                         <FileIcon />
     120            1 :                     </div>
     121            1 :                     <div className={styles.file_info}>
     122            1 :                         <div className={styles.file_name}>{fileName}</div>
     123            1 :                         <div className={styles.file_size}>~&nbsp;{formatBytes(fileBytes.length)}</div>
     124            1 :                     </div>
     125            1 :                     <div className={styles.download}>
     126            1 :                         <IconButton
     127            1 :                             ref={buttonRef}
     128            1 :                             onClick={downloadFile}
     129            1 :                             disabled={fileBytes.length === 0}
     130            1 :                             aria-labelledby="save-file"
     131            1 :                             aria-label="Save File"
     132              :                         >
     133            1 :                             <DownloadIcon />
     134            1 :                         </IconButton>
     135            1 :                     </div>
     136            1 :                 </div>
     137            1 :                 <SessionExportSettingsView
     138            1 :                     withCatalog={true}
     139            1 :                     withLoginHint={hasLoginHint}
     140            1 :                     settings={settings}
     141            1 :                     setSettings={setSettings}
     142            1 :                 />
     143            1 :             </div>
     144            1 :         </AnchoredOverlay>
     145              :     );
     146            1 : };
        

Generated by: LCOV version 2.0-1