From 77c5a0b9d941bc75719645d9cd0639296ceff586 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 13 Aug 2026 08:44:27 +0700 Subject: [PATCH] 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 image image ## 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. --- .../interfaces/Explorer/ExplorerHome.tsx | 18 +- .../index.tsx => ExplorerQuery.tsx} | 0 .../index.tsx => ExplorerToolbar.tsx} | 92 +++++++-- .../interfaces/Explorer/MarkdownCell.tsx | 33 ++- .../interfaces/Explorer/NotebookEditor.tsx | 115 +++-------- .../interfaces/Explorer/QueryCell/index.tsx | 171 ++++++++++++++++ .../interfaces/Explorer/QueryResultTable.tsx | 188 ++++++++++++++++++ .../ExplorerQuery.test.tsx | 4 +- .../ExplorerToolbar.test.tsx | 21 +- .../components/interfaces/Explorer/hooks.ts | 60 +++--- .../components/interfaces/Explorer/types.ts | 7 + .../interfaces/ProjectHome/Home.tsx | 2 +- .../SQLEditor/UtilityPanel/Results.tsx | 8 +- apps/studio/components/ui/SortableSection.tsx | 14 +- .../studio/state/notebooks/notebooks-state.ts | 11 + 15 files changed, 581 insertions(+), 163 deletions(-) rename apps/studio/components/interfaces/Explorer/{ExplorerQuery/index.tsx => ExplorerQuery.tsx} (100%) rename apps/studio/components/interfaces/Explorer/{ExplorerToolbar/index.tsx => ExplorerToolbar.tsx} (55%) create mode 100644 apps/studio/components/interfaces/Explorer/QueryCell/index.tsx create mode 100644 apps/studio/components/interfaces/Explorer/QueryResultTable.tsx rename apps/studio/components/interfaces/Explorer/{ExplorerQuery => __tests__}/ExplorerQuery.test.tsx (99%) rename apps/studio/components/interfaces/Explorer/{ExplorerToolbar => __tests__}/ExplorerToolbar.test.tsx (76%) create mode 100644 apps/studio/components/interfaces/Explorer/types.ts diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHome.tsx b/apps/studio/components/interfaces/Explorer/ExplorerHome.tsx index 434903793dd..b26f10ed33e 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHome.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerHome.tsx @@ -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()} /> } 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, + }, + ], + }) + } /> diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQuery/index.tsx b/apps/studio/components/interfaces/Explorer/ExplorerQuery.tsx similarity index 100% rename from apps/studio/components/interfaces/Explorer/ExplorerQuery/index.tsx rename to apps/studio/components/interfaces/Explorer/ExplorerQuery.tsx diff --git a/apps/studio/components/interfaces/Explorer/ExplorerToolbar/index.tsx b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx similarity index 55% rename from apps/studio/components/interfaces/Explorer/ExplorerToolbar/index.tsx rename to apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx index 6b21debb933..14e434db488 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerToolbar/index.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx @@ -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, 'children'> & { + children: string + onSaveTitle?: (value: string) => void +} /** Flexible title region for static text or an editable resource name. */ -const ExplorerToolbarTitle = ({ className, ...props }: ExplorerToolbarTitleProps) => ( -
-) +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 ( +
+ {isEditingTitle ? ( + 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 ? ( + + ) : ( + title + )} +
+ ) +} ExplorerToolbarTitle.displayName = 'ExplorerToolbarTitle' export type ExplorerToolbarActionsProps = React.ComponentProps<'div'> @@ -70,7 +132,9 @@ ExplorerToolbarActions.displayName = 'ExplorerToolbarActions' export type ExplorerToolbarActionProps = Omit< React.ComponentPropsWithRef, '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) => ( - + ) ExplorerToolbarAction.displayName = 'ExplorerToolbarAction' diff --git a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx index bdcc73f03f9..b08550a4408 100644 --- a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx +++ b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx @@ -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 ( {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) + ) }} />
@@ -75,7 +88,11 @@ export const MarkdownCell = ({ cell, onCommitChanges }: MarkdownCellProps) => { -
diff --git a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx index 11f42cf233c..bb680a21b1d 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx +++ b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx @@ -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(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 (
-
-
+ + - {isEditingTitle ? ( - 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() - } - }} - /> - ) : ( - - )} -
-
- - } - className="px-1" - tooltip={{ content: { side: 'bottom', text: 'Run notebook' } }} - /> - } - className="px-1" - tooltip={{ content: { side: 'bottom', text: 'Save changes' } }} - /> -
-
+ + } tooltip="Run notebook" /> + } tooltip="Save changes" /> + +
-
+
{cells.length === 0 && ( } @@ -163,18 +113,15 @@ export const NotebookEditor = () => { items={cells.map((cell) => cell.id)} strategy={verticalListSortingStrategy} > -
+
{cells.map((cell) => { switch (cell._tag) { case 'markdown_cell': - return ( - handleUpdateCellText(cell.id, text)} - /> - ) + return + case 'database_cell': + return + case 'log_cell': // [Joshen] Will eventually hook it up return null diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx new file mode 100644 index 00000000000..bc3846d41d8 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -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(cell.unchecked_sql) + const [result, setResult] = useState() + + 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 ( + + + + + + + handleUpdateCell({ title: newTitle })}> + {title} + + + } + tooltip="Result settings" + /> + : } + tooltip={showQuery ? 'Hide query' : 'Show query'} + onClick={() => setShowQuery((prev) => !prev)} + /> + } + tooltip="Run query" + onClick={onRunQuery} + /> + + + + {showQuery && ( + + setValue(v ?? '')} + className="h-32" + actions={{ runQuery: { enabled: true, callback: onRunQuery } }} + onMount={(editor) => { + editor.onDidBlurEditorWidget(() => + handleUpdateCellRef.current({ sql: valueRef.current }) + ) + }} + /> + + )} + + + + + + +

{(result?.rows ?? []).length.toLocaleString()} rows

+

ยท

+

Limit {row_limit} rows

+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Explorer/QueryResultTable.tsx b/apps/studio/components/interfaces/Explorer/QueryResultTable.tsx new file mode 100644 index 00000000000..b3aab76c07a --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryResultTable.tsx @@ -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

Run the query to see results

+ } + + if (error) { + return + } + + if ((rows ?? []).length === 0) { + return

Success. No rows returned

+ } + + if (rows && rows.length > 0) { + return + } +} + +const QueryError = ({ + error, + autoLimit, +}: { + error: NonNullable + 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 ( +
+
+ {isTimeout ? ( +
+

+ Error: SQL query ran into an upstream timeout +

+

+ You can either{' '} + + optimize your query + + , or{' '} + + increase the statement timeout + + {' or '} + + . +

+
+ ) : ( +
+ {errorLines.length > 0 ? ( + errorLines.map((x: string, i: number) => ( +
+                  {x}
+                
+ )) + ) : ( +

Error: {error.message}

+ )} + {!isTimeout && !isNetWorkError && autoLimit && ( +

+ 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. +

+ )} + {readReplicaError && ( +

+ Note: Read replicas are for read only queries. Run write queries on the primary + database instead. +

+ )} + {payloadTooLargeError && ( +

+ Run this query by{' '} + + . +

+ )} +
+ )} + +
+ {readReplicaError && ( + + )} + {errorLines.length > 0 && ( + + + + + + Copy error + + + )} + {!hasHipaaAddon && ( + // [Joshen] TODO + ''} + onOpenAssistant={() => {}} + disabled={false} + loading={false} + /> + )} +
+
+
+ ) +} + +// [Joshen] Eventually migrate the Results component here from SQL Editor +const QueryResults = ({ rows }: { rows: NonNullable }) => { + return +} diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerQuery.test.tsx similarity index 99% rename from apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx rename to apps/studio/components/interfaces/Explorer/__tests__/ExplorerQuery.test.tsx index f01c68a66e9..7df4c9fc8e0 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerQuery.test.tsx @@ -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', () => { diff --git a/apps/studio/components/interfaces/Explorer/ExplorerToolbar/ExplorerToolbar.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerToolbar.test.tsx similarity index 76% rename from apps/studio/components/interfaces/Explorer/ExplorerToolbar/ExplorerToolbar.test.tsx rename to apps/studio/components/interfaces/Explorer/__tests__/ExplorerToolbar.test.tsx index b1cc53ed922..6b748059c21 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerToolbar/ExplorerToolbar.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerToolbar.test.tsx @@ -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() - - // 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() - - const toolbar = screen.getByRole('toolbar') - expect(toolbar).toHaveClass('h-12') - expect(toolbar).not.toHaveClass('h-10') - }) }) diff --git a/apps/studio/components/interfaces/Explorer/hooks.ts b/apps/studio/components/interfaces/Explorer/hooks.ts index cdb0f932839..4ddb6c9e012 100644 --- a/apps/studio/components/interfaces/Explorer/hooks.ts +++ b/apps/studio/components/interfaces/Explorer/hooks.ts @@ -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, diff --git a/apps/studio/components/interfaces/Explorer/types.ts b/apps/studio/components/interfaces/Explorer/types.ts new file mode 100644 index 00000000000..91c1087fd09 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/types.ts @@ -0,0 +1,7 @@ +import { type ResponseError } from '@/types' + +export type QueryResult = { + rows?: Record[] + error?: ResponseError + autoLimit?: number +} diff --git a/apps/studio/components/interfaces/ProjectHome/Home.tsx b/apps/studio/components/interfaces/ProjectHome/Home.tsx index cde13bf4c2d..2935c876955 100644 --- a/apps/studio/components/interfaces/ProjectHome/Home.tsx +++ b/apps/studio/components/interfaces/ProjectHome/Home.tsx @@ -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() diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx index 393f243f8e0..ec9087f524e 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx @@ -79,11 +79,9 @@ export const Results = ({ rows }: { rows: readonly any[] }) => { return ( <> {rows.length === 0 ? ( -
-

- Success. No rows returned -

-
+

+ Success. No rows returned +

) : ( <> diff --git a/apps/studio/components/ui/SortableSection.tsx b/apps/studio/components/ui/SortableSection.tsx index 09295a0b94d..3484e61e49e 100644 --- a/apps/studio/components/ui/SortableSection.tsx +++ b/apps/studio/components/ui/SortableSection.tsx @@ -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 = ({
- + icon={} + />
{children}
) diff --git a/apps/studio/state/notebooks/notebooks-state.ts b/apps/studio/state/notebooks/notebooks-state.ts index faf55794eff..3f713c147d7 100644 --- a/apps/studio/state/notebooks/notebooks-state.ts +++ b/apps/studio/state/notebooks/notebooks-state.ts @@ -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 +}