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