LCOV - code coverage report
Current view: top level - dashql-app/src/session/scripts - session_commands.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 71.7 % 127 91
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { ConnectionHealth, printConnectionHealth, type ConnectionState } from '../../connections/connection_state.js';
       4              : import { KeyEventHandler, useKeyEvents } from '../../utils/key_events.js';
       5              : import { getSelectedScriptRef, newScriptConnection, SELECT_NEXT_SCRIPT, SELECT_PREV_SCRIPT, type ScriptData, type SessionScripts } from './session_scripts.js';
       6              : import type { ScriptRef } from './script_types.js';
       7              : import { useCatalogLoaderQueue } from '../../connections/catalog/catalog_loader.js';
       8              : import { useConnectionRegistryStore, useSessionConnections, type ConnectionRegistry } from '../../connections/connection_registry.js';
       9              : import { useLogger } from '../../platform/logger/logger_provider.js';
      10              : import { useQueryExecutor } from '../../connections/query_executor.js';
      11              : import { useRouteContext } from '../../app/router/router.js';
      12              : import { useSessionScripts } from './session_scripts_registry.js';
      13              : import { isCatalogRefreshRunning } from '../../connections/catalog/catalog_update_state.js';
      14              : import { runSessionScript } from './rerun_query.js';
      15              : import { connectionHealthKey, SessionConnectionAttentionContext, useSessionConnectionAttention } from '../session_connection_attention.js';
      16              : import { SessionViewMode, useSessionViewMode } from '../session_view_mode.js';
      17              : 
      18            1 : const LOG_CTX = "session_commands";
      19              : 
      20              : /// The selected script and the connection it runs on, the connection of the next script without a selected script
      21            1 : function selectedScriptConnection(sessionScripts: SessionScripts, registry: ConnectionRegistry): {
      22              :     entry: ScriptRef | undefined;
      23              :     scriptData: ScriptData | undefined;
      24              :     scriptConnection: ConnectionState | null;
      25            1 : } {
      26            1 :     const entry = getSelectedScriptRef(sessionScripts);
      27            1 :     const scriptData = entry != null ? sessionScripts.scripts[entry.scriptId] : undefined;
      28            1 :     const connectionId = scriptData?.connection.connectionId ?? newScriptConnection(sessionScripts, null)?.connectionId;
      29            1 :     const scriptConnection = connectionId == null ? null : registry.connections.get(connectionId) ?? null;
      30            1 :     return { entry, scriptData, scriptConnection };
      31            1 : }
      32              : 
      33            1 : export enum SessionCommandType {
      34            1 :     ExecuteEditorQuery = 1,
      35            1 :     RefreshCatalog = 2,
      36            1 :     SelectPreviousSessionScript = 6,
      37            1 :     SelectNextSessionScript = 7,
      38            1 : }
      39              : 
      40              : export type ScriptCommandDispatch = (command: SessionCommandType) => void;
      41              : 
      42              : interface Props {
      43              :     children?: React.ReactElement | React.ReactElement[];
      44              :     dependencies?: SessionCommandsDependencies;
      45              : }
      46              : 
      47              : export interface SessionCommandsDependencies {
      48              :     useSessionScripts: typeof useSessionScripts;
      49              :     useSessionConnections: typeof useSessionConnections;
      50              :     useConnectionRegistryStore: typeof useConnectionRegistryStore;
      51              :     useQueryExecutor: typeof useQueryExecutor;
      52              :     useCatalogLoaderQueue: typeof useCatalogLoaderQueue;
      53              :     useLogger: typeof useLogger;
      54              :     useSessionViewMode: typeof useSessionViewMode;
      55              :     runSessionScript: typeof runSessionScript;
      56              : }
      57              : 
      58            1 : const DEFAULT_DEPENDENCIES: SessionCommandsDependencies = {
      59            1 :     useSessionScripts, useSessionConnections, useConnectionRegistryStore,
      60            1 :     useQueryExecutor, useCatalogLoaderQueue, useLogger, useSessionViewMode, runSessionScript,
      61            1 : };
      62              : 
      63            1 : const COMMAND_DISPATCH_CTX = React.createContext<ScriptCommandDispatch | null>(null);
      64              : export const useSessionCommandDispatch = () => React.useContext(COMMAND_DISPATCH_CTX)!;
      65              : 
      66            1 : export const SessionCommands: React.FC<Props> = (props: Props) => {
      67            1 :     const dependencies = props.dependencies ?? DEFAULT_DEPENDENCIES;
      68            1 :     const route = useRouteContext();
      69            1 :     const logger = dependencies.useLogger();
      70            1 :     const viewMode = dependencies.useSessionViewMode().mode;
      71              : 
      72            1 :     const [sessionScripts, modifySessionScripts] = dependencies.useSessionScripts(route.sessionId ?? null);
      73            1 :     const connections = dependencies.useSessionConnections(sessionScripts?.sessionId ?? null);
      74            1 :     const connectionRegistryStore = dependencies.useConnectionRegistryStore();
      75            1 :     const connectionAttention = useSessionConnectionAttention(sessionScripts?.sessionId ?? null, connectionHealthKey(connections));
      76            1 :     const executeQuery = dependencies.useQueryExecutor();
      77            1 :     const refreshCatalog = dependencies.useCatalogLoaderQueue();
      78              : 
      79              :     // Setup command dispatch logic.
      80              :     // The session scripts change with every keystroke and the connection with every query event.
      81              :     // Read them through a ref to keep the dispatch (and the context value) stable.
      82            1 :     const latest = React.useRef({ sessionScripts, modifySessionScripts, connectionAttention, executeQuery, refreshCatalog, viewMode, sessionId: route.sessionId });
      83            1 :     latest.current = { sessionScripts, modifySessionScripts, connectionAttention, executeQuery, refreshCatalog, viewMode, sessionId: route.sessionId };
      84            1 :     const commandDispatch = React.useCallback(
      85            1 :         async (command: SessionCommandType) => {
      86            1 :             const { sessionScripts, modifySessionScripts, connectionAttention, executeQuery, refreshCatalog, viewMode, sessionId } = latest.current;
      87            1 :             if (sessionScripts == null) {
      88            0 :                 logger.warn("Session is null", {});
      89            0 :                 return;
      90            0 :             }
      91            1 :             switch (command) {
      92              :                 // Execute the query script in the current session
      93            1 :                 case SessionCommandType.ExecuteEditorQuery:
      94            1 :                     logger.info("Ctrl+E session command received", {
      95            1 :                         sessionId,
      96            1 :                         viewMode: viewMode.toString(),
      97            1 :                         focusedFile: sessionScripts.scriptFocus.fileName,
      98            1 :                     }, LOG_CTX);
      99            1 :                     if (viewMode !== SessionViewMode.Session) {
     100            0 :                         logger.warn("Ignoring Ctrl+E outside session view", { sessionId }, LOG_CTX);
     101            0 :                         break;
     102            0 :                     }
     103            1 :                     {
     104            1 :                         const { entry, scriptData, scriptConnection } = selectedScriptConnection(sessionScripts, connectionRegistryStore.current);
     105            1 :                         if (scriptConnection == null || scriptConnection.connectionHealth !== ConnectionHealth.ONLINE) {
     106            1 :                             connectionAttention.request(sessionScripts.sessionId);
     107            1 :                             logger.warn("Cannot execute query command with an unhealthy connection", {
     108            1 :                                 sessionId,
     109            1 :                                 status: printConnectionHealth(scriptConnection?.connectionHealth ?? ConnectionHealth.NOT_STARTED)
     110            1 :                             }, LOG_CTX);
     111            1 :                         } else if (!entry) {
     112            0 :                             logger.warn("Ignoring Ctrl+E because no committed script is selected", {
     113            0 :                                 sessionId,
     114            0 :                                 focusedFile: sessionScripts.scriptFocus.fileName,
     115            0 :                             }, LOG_CTX);
     116            1 :                         } else if (!scriptData) {
     117            0 :                             logger.warn("Ignoring Ctrl+E because selected script data is missing", {
     118            0 :                                 sessionId,
     119            0 :                                 scriptKey: entry.scriptId.toString(),
     120            0 :                             }, LOG_CTX);
     121              :                         } else {
     122            1 :                             await dependencies.runSessionScript(
     123            1 :                                 sessionScripts,
     124            1 :                                 scriptData,
     125            1 :                                 executeQuery,
     126            1 :                                 modifySessionScripts,
     127            1 :                                 logger,
     128            1 :                             );
     129              :                         }
     130            1 :                     }
     131            1 :                     break;
     132            0 :                 case SessionCommandType.RefreshCatalog: {
     133              :                     // The catalog of the connection that the selected script runs on is refreshed
     134            0 :                     const { scriptConnection } = selectedScriptConnection(sessionScripts, connectionRegistryStore.current);
     135            0 :                     if (scriptConnection?.connectionHealth != ConnectionHealth.ONLINE) {
     136            0 :                         logger.warn("Cannot refresh the catalog of unhealthy connection", {}, LOG_CTX);
     137            0 :                     } else if (isCatalogRefreshRunning(scriptConnection)) {
     138            0 :                         logger.debug("Catalog refresh already running", { sessionId: sessionScripts.sessionId }, LOG_CTX);
     139              :                     } else {
     140            0 :                         refreshCatalog(scriptConnection.connectionId, true);
     141              :                     }
     142            0 :                     break;
     143            0 :                 }
     144            0 :                 case SessionCommandType.SelectPreviousSessionScript:
     145            0 :                     if (modifySessionScripts) {
     146            0 :                         modifySessionScripts({
     147            0 :                             type: SELECT_PREV_SCRIPT,
     148            0 :                             value: null,
     149            0 :                         });
     150              :                     }
     151            0 :                     break;
     152            0 :                 case SessionCommandType.SelectNextSessionScript:
     153            0 :                     if (modifySessionScripts) {
     154            0 :                         modifySessionScripts({
     155            0 :                             type: SELECT_NEXT_SCRIPT,
     156            0 :                             value: null,
     157            0 :                         });
     158              :                     }
     159              :                     break;
     160            1 :             }
     161            1 :         },
     162            1 :         [logger],
     163            1 :     );
     164              : 
     165              :     // Create key event handlers
     166            1 :     const keyHandlers = React.useMemo<KeyEventHandler[]>(
     167            1 :         () => [
     168            1 :             {
     169            1 :                 key: 'e',
     170            1 :                 ctrlKey: true,
     171            1 :                 callback: () => commandDispatch(SessionCommandType.ExecuteEditorQuery),
     172            1 :             },
     173            1 :             {
     174            1 :                 key: 'r',
     175            1 :                 ctrlKey: true,
     176            1 :                 callback: () => commandDispatch(SessionCommandType.RefreshCatalog),
     177            1 :             },
     178              :             // Ctrl+H / Ctrl+L (page-bar navigation) and Ctrl+J / Ctrl+K (feed navigation) are
     179              :             // handled in the session page, where the meta tabs (relations/functions) and the
     180              :             // editing/details view state are in scope. They fold the meta tabs into the same
     181              :             // left/right stepping and become no-ops when the feed isn't showing. They are
     182              :             // intentionally not bound here to avoid double-handling the key.
     183            1 :         ],
     184            1 :         [commandDispatch],
     185            1 :     );
     186              : 
     187              :     // Setup key event handlers
     188            1 :     useKeyEvents(keyHandlers);
     189              : 
     190            1 :     return (
     191            1 :         <SessionConnectionAttentionContext.Provider value={connectionAttention}>
     192            1 :             <COMMAND_DISPATCH_CTX.Provider value={commandDispatch}>
     193            1 :                 {props.children}
     194            1 :             </COMMAND_DISPATCH_CTX.Provider>
     195            1 :         </SessionConnectionAttentionContext.Provider>
     196              :     );
     197            1 : };
        

Generated by: LCOV version 2.0-1