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