Files
supabase/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx
T
Joshen Lim 77c5a0b9d9 Joshen/fe 4077 explorer query cells (#48989)
## Context

Related to Explorer / Notebooks - adds a barebones query cell that
minimally can run SQL queries + render results

The intention is to migrate the components used in the SQL Editor into
this new Query cell since all the functionality is very similar, but the
SQL Editor component is tightly coupled to the SQL Editor valtio store.
So we'll be duplicating a bit of UI for now - which will also make
deprecating the SQL Editor eventually a bit easier by just deleting them

Have deliberately omitted a lot of details for now just to keep the PRs
small, so will be continuing to build out the QueryCell's functionality
in subsequent PRs. This includes
- Source selector
- Data display (Table / Chart)
- Autolimit logic

Other changes also includes
- Updating NotebookEditor to use the new Explorer UI components that
Saxon introduced

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/d709f6f1-f6cc-4e6a-babd-f5b68bba2a55"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/a286ceca-ac84-4fbd-afd5-b24f940e2e29"
/>



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

* **New Features**
* Added notebook database cells for writing, editing, and running SQL
queries.
* Added query result displays with loading, empty, error, row-limit, and
result states.
  * Added editable notebook titles with save and cancel controls.
  * New notebooks can start with customizable Markdown and SQL cells.
* Added helpful SQL error actions, including copying messages, database
connection guidance, and AI Assistant support where available.

* **Improvements**
* Improved notebook spacing, section layout, toolbar tooltips, and
empty-result presentation.
  * Markdown changes now save automatically through the notebook editor.
  * Improved drag-and-drop controls and query visibility management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-13 08:44:27 +07:00

139 lines
4.7 KiB
TypeScript

import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { useParams } from 'common'
import { Notebook, NotebookText, Play, Save } from 'lucide-react'
import { useEffect, useEffectEvent } from 'react'
import { AiIconAnimation, Button } from 'ui'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
import {
ExplorerToolbar,
ExplorerToolbarAction,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from './ExplorerToolbar'
import { MarkdownCell } from './MarkdownCell'
import { QueryCell } from './QueryCell'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
export const NotebookEditor = () => {
const { id } = useParams()
const tabs = useTabsStateSnapshot()
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const { name, content } = currentNotebook?.notebook ?? {}
const cells = content?.cells ?? []
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
const handleSaveTitle = (titleValue: string) => {
const trimmedName = titleValue.trim()
if (id && trimmedName && trimmedName !== name) {
snap.renameNotebook({ id, name: trimmedName })
tabs.updateTab(createTabId('notebook', { id }), { label: trimmedName })
}
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!id || !over || active.id === over.id) return
const oldIndex = cells.findIndex((cell) => cell.id === active.id)
const newIndex = cells.findIndex((cell) => cell.id === over.id)
if (oldIndex === -1 || newIndex === -1) return
snap.updateCells({ id, cells: arrayMove([...cells], oldIndex, newIndex) })
}
const registerTab = useEffectEvent(() => {
if (!id) return
tabs.addTab({
id: createTabId('notebook', { id }),
type: 'notebook',
label: name ?? 'New Notebook',
metadata: { notebookId: id },
isPreview: false,
})
})
useEffect(() => registerTab(), [id])
return (
<div className="flex flex-col h-full bg-surface-100">
<ExplorerToolbar className="px-4">
<ExplorerToolbarIcon>
<NotebookText size={14} className="text-foreground-light" />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={handleSaveTitle}>{name ?? ''}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<ExplorerToolbarAction icon={<AiIconAnimation size={16} />}>
Analyze
</ExplorerToolbarAction>
<ExplorerToolbarAction icon={<Play />} tooltip="Run notebook" />
<ExplorerToolbarAction icon={<Save />} tooltip="Save changes" />
</ExplorerToolbarActions>
</ExplorerToolbar>
<div className="w-full mx-auto flex-grow min-h-0 overflow-y-auto">
<div className="p-4 pb-10">
{cells.length === 0 && (
<EmptyStatePresentational
icon={<Notebook className="text-foreground-lighter" />}
title="This notebook is empty"
description="Add a query cell to run SQL against your database or logs."
contentClassName="[&>h3]:text-sm [&>p]:text-xs"
>
<div className="flex items-center gap-x-2">
<Button variant="default">Add query cell</Button>
<Button variant="default">Add markdown cell</Button>
</div>
</EmptyStatePresentational>
)}
{cells.length > 0 && (
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext
items={cells.map((cell) => cell.id)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-y-3">
{cells.map((cell) => {
switch (cell._tag) {
case 'markdown_cell':
return <MarkdownCell key={cell.id} cell={cell} />
case 'database_cell':
return <QueryCell key={cell.id} cell={cell} />
case 'log_cell':
// [Joshen] Will eventually hook it up
return null
}
})}
</div>
</SortableContext>
</DndContext>
)}
</div>
</div>
</div>
)
}