From 1b5b9b163fca7b779f09112c6980565870a3d2ce Mon Sep 17 00:00:00 2001 From: Ilya Boyandin Date: Mon, 26 Jan 2026 17:36:49 +0100 Subject: [PATCH 1/3] feat: Abort query in CreateTableForm Signed-off-by: Ilya Boyandin --- packages/room-shell/src/RoomShellSlice.ts | 12 +- .../src/components/CreateTableModal.tsx | 276 +++++++++++++----- 2 files changed, 209 insertions(+), 79 deletions(-) diff --git a/packages/room-shell/src/RoomShellSlice.ts b/packages/room-shell/src/RoomShellSlice.ts index c9189a9e4..907acba2a 100644 --- a/packages/room-shell/src/RoomShellSlice.ts +++ b/packages/room-shell/src/RoomShellSlice.ts @@ -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,9 @@ 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..26fdb83da 100644 --- a/packages/sql-editor/src/components/CreateTableModal.tsx +++ b/packages/sql-editor/src/components/CreateTableModal.tsx @@ -1,6 +1,6 @@ -import {zodResolver} from '@hookform/resolvers/zod'; -import {makeQualifiedTableName} from '@sqlrooms/duckdb'; -import {SqlQueryDataSource} from '@sqlrooms/room-shell'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { makeQualifiedTableName } from '@sqlrooms/duckdb'; +import { SqlQueryDataSource } from '@sqlrooms/room-shell'; import { Alert, AlertDescription, @@ -34,14 +34,14 @@ import { TooltipContent, TooltipProvider, TooltipTrigger, - cn, + cn } from '@sqlrooms/ui'; -import {Check, ChevronsUpDown, HelpCircle} from 'lucide-react'; -import {FC, useCallback, useMemo, useState} from 'react'; -import {useForm} from 'react-hook-form'; +import { Check, ChevronsUpDown, HelpCircle } from 'lucide-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'; -import {SqlMonacoEditor} from '../SqlMonacoEditor'; +import { useStoreWithSqlEditor } from '../SqlEditorSlice'; +import { SqlMonacoEditor } from '../SqlMonacoEditor'; const VALID_TABLE_OR_COLUMN_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/; @@ -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,23 @@ 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; }; /** @@ -143,54 +160,54 @@ const SchemaCombobox: FC<{ emptyMessage, disabled, }) => { - const [open, setOpen] = useState(false); - - return ( - - - - - - - - - {emptyMessage} - - {options.map((option) => ( - { - onChange(currentValue === value ? undefined : currentValue); - setOpen(false); - }} - > - - {option} - - ))} - - - - - - ); -}; + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + {emptyMessage} + + {options.map((option) => ( + { + onChange(currentValue === value ? undefined : currentValue); + setOpen(false); + }} + > + + {option} + + ))} + + + + + + ); + }; /** * Compact checkbox option with clickable label and tooltip. @@ -202,7 +219,7 @@ const OptionCheckbox: FC<{ checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; -}> = ({id, label, tooltip, checked, onCheckedChange, disabled}) => ( +}> = ({ id, label, tooltip, checked, onCheckedChange, disabled }) => (
= ({ query, onClose, + onRequestClose, editDataSource, allowMultipleStatements = false, showSchemaSelection = false, onAddOrUpdateSqlQuery, initialValues, + onSubmittingChange, + onRegisterCancel, }) => { const connector = useStoreWithSqlEditor((state) => state.db.connector); const createTableFromQuery = useStoreWithSqlEditor( @@ -256,7 +276,7 @@ const CreateTableForm: FC = ({ ); // Extract unique schemas and databases from tables (excluding system ones) - const {schemas, databases} = useMemo(() => { + const { schemas, databases } = useMemo(() => { const schemaSet = new Set(); const databaseSet = new Set(); @@ -293,11 +313,26 @@ 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} = + const { tableName, query, schema, database, replace, temp, view } = values; if (onAddOrUpdateSqlQuery) { @@ -306,12 +341,13 @@ const CreateTableForm: FC = ({ tableName, query, editDataSource?.tableName, + abortController.signal, ); } else { // New path: call createTableFromQuery directly const qualifiedName = schema || database - ? makeQualifiedTableName({table: tableName, schema, database}) + ? makeQualifiedTableName({ table: tableName, schema, database }) : tableName; await createTableFromQuery(qualifiedName, query, { @@ -319,6 +355,7 @@ const CreateTableForm: FC = ({ temp, view, allowMultipleStatements, + abortSignal: abortController.signal, }); // Refresh table schemas to show the new table @@ -328,7 +365,13 @@ const CreateTableForm: FC = ({ form.reset(); onClose(); } catch (err) { - form.setError('root', {type: 'manual', message: `${err}`}); + if (isAbortError(err)) { + return; + } + form.setError('root', { type: 'manual', message: `${err}` }); + } finally { + abortControllerRef.current = null; + setIsCancelling(false); } }, [ @@ -346,6 +389,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,11 +429,11 @@ const CreateTableForm: FC = ({ )} {/* Table name, schema, database in single row */} -
+
( + render={({ field }) => ( {watchView ? 'View name' : 'Table name'} @@ -403,7 +456,7 @@ const CreateTableForm: FC = ({ ( + render={({ field }) => ( Schema @@ -425,7 +478,7 @@ const CreateTableForm: FC = ({ ( + render={({ field }) => ( Database @@ -450,7 +503,7 @@ const CreateTableForm: FC = ({ ( + render={({ field }) => ( = ({ options={{ scrollBeyondLastLine: false, automaticLayout: true, - minimap: {enabled: false}, + minimap: { enabled: false }, wordWrap: 'on', folding: false, lineNumbers: 'off', @@ -481,7 +534,7 @@ const CreateTableForm: FC = ({ ( + render={({ field }) => ( = ({ ( + render={({ field }) => ( = ({ @@ -560,22 +621,83 @@ const CreateTableModal: FC = (props) => { className, initialValues, } = props; + const [isSubmitting, setIsSubmitting] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const cancelRef = useRef<(() => void) | null>(null); + + useEffect(() => { + if (!isOpen) { + setIsSubmitting(false); + setIsConfirmOpen(false); + cancelRef.current = null; + } + }, [isOpen]); + + const handleRequestClose = useCallback(() => { + if (!isSubmitting) { + onClose(); + return; + } + setIsConfirmOpen(true); + }, [isSubmitting, onClose]); + + const handleConfirmClose = useCallback(() => { + setIsConfirmOpen(false); + cancelRef.current?.(); + onClose(); + }, [onClose]); 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. + + + + + + + + ); }; From c7401aeab45471050da97f4bbcc97740f5b7e3e4 Mon Sep 17 00:00:00 2001 From: Ilya Boyandin Date: Mon, 26 Jan 2026 19:14:13 +0100 Subject: [PATCH 2/3] fixes Signed-off-by: Ilya Boyandin --- packages/room-shell/src/RoomShellSlice.ts | 2 +- packages/sql-editor/src/components/CreateTableModal.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/room-shell/src/RoomShellSlice.ts b/packages/room-shell/src/RoomShellSlice.ts index 907acba2a..d9c632f16 100644 --- a/packages/room-shell/src/RoomShellSlice.ts +++ b/packages/room-shell/src/RoomShellSlice.ts @@ -57,7 +57,7 @@ export type TaskProgress = { }; export type RoomShellSliceState = { - initialize?: () => Promise; + initialize?: () => Promise; room: BaseRoomStoreState['room'] & { config: RoomShellSliceConfig; tasksProgress: Record; diff --git a/packages/sql-editor/src/components/CreateTableModal.tsx b/packages/sql-editor/src/components/CreateTableModal.tsx index 26fdb83da..3971b6813 100644 --- a/packages/sql-editor/src/components/CreateTableModal.tsx +++ b/packages/sql-editor/src/components/CreateTableModal.tsx @@ -656,7 +656,6 @@ const CreateTableModal: FC = (props) => { } }} > - {isOpen && ( Date: Mon, 26 Jan 2026 22:19:22 +0100 Subject: [PATCH 3/3] fixes Signed-off-by: Ilya Boyandin --- packages/room-shell/src/RoomShellSlice.ts | 14 +- .../src/components/CreateTableModal.tsx | 180 +++++++++--------- 2 files changed, 96 insertions(+), 98 deletions(-) diff --git a/packages/room-shell/src/RoomShellSlice.ts b/packages/room-shell/src/RoomShellSlice.ts index d9c632f16..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 { @@ -57,7 +57,7 @@ export type TaskProgress = { }; export type RoomShellSliceState = { - initialize?: () => Promise; + initialize?: () => Promise; room: BaseRoomStoreState['room'] & { config: RoomShellSliceConfig; tasksProgress: Record; @@ -333,9 +333,13 @@ export function createRoomShellSlice( await db.getTables(schema), ) : tableName; - const {rowCount} = await db.createTableFromQuery(newTableName, query, { - abortSignal, - }); + 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 3971b6813..f6745fcaf 100644 --- a/packages/sql-editor/src/components/CreateTableModal.tsx +++ b/packages/sql-editor/src/components/CreateTableModal.tsx @@ -1,6 +1,6 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { makeQualifiedTableName } from '@sqlrooms/duckdb'; -import { SqlQueryDataSource } from '@sqlrooms/room-shell'; +import {zodResolver} from '@hookform/resolvers/zod'; +import {makeQualifiedTableName} from '@sqlrooms/duckdb'; +import {SqlQueryDataSource} from '@sqlrooms/room-shell'; import { Alert, AlertDescription, @@ -34,14 +34,14 @@ import { TooltipContent, TooltipProvider, TooltipTrigger, - cn + cn, } from '@sqlrooms/ui'; -import { Check, ChevronsUpDown, HelpCircle } from 'lucide-react'; -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import {Check, ChevronsUpDown, HelpCircle} from 'lucide-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'; -import { SqlMonacoEditor } from '../SqlMonacoEditor'; +import {useStoreWithSqlEditor} from '../SqlEditorSlice'; +import {SqlMonacoEditor} from '../SqlMonacoEditor'; const VALID_TABLE_OR_COLUMN_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/; @@ -133,9 +133,7 @@ const isAbortError = (err: unknown): boolean => { return err.name === 'AbortError'; } if (err instanceof Error) { - return ( - err.name === 'AbortError' || /cancelled|canceled/i.test(err.message) - ); + return err.name === 'AbortError' || /cancelled|canceled/i.test(err.message); } return false; }; @@ -160,54 +158,54 @@ const SchemaCombobox: FC<{ emptyMessage, disabled, }) => { - const [open, setOpen] = useState(false); - - return ( - - - - - - - - - {emptyMessage} - - {options.map((option) => ( - { - onChange(currentValue === value ? undefined : currentValue); - setOpen(false); - }} - > - - {option} - - ))} - - - - - - ); - }; + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + {emptyMessage} + + {options.map((option) => ( + { + onChange(currentValue === value ? undefined : currentValue); + setOpen(false); + }} + > + + {option} + + ))} + + + + + + ); +}; /** * Compact checkbox option with clickable label and tooltip. @@ -219,7 +217,7 @@ const OptionCheckbox: FC<{ checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; -}> = ({ id, label, tooltip, checked, onCheckedChange, disabled }) => ( +}> = ({id, label, tooltip, checked, onCheckedChange, disabled}) => (
= ({ ); // Extract unique schemas and databases from tables (excluding system ones) - const { schemas, databases } = useMemo(() => { + const {schemas, databases} = useMemo(() => { const schemaSet = new Set(); const databaseSet = new Set(); @@ -332,7 +330,7 @@ const CreateTableForm: FC = ({ abortControllerRef.current = abortController; setIsCancelling(false); try { - const { tableName, query, schema, database, replace, temp, view } = + const {tableName, query, schema, database, replace, temp, view} = values; if (onAddOrUpdateSqlQuery) { @@ -347,7 +345,7 @@ const CreateTableForm: FC = ({ // New path: call createTableFromQuery directly const qualifiedName = schema || database - ? makeQualifiedTableName({ table: tableName, schema, database }) + ? makeQualifiedTableName({table: tableName, schema, database}) : tableName; await createTableFromQuery(qualifiedName, query, { @@ -368,7 +366,7 @@ const CreateTableForm: FC = ({ if (isAbortError(err)) { return; } - form.setError('root', { type: 'manual', message: `${err}` }); + form.setError('root', {type: 'manual', message: `${err}`}); } finally { abortControllerRef.current = null; setIsCancelling(false); @@ -433,7 +431,7 @@ const CreateTableForm: FC = ({ ( + render={({field}) => ( {watchView ? 'View name' : 'Table name'} @@ -456,7 +454,7 @@ const CreateTableForm: FC = ({ ( + render={({field}) => ( Schema @@ -478,7 +476,7 @@ const CreateTableForm: FC = ({ ( + render={({field}) => ( Database @@ -503,7 +501,7 @@ const CreateTableForm: FC = ({ ( + render={({field}) => ( = ({ options={{ scrollBeyondLastLine: false, automaticLayout: true, - minimap: { enabled: false }, + minimap: {enabled: false}, wordWrap: 'on', folding: false, lineNumbers: 'off', @@ -534,7 +532,7 @@ const CreateTableForm: FC = ({ ( + render={({field}) => ( = ({ ( + render={({field}) => ( = ({ )} -