LCOV - code coverage report
Current view: top level - dashql-app/src/session/feed - session_data_card.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 94.9 % 79 75
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './session_feed.module.css';
       3              : import type { ConnectionState } from '../../connections/connection_state.js';
       4              : import { connectionReadsAttachments } from '../../connections/connector_descriptor.js';
       5              : import type { DataFile } from '../../storage/storage_backend.js';
       6              : import type { StorageReader } from '../../storage/storage_provider.js';
       7              : import { useFileDownloader } from '../../platform/file/file_downloader_hook.js';
       8              : import { useLogger } from '../../platform/logger/logger_provider.js';
       9              : import { stringifyError } from '../../platform/logger/logger.js';
      10              : import { ButtonSize, ButtonVariant, IconButton } from '../../ui/foundations/button.js';
      11              : import { DownloadIcon, FileIcon, PencilIcon, TrashIcon } from '../../ui/foundations/symbol_icon.js';
      12              : import { ScriptMentionTag } from './script_mention_tag.js';
      13              : import { formatBytes } from '../../utils/format.js';
      14              : import { attachmentParameterName } from '../attachments/session_attachments.js';
      15              : 
      16              : export interface SessionDataCardProps {
      17              :     sessionId: string;
      18              :     file: DataFile;
      19              :     conn: ConnectionState | null;
      20              :     storageReader: StorageReader;
      21              :     onRemoved: () => void;
      22              :     /// Insert the table parameter of the attachment, `:sales`, where the user last wrote
      23              :     onMention?: (name: string) => void;
      24              :     /// Rename the attachment, the name is the new name of its table parameter
      25              :     onRename?: (name: string) => void;
      26              :     /// Is the name of the table parameter the name of a script too? A table parameter with the name is ambiguous.
      27              :     collides?: boolean;
      28              : }
      29              : 
      30            1 : export const SessionDataCard: React.FC<SessionDataCardProps> = (props) => {
      31            1 :     const downloader = useFileDownloader();
      32            1 :     const logger = useLogger();
      33            1 :     const [busy, setBusy] = React.useState(false);
      34            1 :     const [renaming, setRenaming] = React.useState(false);
      35            1 :     const [error, setError] = React.useState<string | null>(null);
      36            1 :     const enabled = connectionReadsAttachments(props.conn?.details);
      37            1 :     const disabled = !enabled || busy;
      38              :     // An attachment is the table parameter of its name, scripts read `sales.parquet` as `:sales`
      39            1 :     const parameter = attachmentParameterName(props.file.name) ?? props.file.name;
      40              : 
      41            1 :     const download = async () => {
      42            1 :         if (disabled) return;
      43            1 :         setBusy(true);
      44            1 :         setError(null);
      45            1 :         try {
      46            1 :             const bytes = await props.storageReader.backend.loadDataFile(props.sessionId, props.file.name);
      47            1 :             await downloader.downloadBufferAsFile(bytes, props.file.name);
      48            1 :         } catch (e) {
      49            0 :             setError('Downloading data file failed');
      50            0 :             logger.error('Downloading data file failed', { error: stringifyError(e) }, 'session_data_card');
      51            1 :         } finally {
      52            1 :             setBusy(false);
      53            1 :         }
      54            1 :     };
      55            1 :     const remove = async () => {
      56            1 :         if (disabled) return;
      57            1 :         setBusy(true);
      58            1 :         setError(null);
      59            1 :         try {
      60            1 :             await props.storageReader.backend.deleteDataFile(props.sessionId, props.file.name);
      61            1 :             props.onRemoved();
      62            1 :         } catch (e) {
      63            0 :             setError('Removing data file failed');
      64            0 :             logger.error('Removing data file failed', { error: stringifyError(e) }, 'session_data_card');
      65            1 :         } finally {
      66            1 :             setBusy(false);
      67            1 :         }
      68            1 :     };
      69              : 
      70            1 :     return (
      71            1 :         <article className={styles.feed_data_card} aria-label={`${props.file.name} attachment`} data-electron-drag-region="false">
      72            1 :             <div className={styles.feed_data_card_topline}>
      73            1 :                 <div className={styles.feed_data_file_icon} aria-hidden="true" data-testid="data-file-type">
      74            1 :                     <FileIcon size={24} />
      75            1 :                     <span>PARQUET</span>
      76            1 :                 </div>
      77            1 :                 <div className={styles.feed_data_title}>
      78            1 :                     <span className={styles.feed_data_name}>{props.file.name}</span>
      79            1 :                     <span className={styles.feed_data_size}>{formatBytes(props.file.sizeBytes)}</span>
      80            1 :                 </div>
      81            1 :                 <div className={styles.feed_data_actions}>
      82              :                     {/* The table parameter of the attachment, mentioned and renamed like a script */}
      83            1 :                     <ScriptMentionTag
      84            1 :                         scriptName={parameter}
      85            1 :                         subject="attachment"
      86            1 :                         canTarget={!disabled && props.onMention != null}
      87            1 :                         onTarget={() => props.onMention?.(parameter)}
      88            1 :                         renaming={renaming}
      89            1 :                         onRenamingChange={setRenaming}
      90            1 :                         onRename={name => props.onRename?.(name)}
      91            1 :                     />
      92            1 :                     {props.onRename != null && (
      93            1 :                         <IconButton variant={ButtonVariant.Invisible} size={ButtonSize.Medium} disabled={disabled}
      94              :                             // The input takes the focus, a click while it is open keeps the edit
      95            1 :                             onMouseDown={(event) => event.preventDefault()}
      96            1 :                             onClick={() => setRenaming(true)} aria-label={`Rename ${props.file.name}`} data-testid="data-file-rename">
      97            1 :                             <PencilIcon size={16} />
      98            1 :                         </IconButton>
      99              :                     )}
     100            1 :                     <IconButton variant={ButtonVariant.Invisible} size={ButtonSize.Medium}
     101            1 :                         disabled={disabled} onClick={download} aria-label={`Download ${props.file.name}`} data-testid="data-file-download">
     102            1 :                         <DownloadIcon size={16} />
     103            1 :                     </IconButton>
     104            1 :                     <IconButton variant={ButtonVariant.Invisible} size={ButtonSize.Medium}
     105            1 :                         disabled={disabled} onClick={remove} aria-label={`Remove ${props.file.name}`} data-testid="data-file-remove">
     106            1 :                         <TrashIcon size={16} />
     107            1 :                     </IconButton>
     108            1 :                 </div>
     109            1 :             </div>
     110            1 :             {props.collides && (
     111            1 :                 <div role="status" data-testid="data-file-collision">A script is named {parameter} too, :{parameter} is ambiguous</div>
     112              :             )}
     113            1 :             {error != null && <div role="alert">{error}</div>}
     114            1 :         </article>
     115              :     );
     116            1 : };
        

Generated by: LCOV version 2.0-1