Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/room-shell/src/RoomShellSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -118,6 +118,7 @@ export type RoomShellSliceState = {
tableName: string,
query: string,
oldTableName?: string,
abortSignal?: AbortSignal,
): Promise<void>;
areDatasetsReady(): boolean;

Expand Down Expand Up @@ -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 =
Expand All @@ -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);
}
Expand Down
143 changes: 129 additions & 14 deletions packages/sql-editor/src/components/CreateTableModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -98,6 +98,7 @@ export type CreateTableModalProps = {
tableName: string,
query: string,
oldTableName?: string,
abortSignal?: AbortSignal,
) => Promise<void>;
/**
* Additional class name for the dialog content.
Expand All @@ -112,15 +113,29 @@ export type CreateTableModalProps = {
type CreateTableFormProps = {
query: string;
onClose: () => void;
onRequestClose: () => void;
editDataSource?: SqlQueryDataSource;
allowMultipleStatements?: boolean;
showSchemaSelection?: boolean;
onAddOrUpdateSqlQuery?: (
tableName: string,
query: string,
oldTableName?: string,
abortSignal?: AbortSignal,
) => Promise<void>;
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;
};

/**
Expand Down Expand Up @@ -234,11 +249,14 @@ const OptionCheckbox: FC<{
const CreateTableForm: FC<CreateTableFormProps> = ({
query,
onClose,
onRequestClose,
editDataSource,
allowMultipleStatements = false,
showSchemaSelection = false,
onAddOrUpdateSqlQuery,
initialValues,
onSubmittingChange,
onRegisterCancel,
}) => {
const connector = useStoreWithSqlEditor((state) => state.db.connector);
const createTableFromQuery = useStoreWithSqlEditor(
Expand Down Expand Up @@ -293,9 +311,24 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
});

const isSubmitting = form.formState.isSubmitting;
const [isCancelling, setIsCancelling] = useState(false);
const abortControllerRef = useRef<AbortController | null>(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;
Expand All @@ -306,6 +339,7 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
tableName,
query,
editDataSource?.tableName,
abortController.signal,
);
} else {
// New path: call createTableFromQuery directly
Expand All @@ -319,6 +353,7 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
temp,
view,
allowMultipleStatements,
abortSignal: abortController.signal,
});

// Refresh table schemas to show the new table
Expand All @@ -328,7 +363,13 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
form.reset();
onClose();
} catch (err) {
if (isAbortError(err)) {
return;
}
form.setError('root', {type: 'manual', message: `${err}`});
} finally {
abortControllerRef.current = null;
setIsCancelling(false);
}
},
[
Expand All @@ -346,6 +387,16 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
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 (
<TooltipProvider delayDuration={200}>
<Form {...form}>
Expand Down Expand Up @@ -376,7 +427,7 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
)}

{/* Table name, schema, database in single row */}
<div className="flex items-end gap-3">
<div className="flex items-start gap-3">
<FormField
control={form.control}
name="tableName"
Expand Down Expand Up @@ -526,20 +577,22 @@ const CreateTableForm: FC<CreateTableFormProps> = ({
)}

<DialogFooter>
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isSubmitting}
>
Cancel
<Button type="button" variant="outline" onClick={onRequestClose}>
Close
</Button>
<Button
type="submit"
disabled={isSubmitting || !watchTableName?.trim()}
type={isSubmitting ? 'button' : 'submit'}
onClick={isSubmitting ? handleCancel : undefined}
disabled={isSubmitting ? isCancelling : !watchTableName?.trim()}
>
{isSubmitting && <Spinner className="mr-2" />}
{editDataSource ? 'Update' : 'Create'}
{isSubmitting
? isCancelling
? 'Cancelling...'
: 'Cancel'
: editDataSource
? 'Update'
: 'Create'}
</Button>
</DialogFooter>
</form>
Expand All @@ -560,22 +613,84 @@ const CreateTableModal: FC<CreateTableModalProps> = (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 (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) {
handleRequestClose();
}
}}
>
<DialogContent className={cn('w-3xl max-w-[80%]', className)}>
{isOpen && (
<CreateTableForm
query={query}
onClose={onClose}
onClose={handleClose}
onRequestClose={handleRequestClose}
editDataSource={editDataSource}
allowMultipleStatements={allowMultipleStatements}
showSchemaSelection={showSchemaSelection}
onAddOrUpdateSqlQuery={onAddOrUpdateSqlQuery}
initialValues={initialValues}
onSubmittingChange={setIsSubmitting}
onRegisterCancel={(cancel) => {
cancelRef.current = cancel;
}}
/>
)}
</DialogContent>
<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Cancel running query?</DialogTitle>
<DialogDescription>
A query is still running. Cancelling it will stop the query and
close this dialog.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsConfirmOpen(false)}
>
Keep running
</Button>
<Button type="button" onClick={handleConfirmClose}>
Cancel & close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
);
};
Expand Down
Loading