diff --git a/packages/room-shell/src/RoomShellSlice.ts b/packages/room-shell/src/RoomShellSlice.ts index c9189a9e4..791c70ff5 100644 --- a/packages/room-shell/src/RoomShellSlice.ts +++ b/packages/room-shell/src/RoomShellSlice.ts @@ -37,7 +37,7 @@ import { convertToValidColumnOrTableName, downloadFile, } from '@sqlrooms/utils'; -import {castDraft, produce} from 'immer'; +import {produce} from 'immer'; import {ReactNode} from 'react'; import {StateCreator, StoreApi} from 'zustand'; import { @@ -118,6 +118,7 @@ export type RoomShellSliceState = { tableName: string, query: string, oldTableName?: string, + abortSignal?: AbortSignal, ): Promise; areDatasetsReady(): boolean; @@ -317,7 +318,12 @@ export function createRoomShellSlice( await maybeDownloadDataSources(); }, - async addOrUpdateSqlQueryDataSource(tableName, query, oldTableName) { + async addOrUpdateSqlQueryDataSource( + tableName, + query, + oldTableName, + abortSignal, + ) { const {schema} = get().db; const {db} = get(); const newTableName = @@ -327,7 +333,13 @@ export function createRoomShellSlice( await db.getTables(schema), ) : tableName; - const {rowCount} = await db.createTableFromQuery(newTableName, query); + const {rowCount} = await db.createTableFromQuery( + newTableName, + query, + { + abortSignal, + }, + ); if (rowCount !== undefined) { get().db.setTableRowCount(newTableName, rowCount); } diff --git a/packages/sql-editor/src/components/CreateTableModal.tsx b/packages/sql-editor/src/components/CreateTableModal.tsx index f3f9a4c15..f6745fcaf 100644 --- a/packages/sql-editor/src/components/CreateTableModal.tsx +++ b/packages/sql-editor/src/components/CreateTableModal.tsx @@ -37,7 +37,7 @@ import { cn, } from '@sqlrooms/ui'; import {Check, ChevronsUpDown, HelpCircle} from 'lucide-react'; -import {FC, useCallback, useMemo, useState} from 'react'; +import {FC, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useForm} from 'react-hook-form'; import * as z from 'zod'; import {useStoreWithSqlEditor} from '../SqlEditorSlice'; @@ -98,6 +98,7 @@ export type CreateTableModalProps = { tableName: string, query: string, oldTableName?: string, + abortSignal?: AbortSignal, ) => Promise; /** * Additional class name for the dialog content. @@ -112,6 +113,7 @@ export type CreateTableModalProps = { type CreateTableFormProps = { query: string; onClose: () => void; + onRequestClose: () => void; editDataSource?: SqlQueryDataSource; allowMultipleStatements?: boolean; showSchemaSelection?: boolean; @@ -119,8 +121,21 @@ type CreateTableFormProps = { tableName: string, query: string, oldTableName?: string, + abortSignal?: AbortSignal, ) => Promise; initialValues?: CreateTableFormInitialValues; + onSubmittingChange?: (isSubmitting: boolean) => void; + onRegisterCancel?: (cancel: () => void) => void; +}; + +const isAbortError = (err: unknown): boolean => { + if (err instanceof DOMException) { + return err.name === 'AbortError'; + } + if (err instanceof Error) { + return err.name === 'AbortError' || /cancelled|canceled/i.test(err.message); + } + return false; }; /** @@ -234,11 +249,14 @@ const OptionCheckbox: FC<{ const CreateTableForm: FC = ({ query, onClose, + onRequestClose, editDataSource, allowMultipleStatements = false, showSchemaSelection = false, onAddOrUpdateSqlQuery, initialValues, + onSubmittingChange, + onRegisterCancel, }) => { const connector = useStoreWithSqlEditor((state) => state.db.connector); const createTableFromQuery = useStoreWithSqlEditor( @@ -293,9 +311,24 @@ const CreateTableForm: FC = ({ }); const isSubmitting = form.formState.isSubmitting; + const [isCancelling, setIsCancelling] = useState(false); + const abortControllerRef = useRef(null); + + useEffect(() => { + return () => { + abortControllerRef.current?.abort(); + abortControllerRef.current = null; + }; + }, []); + useEffect(() => { + onSubmittingChange?.(isSubmitting); + }, [isSubmitting, onSubmittingChange]); const onSubmit = useCallback( async (values: FormValues) => { + const abortController = new AbortController(); + abortControllerRef.current = abortController; + setIsCancelling(false); try { const {tableName, query, schema, database, replace, temp, view} = values; @@ -306,6 +339,7 @@ const CreateTableForm: FC = ({ tableName, query, editDataSource?.tableName, + abortController.signal, ); } else { // New path: call createTableFromQuery directly @@ -319,6 +353,7 @@ const CreateTableForm: FC = ({ temp, view, allowMultipleStatements, + abortSignal: abortController.signal, }); // Refresh table schemas to show the new table @@ -328,7 +363,13 @@ const CreateTableForm: FC = ({ form.reset(); onClose(); } catch (err) { + if (isAbortError(err)) { + return; + } form.setError('root', {type: 'manual', message: `${err}`}); + } finally { + abortControllerRef.current = null; + setIsCancelling(false); } }, [ @@ -346,6 +387,16 @@ const CreateTableForm: FC = ({ const watchTemp = form.watch('temp'); const watchTableName = form.watch('tableName'); + const handleCancel = useCallback(async () => { + if (abortControllerRef.current) { + setIsCancelling(true); + abortControllerRef.current.abort(); + } + }, []); + useEffect(() => { + onRegisterCancel?.(handleCancel); + }, [handleCancel, onRegisterCancel]); + return (
@@ -376,7 +427,7 @@ const CreateTableForm: FC = ({ )} {/* Table name, schema, database in single row */} -
+
= ({ )} - @@ -560,22 +613,84 @@ const CreateTableModal: FC = (props) => { className, initialValues, } = props; + const [isSubmitting, setIsSubmitting] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const cancelRef = useRef<(() => void) | null>(null); + + const resetState = useCallback(() => { + setIsSubmitting(false); + setIsConfirmOpen(false); + cancelRef.current = null; + }, []); + + const handleClose = useCallback(() => { + resetState(); + onClose(); + }, [onClose, resetState]); + + const handleRequestClose = useCallback(() => { + if (!isSubmitting) { + handleClose(); + return; + } + setIsConfirmOpen(true); + }, [handleClose, isSubmitting]); + + const handleConfirmClose = useCallback(() => { + cancelRef.current?.(); + handleClose(); + }, [handleClose]); return ( - !open && onClose()}> + { + if (!open) { + handleRequestClose(); + } + }} + > {isOpen && ( { + cancelRef.current = cancel; + }} /> )} + + + + Cancel running query? + + A query is still running. Cancelling it will stop the query and + close this dialog. + + + + + + + + ); };