diff --git a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx index d4125fe3b2b..556e2f130cd 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx @@ -29,7 +29,7 @@ import { Trash, } from 'lucide-react' import { useRouter } from 'next/router' -import { useRef, useState } from 'react' +import { useEffect, useEffectEvent, useRef, useState } from 'react' import { toast } from 'sonner' import { AiIconAnimation, @@ -108,6 +108,7 @@ export const ExplorerNotebookTab = () => { const [skipMutatingCells, setSkipMutatingCells] = useState(false) const queryCellRefs = useRef(new Map()) const savedContentRef = useRef(undefined) + const scrollContainerRef = useRef(null) const { mutate: updateNotebook, isPending: isUpdating } = useUpsertNotebookMutation({ onSuccess: () => { @@ -322,6 +323,17 @@ export const ExplorerNotebookTab = () => { snap.insertCellAfter({ id: notebookId, cellId: lastCellId, cell }) } + const scrollToBottomIfPending = useEffectEvent(() => { + if (!id || snap.pendingScrollToBottom !== id || !scrollContainerRef.current) return + + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + }) + snap.clearPendingScrollToBottom() + }) + + useEffect(() => scrollToBottomIfPending(), [id, snap.pendingScrollToBottom, content]) + if (isNotFound) { return (
@@ -358,14 +370,6 @@ export const ExplorerNotebookTab = () => { > Analyze - } - tooltip="Run notebook" - loading={isRunningNotebook} - disabled={queryCellIds.length === 0} - onClick={handleRunNotebook} - /> } @@ -397,10 +401,20 @@ export const ExplorerNotebookTab = () => { + } + tooltip="Run notebook" + loading={isRunningNotebook} + disabled={queryCellIds.length === 0} + onClick={handleRunNotebook} + > + Run + -
+
{cells.length === 0 && ( { - const { id, ref } = useParams() const router = useRouter() + const { id, ref } = useParams() const tabs = useContext(TabsStateContext) const querySnap = useExplorerQueryStateSnapshot() + const { createNotebook } = useCreateNotebook() + const notebooksSnap = useNotebooksStateSnapshot() + + const [isIntellisenseEnabled, setIsIntellisenseEnabled] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE, + true + ) + const [restoredQueryKey, setRestoredQueryKey] = useState() const [showQuery, setShowQuery] = useState(true) + const [search, setSearch] = useState('') + const debouncedSearch = useDebounce(search, 500) + + const { data: notebooksData, isPending } = useNotebooksInfiniteQuery({ + projectRef: ref, + limit: 100, + name: search.length === 0 ? search : debouncedSearch, + }) + const notebooks = useMemo(() => { + const items = notebooksData?.pages.flatMap((page) => page.content) ?? [] + return items + }, [notebooksData?.pages]) const stateDraft = id ? querySnap.drafts[id] : undefined const draft = stateDraft?.projectRef === ref ? stateDraft : undefined @@ -94,6 +138,32 @@ export const ExplorerQueryTab = () => { }) } + const onAddToNewNotebook = () => { + createNotebook({ + cells: [createQueryCellSkeleton({ title: draft.name, sql: draft.uncheckedSql })], + }) + } + + const onAddToExistingNotebook = async (notebookId: string) => { + if (!ref) return + try { + if (!notebooksSnap.notebooks[notebookId]?.notebook.content) { + const notebook = await getNotebook({ projectRef: ref, id: notebookId }) + notebooksSnap.setNotebook({ projectRef: ref, notebook }) + } + + notebooksSnap.insertCellAfter({ + id: notebookId, + cell: createQueryCellSkeleton({ title: draft.name, sql: draft.uncheckedSql }), + }) + notebooksSnap.requestScrollToBottom(notebookId) + + router.push(`/project/${ref}/explorer/notebook/${notebookId}`) + } catch (error) { + toast.error('Failed to add query to notebook') + } + } + return ( { persistTab() explorerQueryState.setDisplay({ id, display }) }} + toolbarActions={ + <> + + + } tooltip="Save query" /> + + + + Add to existing notebook + + + + + + {isPending ? ( +
+ + +
+ ) : !notebooks?.length ? ( +

+ No notebooks found +

+ ) : null} + {notebooks?.map((notebook) => ( + onAddToExistingNotebook(notebook.id)} + > + {notebook.name} + + ))} +
+
+
+
+
+ + Create a new notebook + +
+
+ + + } /> + + + setIsIntellisenseEnabled(!isIntellisenseEnabled)} + > +
+ + Intellisense enabled +
+ {isIntellisenseEnabled && } +
+
+
+ + } /> ) } diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index 3e46de3d5c5..8974a780ee0 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -366,13 +366,12 @@ export const QueryEditor = forwardRef(funct return ( <> - + {title} - {toolbarActions} {onSourceChange && ( (funct roleImpersonationState={roleImpersonationState} /> )} + {display && onDisplayChange && ( (funct tooltip={showQuery ? 'Hide query' : 'Show query'} onClick={() => onShowQueryChange(!showQuery)} /> + + {toolbarActions} + } + loading={isExecuting} tooltip={hasSelection ? 'Run selected query' : 'Run query'} disabled={ isBusy || pendingProposal !== null || isRunDisabled || sql.trim().length === 0 diff --git a/apps/studio/state/notebooks/notebooks-state.ts b/apps/studio/state/notebooks/notebooks-state.ts index 486399fadb1..96a7b716151 100644 --- a/apps/studio/state/notebooks/notebooks-state.ts +++ b/apps/studio/state/notebooks/notebooks-state.ts @@ -25,6 +25,13 @@ export const notebooksState = proxy({ cellLocalState: proxyMap([]), /** Session-only conflicts where an assistant changed the server while local edits remain. */ serverDivergedWhileDirty: proxyMap([]), + /** + * Id of the notebook the tab should scroll to the bottom of once rendered — + * set by a surface that adds a cell to a notebook it's about to navigate to + * (e.g. "Add to existing notebook" from a query tab), so the newly added + * cell lands in view. + */ + pendingScrollToBottom: undefined as string | undefined, /** * Load notebook into the Valtio store. No-ops if already present. @@ -242,6 +249,14 @@ export const notebooksState = proxy({ }, addNeedsSaving: (id: string) => notebooksState.needsSaving.set(id, true), + + requestScrollToBottom: (id: string) => { + notebooksState.pendingScrollToBottom = id + }, + + clearPendingScrollToBottom: () => { + notebooksState.pendingScrollToBottom = undefined + }, }) export const getNotebooksStateSnapshot = () => snapshot(notebooksState)