Files
supabase/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx
Joshen Lim 2f89014f74 Add logs cells (#49064)
## Context

Related to Explorer/Notebooks - adds the source selector for query cell
within a notebook

<img width="1108" height="648" alt="image"
src="https://github.com/user-attachments/assets/d1434197-3738-41b0-a8ef-1919c91181d9"
/>

<img width="1103" height="633" alt="image"
src="https://github.com/user-attachments/assets/97f8c6a3-629d-4a41-aab4-3fdc5e8b2c7e"
/>


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

* **New Features**
  * Added support for displaying log cells in the query editor.
  * Added switching between database and log query sources.
  * Log and database cells can display results as tables or charts.
  * Log queries support optional row limits.
  * Improved reliability when changing query settings.
  * Notebook query views now default to table display when unspecified.

* **Bug Fixes**
  * Log cells no longer appear blank or get omitted from notebook views.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-14 02:23:06 +07:00

151 lines
5.5 KiB
TypeScript

import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { useParams } from 'common'
import { FileText, Notebook, NotebookText, Play, Save, SquareCode } from 'lucide-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 { createMarkdownCellSkeleton, createQueryCellSkeleton } from './utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
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
snap.reorderCells({ id, activeCellId: active.id, overCellId: over.id })
}
const onSelectAddCell = (type: 'markdown' | 'query') => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
const cell = type === 'markdown' ? createMarkdownCellSkeleton() : createQueryCellSkeleton()
const lastCellId = cells[cells.length - 1]?.id
snap.insertCellAfter({ id: notebookId, cellId: lastCellId, cell })
}
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" onClick={() => onSelectAddCell('query')}>
Add query cell
</Button>
<Button variant="default" onClick={() => onSelectAddCell('markdown')}>
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-4">
{cells.map((cell) => {
switch (cell._tag) {
case 'markdown_cell':
return <MarkdownCell key={cell.id} cell={cell} />
case 'database_cell':
case 'log_cell':
return <QueryCell key={cell.id} cell={cell} />
}
})}
</div>
</SortableContext>
</DndContext>
<div className="flex items-center justify-center gap-x-2 mt-4">
<ButtonTooltip
variant="outline"
icon={<SquareCode />}
className="w-7"
onClick={() => onSelectAddCell('query')}
tooltip={{ content: { side: 'bottom', text: 'Add query cell' } }}
/>
<ButtonTooltip
variant="outline"
icon={<FileText />}
className="w-7"
onClick={() => onSelectAddCell('markdown')}
tooltip={{ content: { side: 'bottom', text: 'Add markdown cell' } }}
/>
</div>
</>
)}
</div>
</div>
</div>
)
}