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 -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-08-13 08:44:27 +07:00
1 parent cf36ad9e52
commit 77c5a0b9d9
15 files changed
+581 -163

No files matched your search

@@ -1,9 +1,11 @@
import { untrustedSql } from '@supabase/pg-meta'
import { MessageCirclePlus, NotebookText, SquareCode } from 'lucide-react'
import { useState } from 'react'
import { useCreateNotebook } from './hooks'
import { ActionCard } from '@/components/layouts/Tabs/ActionCard'
import { AssistantChatForm } from '@/components/ui/AIAssistantPanel/AssistantChatForm'
import { generateUuid } from '@/lib/api/snippets.browser'
import { AssistantModel } from '@/state/ai-assistant-state'
export const ExplorerHome = () => {
@@ -43,14 +45,26 @@ export const ExplorerHome = () => {
title="Create a notebook"
description="Combine notes, queries, and results"
bgColor="bg-blue-500"
onClick={createNotebook}
onClick={() => createNotebook()}
/>
<ActionCard
icon={<SquareCode className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Run SQL"
description="Write and run an ad-hoc query"
bgColor="bg-blue-500"
onClick={onCreateNotebook}
onClick={() =>
createNotebook({
name: 'SQL query',
cells: [
{
_tag: 'database_cell',
id: generateUuid(),
unchecked_sql: untrustedSql(''),
row_limit: 100,
},
],
})
}
/>
</div>
</section>
@@ -1,5 +1,9 @@
import React from 'react'
import { Edit } from 'lucide-react'
import React, { useState } from 'react'
import { Button, cn } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export type ExplorerToolbarProps = React.ComponentProps<'div'>
@@ -15,7 +19,7 @@ const ExplorerToolbar = ({ className, role = 'toolbar', ...props }: ExplorerTool
data-slot="explorer-toolbar"
role={role}
className={cn(
'flex h-10 w-full shrink-0 items-center gap-2 border-b bg-transparent px-3 md:min-h-[var(--header-height,2.5rem)]',
'flex h-10 w-full shrink-0 items-center gap-2 border-b bg-transparent px-3',
className
)}
{...props}
@@ -40,16 +44,74 @@ const ExplorerToolbarIcon = ({
)
ExplorerToolbarIcon.displayName = 'ExplorerToolbarIcon'
export type ExplorerToolbarTitleProps = React.ComponentProps<'div'>
export type ExplorerToolbarTitleProps = Omit<React.ComponentProps<'div'>, 'children'> & {
children: string
onSaveTitle?: (value: string) => void
}
/** Flexible title region for static text or an editable resource name. */
const ExplorerToolbarTitle = ({ className, ...props }: ExplorerToolbarTitleProps) => (
<div
data-slot="explorer-toolbar-title"
className={cn('min-w-0 flex-1 truncate text-sm', className)}
{...props}
/>
)
const ExplorerToolbarTitle = ({
children: title,
className,
onSaveTitle,
...props
}: ExplorerToolbarTitleProps) => {
const [isEditingTitle, setIsEditingTitle] = useState(false)
const [value, setValue] = useState(title)
const handleStartEditing = () => {
setValue(title)
setIsEditingTitle(true)
}
return (
<div
data-slot="explorer-toolbar-title"
className={cn('min-w-0 flex-1 truncate text-sm', className)}
{...props}
>
{isEditingTitle ? (
<Input
autoFocus
size="tiny"
containerClassName="max-w-64 has-[[data-slot=input-group-control]:focus-visible]:ring-0"
className="outline-none"
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
if (isEditingTitle) {
setIsEditingTitle(false)
onSaveTitle?.(value)
}
}}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
setIsEditingTitle(false)
} else if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
setIsEditingTitle(false)
onSaveTitle?.(value)
}
}}
/>
) : onSaveTitle ? (
<Button
variant="text"
className="group"
onClick={handleStartEditing}
iconRight={<Edit className="opacity-0 group-hover:opacity-100 transition" />}
>
{title}
</Button>
) : (
title
)}
</div>
)
}
ExplorerToolbarTitle.displayName = 'ExplorerToolbarTitle'
export type ExplorerToolbarActionsProps = React.ComponentProps<'div'>
@@ -70,7 +132,9 @@ ExplorerToolbarActions.displayName = 'ExplorerToolbarActions'
export type ExplorerToolbarActionProps = Omit<
React.ComponentPropsWithRef<typeof Button>,
'size' | 'variant'
>
> & {
tooltip?: string
}
/**
* The standard tiny, text-style button used for a direct toolbar action.
@@ -80,18 +144,20 @@ const ExplorerToolbarAction = ({
children,
className,
ref,
tooltip,
...props
}: ExplorerToolbarActionProps) => (
<Button
<ButtonTooltip
ref={ref}
data-slot="explorer-toolbar-action"
variant="text"
size="tiny"
className={cn(children == null && 'w-7 px-0', className)}
tooltip={{ content: { side: 'bottom', text: tooltip } }}
{...props}
>
{children}
</Button>
</ButtonTooltip>
)
ExplorerToolbarAction.displayName = 'ExplorerToolbarAction'
@@ -8,18 +8,21 @@ import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { SortableSection } from '@/components/ui/SortableSection'
import { type MarkdownCell as MarkdownCellSchema } from '@/data/content/notebooks/notebook-schema'
import { useLatest } from '@/hooks/misc/useLatest'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
interface MarkdownCellProps {
cell: MarkdownCellSchema
onCommitChanges: (text: string) => void
}
export const MarkdownCell = ({ cell, onCommitChanges }: MarkdownCellProps) => {
export const MarkdownCell = ({ cell }: MarkdownCellProps) => {
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const cells = currentNotebook?.notebook.content?.cells ?? []
const [value, setValue] = useState(cell.text)
const [isEditing, setIsEditing] = useState(false)
const valueRef = useLatest(value)
const onCommitChangesRef = useLatest(onCommitChanges)
const handleStartEditing = () => {
setValue(cell.text)
@@ -30,11 +33,17 @@ export const MarkdownCell = ({ cell, onCommitChanges }: MarkdownCellProps) => {
setIsEditing(false)
}
const handleCommit = () => {
onCommitChangesRef.current(valueRef.current)
const handleUpdateMarkdown = (cellId: string, text: string) => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
const nextCells = cells.map((c) => (c.id === cellId ? { ...c, text } : c))
snap.updateCells({ id: notebookId, cells: nextCells })
setIsEditing(false)
}
const handleUpdateMarkdownRef = useLatest(handleUpdateMarkdown)
return (
<SortableSection gripClassName="mt-2.5" id={cell.id}>
{isEditing ? (
@@ -65,8 +74,12 @@ export const MarkdownCell = ({ cell, onCommitChanges }: MarkdownCellProps) => {
'!inlineSuggestionVisible',
].join(' && ')
)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, handleCommit)
editor.onDidBlurEditorWidget(handleCommit)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () =>
handleUpdateMarkdownRef.current(cell.id, valueRef.current)
)
editor.onDidBlurEditorWidget(() =>
handleUpdateMarkdownRef.current(cell.id, valueRef.current)
)
}}
/>
<div className="border-t flex items-center justify-between pl-3 pr-1 py-1">
@@ -75,7 +88,11 @@ export const MarkdownCell = ({ cell, onCommitChanges }: MarkdownCellProps) => {
<Button variant="text" onMouseDown={(e) => e.preventDefault()} onClick={handleCancel}>
Cancel
</Button>
<Button variant="text" onMouseDown={(e) => e.preventDefault()} onClick={handleCommit}>
<Button
variant="text"
onMouseDown={(e) => e.preventDefault()}
onClick={() => handleUpdateMarkdown(cell.id, value)}
>
Save
</Button>
</div>
@@ -13,28 +13,30 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { useParams } from 'common'
import { Edit, Notebook, NotebookText, Play, Save } from 'lucide-react'
import { useEffect, useEffectEvent, useState } from 'react'
import { Notebook, NotebookText, Play, Save } from 'lucide-react'
import { useEffect, useEffectEvent } from 'react'
import { AiIconAnimation, Button } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
import {
ExplorerToolbar,
ExplorerToolbarAction,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from './ExplorerToolbar'
import { MarkdownCell } from './MarkdownCell'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
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 stateNotebook = id ? snap.notebooks[id] : undefined
const { name, content } = stateNotebook?.notebook ?? {}
const [titleValue, setTitleValue] = useState<string>(name ?? '')
const [isEditingTitle, setIsEditingTitle] = useState(false)
const currentNotebook = useCurrentNotebook()
const { name, content } = currentNotebook?.notebook ?? {}
const cells = content?.cells ?? []
const sensors = useSensors(
@@ -42,20 +44,12 @@ export const NotebookEditor = () => {
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
const handleSaveTitle = () => {
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 })
}
setIsEditingTitle(false)
}
const handleUpdateCellText = (cellId: string, text: string) => {
if (!id) return
const nextCells = cells.map((cell) => (cell.id === cellId ? { ...cell, text } : cell))
snap.updateCells({ id, cells: nextCells })
}
const handleDragEnd = (event: DragEndEvent) => {
@@ -84,66 +78,22 @@ export const NotebookEditor = () => {
return (
<div className="flex flex-col h-full bg-surface-100">
<div className="border-b min-h-10 flex items-center justify-between px-4">
<div className="flex items-center gap-x-2">
<ExplorerToolbar className="px-4">
<ExplorerToolbarIcon>
<NotebookText size={14} className="text-foreground-light" />
{isEditingTitle ? (
<Input
autoFocus
size="tiny"
value={titleValue}
onChange={(e) => setTitleValue(e.target.value)}
onBlur={() => {
if (isEditingTitle) handleSaveTitle()
}}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
setIsEditingTitle(false)
setTitleValue(name ?? '')
} else if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
handleSaveTitle()
}
}}
/>
) : (
<Button
variant="text"
className="group"
onClick={() => {
setTitleValue(name ?? '')
setIsEditingTitle(true)
}}
iconRight={<Edit className="opacity-0 group-hover:opacity-100" />}
>
{name}
</Button>
)}
</div>
<div className="flex items-center gap-x-2">
<Button variant="outline" icon={<AiIconAnimation size={16} />}>
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={handleSaveTitle}>{name ?? ''}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<ExplorerToolbarAction icon={<AiIconAnimation size={16} />}>
Analyze
</Button>
<ButtonTooltip
variant="outline"
icon={<Play />}
className="px-1"
tooltip={{ content: { side: 'bottom', text: 'Run notebook' } }}
/>
<ButtonTooltip
variant="outline"
icon={<Save />}
className="px-1"
tooltip={{ content: { side: 'bottom', text: 'Save changes' } }}
/>
</div>
</div>
</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">
<div className="p-4 pb-10">
{cells.length === 0 && (
<EmptyStatePresentational
icon={<Notebook className="text-foreground-lighter" />}
@@ -163,18 +113,15 @@ export const NotebookEditor = () => {
items={cells.map((cell) => cell.id)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-y-3">
{cells.map((cell) => {
switch (cell._tag) {
case 'markdown_cell':
return (
<MarkdownCell
key={cell.id}
cell={cell}
onCommitChanges={(text) => handleUpdateCellText(cell.id, text)}
/>
)
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
@@ -0,0 +1,171 @@
import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
import { CodeSquare, Eye, EyeOff, Play, Settings2 } from 'lucide-react'
import { useState } from 'react'
import { cn } from 'ui'
import {
ExplorerQuery,
ExplorerQueryEditor,
ExplorerQueryFooter,
ExplorerQueryResults,
} from '../ExplorerQuery'
import {
ExplorerToolbar,
ExplorerToolbarAction,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from '../ExplorerToolbar'
import { QueryResultTable } from '../QueryResultTable'
import { type QueryResult } from '../types'
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { SortableSection } from '@/components/ui/SortableSection'
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
import { useLatest } from '@/hooks/misc/useLatest'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { type ResponseError } from '@/types'
interface QueryCellProps {
cell: DatabaseCellSchema
}
/**
* [Joshen] Aiming to keep PRs small so the following are deliberating missing for now:
* - Auto limit logic
* - Database selection logic
* - Data display logic
*
* QueryCell atm minimally supports running queries and rendering results
*/
export const QueryCell = ({ cell }: QueryCellProps) => {
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const { data: project } = useSelectedProjectQuery()
const cells = currentNotebook?.notebook.content?.cells ?? []
const { title = 'Untitled snippet', row_limit } = cell
const [showQuery, setShowQuery] = useState(true)
const [value, setValue] = useState<string>(cell.unchecked_sql)
const [result, setResult] = useState<QueryResult>()
const valueRef = useLatest(value)
const { mutateAsync: executeQuery, isPending: isExecuting } = useExecuteSqlMutation({
onSuccess: (data) =>
setResult({
rows: data.result,
error: undefined,
autoLimit: undefined,
}),
onError: (error) =>
setResult({
rows: undefined,
error: error as unknown as ResponseError,
autoLimit: undefined,
}),
})
const onRunQuery = async () => {
if (!project) return console.error('Project is required')
handleUpdateCell({ sql: value })
executeQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
sql: acceptUntrustedSql(untrustedSql(value)),
})
}
const handleUpdateCell = (payload: { sql: string } | { title: string }) => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
const nextCells = cells.map((c) => {
if (c.id !== cell.id || c._tag !== 'database_cell') {
return c
}
if ('sql' in payload) {
return { ...c, unchecked_sql: untrustedSql(payload.sql) }
}
const trimmedTitle = payload.title.trim()
return trimmedTitle ? { ...c, title: trimmedTitle } : c
})
snap.updateCells({ id: notebookId, cells: nextCells })
}
const handleUpdateCellRef = useLatest(handleUpdateCell)
return (
<SortableSection gripClassName="mt-2.5" id={cell.id}>
<ExplorerQuery className="max-w-4xl mx-auto">
<ExplorerToolbar>
<ExplorerToolbarIcon>
<CodeSquare size={14} />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle onSaveTitle={(newTitle) => handleUpdateCell({ title: newTitle })}>
{title}
</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<ExplorerToolbarAction
disabled={(result?.rows ?? []).length === 0}
icon={<Settings2 />}
tooltip="Result settings"
/>
<ExplorerToolbarAction
icon={showQuery ? <EyeOff /> : <Eye />}
tooltip={showQuery ? 'Hide query' : 'Show query'}
onClick={() => setShowQuery((prev) => !prev)}
/>
<ExplorerToolbarAction
loading={isExecuting}
icon={<Play />}
tooltip="Run query"
onClick={onRunQuery}
/>
</ExplorerToolbarActions>
</ExplorerToolbar>
{showQuery && (
<ExplorerQueryEditor>
<CodeEditor
language="pgsql"
value={value}
onInputChange={(v) => setValue(v ?? '')}
className="h-32"
actions={{ runQuery: { enabled: true, callback: onRunQuery } }}
onMount={(editor) => {
editor.onDidBlurEditorWidget(() =>
handleUpdateCellRef.current({ sql: valueRef.current })
)
}}
/>
</ExplorerQueryEditor>
)}
<ExplorerQueryResults
className={cn(
(result?.rows ?? []).length === 0
? 'flex items-center justify-center'
: 'overflow-x-auto'
)}
>
<QueryResultTable result={result} />
</ExplorerQueryResults>
<ExplorerQueryFooter className="flex items-center gap-x-2">
<p>{(result?.rows ?? []).length.toLocaleString()} rows</p>
<p>·</p>
<p>Limit {row_limit} rows</p>
</ExplorerQueryFooter>
</ExplorerQuery>
</SortableSection>
)
}
@@ -0,0 +1,188 @@
import { useParams } from 'common'
import { ExternalLink } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils'
import { Results } from '../SQLEditor/UtilityPanel/Results'
import { getSqlErrorLines } from '../SQLEditor/UtilityPanel/UtilityTabResults.utils'
import { type QueryResult } from './types'
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
import CopyButton from '@/components/ui/CopyButton'
import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { DOCS_URL } from '@/lib/constants'
interface QueryResultTableProps {
result?: QueryResult
}
// [Joshen] This is essentially a duplicate of UtilityTabResults from the SQL Editor
// I'll eventually migrate the Results component over - just trying to avoid bloating
// changes wherever possible
export const QueryResultTable = ({ result }: QueryResultTableProps) => {
const { rows, error, autoLimit } = result ?? {}
if (!result) {
return <p className="text-xs text-foreground-light">Run the query to see results</p>
}
if (error) {
return <QueryError error={error} autoLimit={autoLimit} />
}
if ((rows ?? []).length === 0) {
return <p className="text-xs text-foreground-light">Success. No rows returned</p>
}
if (rows && rows.length > 0) {
return <QueryResults rows={rows} />
}
}
const QueryError = ({
error,
autoLimit,
}: {
error: NonNullable<QueryResult['error']>
autoLimit?: QueryResult['autoLimit']
}) => {
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug })
const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const isTimeout =
error.message?.includes('canceling statement due to statement timeout') ||
error.message?.includes('upstream request timeout') ||
error.message?.includes('Query read timeout')
const isNetWorkError = error.message?.includes('EHOSTUNREACH')
const errorLines = getSqlErrorLines(error)
// [Joshen] Need to check if a replica is selected as well
const readReplicaError = error.message.includes('in a read-only transaction')
const payloadTooLargeError = error.message.includes(
'Query is too large to be run via the SQL Editor'
)
return (
<div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
<div className="flex flex-row justify-between items-start py-4 px-6 gap-x-4">
{isTimeout ? (
<div className="flex flex-col gap-y-1">
<p className="font-mono text-sm tracking-tight">
Error: SQL query ran into an upstream timeout
</p>
<p className="text-sm text-foreground-light">
You can either{' '}
<InlineLink
href={`${DOCS_URL}/guides/platform/performance#examining-query-performance`}
>
optimize your query
</InlineLink>
, or{' '}
<InlineLink href={`${DOCS_URL}/guides/database/timeouts`}>
increase the statement timeout
</InlineLink>
{' or '}
<button
type="button"
tabIndex={0}
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => setShowConnect(true)}
>
connect to your database directly
</button>
.
</p>
</div>
) : (
<div className="flex flex-col gap-y-1">
{errorLines.length > 0 ? (
errorLines.map((x: string, i: number) => (
<pre key={`error-${i}`} className="font-mono text-sm text-wrap">
{x}
</pre>
))
) : (
<p className="font-mono text-sm tracking-tight">Error: {error.message}</p>
)}
{!isTimeout && !isNetWorkError && autoLimit && (
<p className="text-sm text-foreground-light">
Note: A limit of {autoLimit} was applied to your query. If this was the cause of a
syntax error, try selecting "No limit" instead and re-run the query.
</p>
)}
{readReplicaError && (
<p className="text-sm text-foreground-light">
Note: Read replicas are for read only queries. Run write queries on the primary
database instead.
</p>
)}
{payloadTooLargeError && (
<p className="text-sm text-foreground-light flex items-center gap-x-1">
Run this query by{' '}
<button
type="button"
tabIndex={0}
onClick={() => setShowConnect(true)}
className={cn(InlineLinkClassName, 'flex items-center gap-x-1')}
>
connecting to your database directly
<ExternalLink size={12} />
</button>
.
</p>
)}
</div>
)}
<div className="flex items-center gap-x-2">
{readReplicaError && (
<Button
className="py-2"
variant="default"
// [Joshen] TODO
onClick={() => {}}
>
Switch to primary database
</Button>
)}
{errorLines.length > 0 && (
<Tooltip>
<TooltipTrigger>
<CopyButton iconOnly variant="default" text={errorLines.join('\n')} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
<span>Copy error</span>
</TooltipContent>
</Tooltip>
)}
{!hasHipaaAddon && (
// [Joshen] TODO
<AiAssistantDropdown
telemetrySource="sql_debug"
label="Debug with Assistant"
buildPrompt={() => ''}
onOpenAssistant={() => {}}
disabled={false}
loading={false}
/>
)}
</div>
</div>
</div>
)
}
// [Joshen] Eventually migrate the Results component here from SQL Editor
const QueryResults = ({ rows }: { rows: NonNullable<QueryResult['rows']> }) => {
return <Results rows={rows} />
}
@@ -2,14 +2,14 @@ import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { describe, expect, it } from 'vitest'
import { ExplorerToolbar, ExplorerToolbarTitle } from '../ExplorerToolbar'
import {
ExplorerQuery,
ExplorerQueryEditor,
ExplorerQueryFooter,
ExplorerQueryResults,
ExplorerQueryViewport,
} from './index'
} from '../ExplorerQuery'
import { ExplorerToolbar, ExplorerToolbarTitle } from '../ExplorerToolbar'
describe('ExplorerQuery', () => {
it('composes the toolbar, editor, results, and footer without owning their behavior', () => {
@@ -1,4 +1,4 @@
import { render, screen, within } from '@testing-library/react'
import { screen, within } from '@testing-library/react'
import { createRef } from 'react'
import { describe, expect, it } from 'vitest'
@@ -8,7 +8,8 @@ import {
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from './index'
} from '../ExplorerToolbar'
import { customRender as render } from '@/tests/lib/custom-render'
describe('ExplorerToolbar', () => {
it('composes the resource icon, title, direct actions, and custom controls', () => {
@@ -65,20 +66,4 @@ describe('ExplorerToolbar', () => {
expect(action).not.toHaveClass('w-7')
expect(actionRef.current).toBe(action)
})
it('defaults its height but lets a consumer drive it', () => {
const { rerender } = render(<ExplorerToolbar aria-label="Default height" />)
// The header height variable stays consumer-owned, but it carries a fallback
// so apps that leave it unset keep the default row instead of an invalid
// declaration the browser drops.
expect(screen.getByRole('toolbar')).toHaveClass('h-10')
expect(screen.getByRole('toolbar').className).toContain('var(--header-height,2.5rem)')
rerender(<ExplorerToolbar aria-label="Taller height" className="h-12" />)
const toolbar = screen.getByRole('toolbar')
expect(toolbar).toHaveClass('h-12')
expect(toolbar).not.toHaveClass('h-10')
})
})
@@ -1,3 +1,4 @@
import { untrustedSql } from '@supabase/pg-meta'
import { useRouter } from 'next/router'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -5,6 +6,7 @@ import { generateUuid } from '@/lib/api/snippets.browser'
import { useProfile } from '@/lib/profile'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { type Notebook } from '@/state/notebooks/types'
import { Notebooks } from '@/types'
export const useCreateNotebook = () => {
const router = useRouter()
@@ -12,10 +14,43 @@ export const useCreateNotebook = () => {
const { data: project } = useSelectedProjectQuery()
const notebooksSnap = useNotebooksStateSnapshot()
const createNotebook = ({ id: idOverride, name }: { id?: string; name?: string } = {}) => {
const createNotebook = ({
id: idOverride,
name,
cells,
}: { id?: string; name?: string; cells?: Notebooks.Content['cells'] } = {}) => {
if (!profile) return console.error('Profile is required')
if (!project) return console.error('Project is required')
// [Joshen] Just adding sample data to play around with, keep for now - clean up at the end
const DEFAULT_CELLS = [
{
_tag: 'markdown_cell',
id: generateUuid(),
text: `
# Title
A brief description on what this notebook is about
`.trim(),
},
{
_tag: 'markdown_cell',
id: generateUuid(),
text: `
## Section
This is a sample paragraph to demonstrate the Markdown cells
1. List item 1
2. List item 2
3. List item 3
`,
},
{
_tag: 'database_cell',
id: generateUuid(),
unchecked_sql: untrustedSql('select * from colors;'),
row_limit: 100,
},
] as Notebooks.Content['cells']
const id = idOverride ?? generateUuid()
const notebook: Notebook = {
@@ -27,28 +62,7 @@ export const useCreateNotebook = () => {
favorite: false,
content: {
schema_version: 1,
// [Joshen] Just adding sample data to play around with, keep for now - clean up at the end
cells: [
{
_tag: 'markdown_cell',
id: '1',
text: `
# Title
A brief description on what this notebook is about
`.trim(),
},
{
_tag: 'markdown_cell',
id: '2',
text: `
## Section
This is a sample paragraph to demonstrate the Markdown cells
1. List item 1
2. List item 2
3. List item 3
`,
},
],
cells: cells ?? DEFAULT_CELLS,
},
owner_id: profile.id,
project_id: project.id,
@@ -0,0 +1,7 @@
import { type ResponseError } from '@/types'
export type QueryResult = {
rows?: Record<string, unknown>[]
error?: ResponseError
autoLimit?: number
}
@@ -33,7 +33,7 @@ import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
const SORT_GRIP_CLASS = 'absolute -left-6 top-1.5'
const SORT_GRIP_CLASS = 'absolute -left-10'
export const ProjectHome = () => {
const { enableBranching } = useParams()
@@ -79,11 +79,9 @@ export const Results = ({ rows }: { rows: readonly any[] }) => {
return (
<>
{rows.length === 0 ? (
<div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark">
<p className="m-0 border-0 px-4 py-3 font-mono text-sm text-foreground-light">
Success. No rows returned
</p>
</div>
<p className="px-4 py-3 font-mono text-sm text-foreground-light">
Success. No rows returned
</p>
) : (
<>
<ContextMenu modal={false}>
@@ -1,7 +1,7 @@
import { useSortable } from '@dnd-kit/sortable'
import { GripVertical } from 'lucide-react'
import type { CSSProperties, PropsWithChildren } from 'react'
import { cn } from 'ui'
import { Button, cn } from 'ui'
export const SortableSection = ({
id,
@@ -23,22 +23,22 @@ export const SortableSection = ({
<div
ref={setNodeRef}
style={style}
className="relative will-change-transform flex items-start gap-x-2"
className="relative will-change-transform flex items-start gap-x-4"
>
<button
<Button
type="button"
variant="text"
aria-label="Drag to reorder section"
className={cn(
'text-foreground-muted hover:text-foreground cursor-grab active:cursor-grabbing',
'w-6 text-foreground-muted hover:text-foreground cursor-grab active:cursor-grabbing',
'rounded-sm focus-ring',
gripClassName
)}
{...attributes}
{...listeners}
tabIndex={0}
>
<GripVertical size={14} />
</button>
icon={<GripVertical />}
/>
<div className={cn('w-full', isDragging && 'opacity-70')}>{children}</div>
</div>
)
@@ -1,3 +1,4 @@
import { useParams } from 'common'
import { useMemo } from 'react'
import { proxy, snapshot, useSnapshot } from 'valtio'
import { proxyMap } from 'valtio/utils'
@@ -109,3 +110,13 @@ export const useNotebooks = (projectRef: string) => {
[projectRef, snapshot.notebooks]
)
}
export const useCurrentNotebook = () => {
const { id, ref } = useParams()
const snapshot = useNotebooksStateSnapshot()
const currentNotebook = id ? snapshot.notebooks[id] : undefined
if (!currentNotebook || currentNotebook.projectRef !== ref) return undefined
return currentNotebook
}