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