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

            Line data    Source code
       1              : import * as React from 'react';
       2              : import type * as dashql from '@ankoh/dashql-core';
       3              : import type { EditorView } from '@codemirror/view';
       4              : 
       5              : import { useAppConfig } from '../../config/app_config.js';
       6              : import { ConnectionHealth, type ConnectionState } from '../../connections/connection_state.js';
       7              : import { ConnectorType } from '../../connections/connector_info.js';
       8              : import { useQueryExecutor, useQueryState } from '../../connections/query_executor.js';
       9              : import { useLogger } from '../../platform/logger/logger_provider.js';
      10              : import { useStorageReader } from '../../storage/storage_provider.js';
      11              : import { displayPath } from '../../storage/session_locator.js';
      12              : import { STORAGE_SCRIPTS_FOLDER } from '../../storage/storage_backend.js';
      13              : import { useConnectionLabeler } from '../connection_labels.js';
      14              : import { DELETE_SCRIPT, SET_SCRIPT_TEXT, SET_SCRIPT_VIS_SPEC, type ScriptData, type SessionScripts } from '../scripts/session_scripts.js';
      15              : import type { ModifySessionScripts } from '../scripts/session_scripts_registry.js';
      16              : import { runSessionScript } from '../scripts/rerun_query.js';
      17              : import { useSessionExecution } from '../scripts/session_execution_provider.js';
      18              : import { renameScript, type RenameDraftReferences } from '../scripts/script_rename.js';
      19              : import { formatScriptEditor, formattingDialectForConnector, getKeywordCase, isScriptFormattable } from '../scripts/script_format.js';
      20              : import type { ScriptStorage } from '../scripts/script_action_menu.js';
      21              : import { scriptConnectionView, usePickScriptConnection } from '../scripts/script_connection_picker.js';
      22              : import type { ScriptReferenceReport } from '../scripts/script_reference_check.js';
      23              : import { FeedEntryHeader } from './feed_entry_header.js';
      24              : 
      25              : export interface ScriptEntryHeaderProps {
      26              :     sessionScripts: SessionScripts;
      27              :     modifySessionScripts: ModifySessionScripts;
      28              :     scriptKey: number;
      29              :     /// The connections that the scripts run on
      30              :     scriptConnections: ReadonlyMap<string, ConnectionState>;
      31              :     /// The checked script references of the script
      32              :     scriptReferences: ScriptReferenceReport | null;
      33              :     /// The function that renames the references in the draft of the composer
      34              :     draftRenameRef?: React.RefObject<RenameDraftReferences | null>;
      35              :     /// The editor of the script, formatting needs it
      36              :     editorView?: EditorView | null;
      37              :     /// Delete the script, the session deletes it by default
      38              :     onDelete?: (script: ScriptData) => void;
      39              :     /// Show the snippets button, true by default. The boxes of the dataflow leave it to the editor dialog.
      40              :     snippets?: boolean;
      41              :     /// Open the script in an editor, an edit button left of the script menu offers it
      42              :     onEdit?: () => void;
      43              :     className?: string;
      44              : }
      45              : 
      46              : /// The header of a feed entry for a script of a session, outside of the feed: in the boxes of the dataflow and in
      47              : /// the script editor dialog. The header runs, renames, formats and deletes the script itself.
      48            1 : export const ScriptEntryHeader: React.FC<ScriptEntryHeaderProps> = React.memo((props: ScriptEntryHeaderProps) => {
      49            1 :     const { sessionScripts, modifySessionScripts } = props;
      50            1 :     const sessionId = sessionScripts.sessionId;
      51            1 :     const scriptData = sessionScripts.scripts[props.scriptKey];
      52            1 :     const fileName = scriptData?.fileName ?? 'script.sql';
      53            1 :     const config = useAppConfig();
      54            1 :     const logger = useLogger();
      55            1 :     const executeQuery = useQueryExecutor();
      56            1 :     const storageReader = useStorageReader();
      57            1 :     const sessionExecution = useSessionExecution(sessionId);
      58            1 :     const queryState = useQueryState(sessionId, scriptData?.latestQueryId ?? null);
      59              : 
      60              :     // The connection of the script, labeled like the feed labels it
      61            1 :     const labeler = useConnectionLabeler();
      62            1 :     const connectionState = scriptData != null ? props.scriptConnections.get(scriptData.connection.connectionId) ?? null : null;
      63            1 :     const connection = React.useMemo(
      64            1 :         () => connectionState == null ? null : scriptConnectionView(connectionState, labeler(connectionState)),
      65            1 :         [connectionState, labeler],
      66            1 :     );
      67            1 :     const online = connectionState?.connectionHealth === ConnectionHealth.ONLINE;
      68            1 :     const pickConnection = usePickScriptConnection(sessionId, modifySessionScripts);
      69              : 
      70            1 :     const scriptDebugMode = config?.settings?.scriptDebugMode ?? false;
      71            1 :     const formattingDebugMode = config?.settings?.formattingDebugMode ?? false;
      72            1 :     const keywordCase = getKeywordCase(config?.settings);
      73            1 :     const connectorType = scriptData?.connection.connectorInfo.connectorType;
      74            1 :     const formattingDialect = formattingDialectForConnector(connectorType);
      75            1 :     const isFormattable = React.useMemo(
      76            1 :         () => isScriptFormattable(scriptData ?? null, formattingDialect),
      77            1 :         [scriptData?.scriptSession, scriptData?.editorUpdate?.textVersion, formattingDialect],
      78            1 :     );
      79            1 :     const editorView = props.editorView ?? null;
      80            1 :     const format = React.useCallback((mode: dashql.buffers.formatting.FormattingMode) => {
      81            0 :         formatScriptEditor(editorView, scriptData ?? null, mode, (text) => {
      82            0 :             modifySessionScripts({ type: SET_SCRIPT_TEXT, value: { scriptKey: props.scriptKey, text, withDiff: true } });
      83            0 :         }, formattingDebugMode, keywordCase, formattingDialect);
      84            1 :     }, [editorView, scriptData, props.scriptKey, modifySessionScripts, formattingDebugMode, keywordCase, formattingDialect]);
      85              : 
      86            1 :     const execute = React.useCallback(() => {
      87            0 :         if (scriptData == null || !online || sessionExecution.runActive) return;
      88            0 :         runSessionScript(sessionScripts, scriptData, executeQuery, modifySessionScripts, logger);
      89            1 :     }, [scriptData, online, sessionExecution.runActive, sessionScripts, executeQuery, modifySessionScripts, logger]);
      90            1 :     const rename = React.useCallback((name: string) => {
      91            0 :         void renameScript(sessionScripts, modifySessionScripts, fileName, name, props.draftRenameRef?.current);
      92            1 :     }, [sessionScripts, modifySessionScripts, fileName, props.draftRenameRef]);
      93            1 :     const remove = React.useCallback(() => {
      94            0 :         if (scriptData == null) return;
      95            0 :         if (props.onDelete != null) props.onDelete(scriptData);
      96            0 :         else modifySessionScripts({ type: DELETE_SCRIPT, value: fileName });
      97            1 :     }, [scriptData, props.onDelete, modifySessionScripts, fileName]);
      98              :     // A snippet replaces the text of the script, a script with text shows the replacement as a diff to accept or reject
      99            1 :     const applySnippet = React.useCallback((text: string) => {
     100            0 :         const priorText = scriptData?.scriptSession.getText() ?? '';
     101            0 :         modifySessionScripts({ type: SET_SCRIPT_TEXT, value: { scriptKey: props.scriptKey, text, withDiff: priorText.trim().length > 0 } });
     102            1 :     }, [scriptData, props.scriptKey, modifySessionScripts]);
     103            1 :     const setVisSpec = React.useCallback((spec: string) => {
     104            0 :         modifySessionScripts({ type: SET_SCRIPT_VIS_SPEC, value: { scriptKey: props.scriptKey, spec } });
     105            1 :     }, [props.scriptKey, modifySessionScripts]);
     106            1 :     const storage = React.useMemo<ScriptStorage>(() => ({
     107            1 :         fileName,
     108            1 :         resolvePath: () => `${displayPath(sessionId, storageReader.getSessionLocation(sessionId))}/${STORAGE_SCRIPTS_FOLDER}/${fileName}`,
     109            1 :         onRename: rename,
     110            1 :     }), [fileName, sessionId, storageReader, rename]);
     111              : 
     112            1 :     return (
     113            1 :         <FeedEntryHeader
     114            1 :             sessionId={sessionId}
     115            1 :             scriptData={scriptData}
     116            1 :             scriptFileName={fileName}
     117            1 :             connection={connection}
     118            1 :             connectorType={connectorType ?? ConnectorType.HYPER}
     119            1 :             queryState={queryState}
     120            1 :             focused
     121            1 :             canExecute={scriptData != null && online}
     122            1 :             scriptDebugMode={scriptDebugMode}
     123            1 :             isFormattable={isFormattable}
     124            1 :             canFormat={editorView != null}
     125            1 :             scriptReferences={props.scriptReferences}
     126              :             // There is no composer outside of the feed
     127            1 :             canTarget={false}
     128            1 :             onTarget={NO_TARGET}
     129            1 :             onExecute={execute}
     130            1 :             onPickConnection={connectionId => pickConnection(fileName, connectionId)}
     131            1 :             onRename={rename}
     132            1 :             onDelete={remove}
     133            1 :             onEdit={props.onEdit}
     134            1 :             onFormat={format}
     135            1 :             onApplySnippet={props.snippets === false ? undefined : applySnippet}
     136            1 :             onSetVisSpec={setVisSpec}
     137            1 :             storage={storage}
     138            1 :             className={props.className}
     139            1 :         />
     140              :     );
     141            1 : });
     142              : 
     143            1 : const NO_TARGET = () => {};
        

Generated by: LCOV version 2.0-1