Files
supabase__supabase/apps/studio/components/interfaces/Explorer/SaveQueryDropdown.tsx
Saxon Fletcher 252f69e451 chore(studio): refine Explorer sidebar, onboarding, and notebooks (#50555)
## Summary

A round of small Explorer refinements.

**Sidebar**
- Adds a **Run SQL** row (with a `+` icon) above Notebooks in the
Explorer sidebar; opens a new query tab.

**Assistant**
- Assistant query cells now have the same **Save** dropdown as query
tabs (add to an existing notebook or create a new one). It shows only
when Explorer is enabled, and not while the query is still streaming.
- `SaveQueryDropdown` takes an optional `source`, so logs queries are
saved as log cells (keeping their time range) instead of database cells.
This also fixes saving logs queries from query tabs.
- The "Drafting notebook..." notice (and the notebook loading/status
rows) now span the full message width; `delete_notebook` parts use the
wide layout like create/update.

**Onboarding**
- Replaces the single page with a four-step walkthrough: Welcome to
Explorer (with a **Preview** badge), Run SQL, Notebooks, and Chat with
your project. Each step has an icon, heading, and short description,
with step dots and **Skip** / **Back** / **Next** buttons; the last step
ends with **Continue to Explorer**.
- Removes the "Choose how Explorer opens" choice (still available in
Account preferences) and the collapsible "Learn more" section. Skipping
or finishing still respects the saved startup preference.
- Deletes `ExplorerOnboardingLearnMore`, `ExplorerHomePreference`, and
`ExplorerHomePreview`, which were only used by onboarding.

**Notebooks**
- Query cells use the same max width as markdown cells (`48rem`, was
`72rem`).
- "Add query cell" / "Add markdown cell" are now **Add query** / **Add
markdown** everywhere; the buttons at the bottom of a notebook are
larger (34px, 18px icons).

## Test plan

- [ ] Explorer sidebar: **Run SQL** opens a new query tab
- [ ] Assistant: generate SQL, use **Save** to add it to a new and an
existing notebook; repeat with a logs query and confirm a log cell is
created
- [ ] Assistant: ask for a notebook and confirm the drafting notice is
full width
- [ ] Clear `hasCompletedOnboarding` in Explorer preferences and step
through onboarding (Next / Back / Skip); finishing or skipping respects
the startup preference set in Account preferences
- [ ] Notebook: query cells line up with markdown cell width; bottom add
buttons are larger


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added a **Run SQL** shortcut to Explorer navigation.
- Assistant query results can now be saved to notebooks, including log
queries.

- **Improvements**
- Updated Explorer onboarding with guided steps, progress navigation,
and visual previews.
  - Shortened Explorer action labels and refined control sizing.
- Reduced notebook query layout width and adjusted assistant notebook
displays.

- **Changes**
- Removed the Explorer startup preference selector and onboarding “Learn
more” section.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:38:29 +08:00

142 lines
4.7 KiB
TypeScript

import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { Save } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ExplorerToolbarAction } from './ExplorerToolbar'
import { useCreateNotebook } from './hooks'
import { createLogCellSkeleton, createQueryCellSkeleton } from './utils'
import { getNotebook } from '@/data/content/notebooks/notebook-query'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import { type QuerySourceBinding } from '@/data/query-sources/query-source-registry'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
interface SaveQueryDropdownProps {
query: { title: string; sql: string }
/** Saves as a log cell when the query targets logs. Defaults to a database cell. */
source?: QuerySourceBinding
}
export const SaveQueryDropdown = ({
children,
query,
source,
}: PropsWithChildren<SaveQueryDropdownProps>) => {
const router = useRouter()
const { ref } = useParams()
const { createNotebook } = useCreateNotebook()
const notebooksSnap = useNotebooksStateSnapshot()
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 createCell = () =>
source?._tag === 'logs'
? createLogCellSkeleton({ ...query, time_range: source.time_range })
: createQueryCellSkeleton(query)
const onAddToNewNotebook = () => {
createNotebook({
cells: [createCell()],
})
}
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: createCell(),
})
notebooksSnap.requestScrollToBottom(notebookId)
router.push(`/project/${ref}/explorer/notebook/${notebookId}`)
} catch (error) {
toast.error('Failed to add query to notebook')
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{children ?? (
<ExplorerToolbarAction icon={<Save size={16} strokeWidth={2} />} tooltip="Save query" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" align="end">
<DropdownMenuSub>
<DropdownMenuSubTrigger>Add to existing notebook</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="p-0">
<Command shouldFilter={false}>
<CommandInput
autoFocus
placeholder="Search notebooks..."
className="text-xs"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandGroup>
{isPending ? (
<div className="flex flex-col p-1 gap-y-1">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
) : !notebooks?.length ? (
<p className="text-xs text-center text-foreground-lighter py-3">
No notebooks found
</p>
) : null}
{notebooks?.map((notebook) => (
<CommandItem
key={notebook.id}
value={notebook.id}
className="cursor-pointer"
onSelect={() => onAddToExistingNotebook(notebook.id)}
>
{notebook.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem onClick={onAddToNewNotebook}>Create a new notebook</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}