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 : };
|