diff --git a/.github/workflows/ai-tests.yml b/.github/workflows/ai-tests.yml index f8db88a83dcf3..fc38bce548228 100644 --- a/.github/workflows/ai-tests.yml +++ b/.github/workflows/ai-tests.yml @@ -38,7 +38,7 @@ jobs: sparse-checkout: | packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/authorize-vercel-deploys.yml b/.github/workflows/authorize-vercel-deploys.yml index 747cddbf5d947..7d6ce653a4ac5 100644 --- a/.github/workflows/authorize-vercel-deploys.yml +++ b/.github/workflows/authorize-vercel-deploys.yml @@ -31,7 +31,7 @@ jobs: sparse-checkout: | scripts patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/autofix_linters.yml b/.github/workflows/autofix_linters.yml index 4ddc87ba5629d..672c69f3756d8 100644 --- a/.github/workflows/autofix_linters.yml +++ b/.github/workflows/autofix_linters.yml @@ -36,7 +36,7 @@ jobs: apps patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 diff --git a/.github/workflows/braintrust-evals.yml b/.github/workflows/braintrust-evals.yml index dc903993293f7..4f6e25d0d95d3 100644 --- a/.github/workflows/braintrust-evals.yml +++ b/.github/workflows/braintrust-evals.yml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: run_install: false @@ -71,7 +71,7 @@ jobs: ref: ${{ github.head_ref || github.ref_name }} - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: run_install: false diff --git a/.github/workflows/braintrust-preview-scorers-deploy.yml b/.github/workflows/braintrust-preview-scorers-deploy.yml index 94c6ab8517938..7811b7360c06b 100644 --- a/.github/workflows/braintrust-preview-scorers-deploy.yml +++ b/.github/workflows/braintrust-preview-scorers-deploy.yml @@ -48,7 +48,7 @@ jobs: - name: Install pnpm if: steps.changed.outputs.skip != 'true' - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: run_install: false diff --git a/.github/workflows/braintrust-scorers-deploy.yml b/.github/workflows/braintrust-scorers-deploy.yml index 8298ba28f917d..cd5d7d83e6890 100644 --- a/.github/workflows/braintrust-scorers-deploy.yml +++ b/.github/workflows/braintrust-scorers-deploy.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: run_install: false diff --git a/.github/workflows/dashboard-pr-reminder.yml b/.github/workflows/dashboard-pr-reminder.yml index 0ea53f030ebe4..9b4470882f269 100644 --- a/.github/workflows/dashboard-pr-reminder.yml +++ b/.github/workflows/dashboard-pr-reminder.yml @@ -24,7 +24,7 @@ jobs: scripts patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-js-libs-update.yml b/.github/workflows/docs-js-libs-update.yml index a423b46502b67..617eebbce328f 100644 --- a/.github/workflows/docs-js-libs-update.yml +++ b/.github/workflows/docs-js-libs-update.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false ref: master - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-last-changed.yml b/.github/workflows/docs-last-changed.yml index 3e138de62b3d0..8779e70cc2082 100644 --- a/.github/workflows/docs-last-changed.yml +++ b/.github/workflows/docs-last-changed.yml @@ -31,7 +31,7 @@ jobs: apps/docs patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-mgmt-api-update.yml b/.github/workflows/docs-mgmt-api-update.yml index a455e489a03d8..951aeec76c92e 100644 --- a/.github/workflows/docs-mgmt-api-update.yml +++ b/.github/workflows/docs-mgmt-api-update.yml @@ -24,7 +24,7 @@ jobs: patches packages/generator - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-sync-auto-troubleshooting.yml b/.github/workflows/docs-sync-auto-troubleshooting.yml index 3e6ada03f6f2f..4d6884a03d14a 100644 --- a/.github/workflows/docs-sync-auto-troubleshooting.yml +++ b/.github/workflows/docs-sync-auto-troubleshooting.yml @@ -22,7 +22,7 @@ jobs: persist-credentials: true - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: run_install: false diff --git a/.github/workflows/docs-sync-troubleshooting.yml b/.github/workflows/docs-sync-troubleshooting.yml index 468b0f3fce138..4b671232b4ef3 100644 --- a/.github/workflows/docs-sync-troubleshooting.yml +++ b/.github/workflows/docs-sync-troubleshooting.yml @@ -31,7 +31,7 @@ jobs: apps/docs patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml index 3c8f90a05c995..7e6aa0ad0a3cb 100644 --- a/.github/workflows/docs-sync.yml +++ b/.github/workflows/docs-sync.yml @@ -34,7 +34,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-tests-smoke.yml b/.github/workflows/docs-tests-smoke.yml index 2fc1579b41632..dbf0259e058fc 100644 --- a/.github/workflows/docs-tests-smoke.yml +++ b/.github/workflows/docs-tests-smoke.yml @@ -27,7 +27,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml index 5acd99674a21e..b92cdead161a9 100644 --- a/.github/workflows/docs-tests.yml +++ b/.github/workflows/docs-tests.yml @@ -33,7 +33,7 @@ jobs: supabase patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/pg-meta-tests.yml b/.github/workflows/pg-meta-tests.yml index f46c0360cd1d3..006120baceb1c 100644 --- a/.github/workflows/pg-meta-tests.yml +++ b/.github/workflows/pg-meta-tests.yml @@ -32,7 +32,7 @@ jobs: packages/tsconfig patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 6b8a5b6e4ed7a..ed93ad9798cf5 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -28,7 +28,7 @@ jobs: i18n packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml index be8a466334b42..4a929c17e3562 100644 --- a/.github/workflows/search.yml +++ b/.github/workflows/search.yml @@ -51,7 +51,7 @@ jobs: supabase patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index 7840b6e618150..890ee7d2be283 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -51,7 +51,7 @@ jobs: - 'pnpm-lock.yaml' - '.github/workflows/studio-e2e-test.yml' - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 if: steps.filter.outputs.studio == 'true' name: Install pnpm with: diff --git a/.github/workflows/studio-lint-ratchet-decrease.yml b/.github/workflows/studio-lint-ratchet-decrease.yml index 937d25ed4b88a..0b0faabf5f356 100644 --- a/.github/workflows/studio-lint-ratchet-decrease.yml +++ b/.github/workflows/studio-lint-ratchet-decrease.yml @@ -23,7 +23,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/studio-lint-ratchet.yml b/.github/workflows/studio-lint-ratchet.yml index 51657bade07b2..2e4e91df52042 100644 --- a/.github/workflows/studio-lint-ratchet.yml +++ b/.github/workflows/studio-lint-ratchet.yml @@ -29,7 +29,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index d499d32112c48..bed6790239f47 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -46,7 +46,7 @@ jobs: relevant: - 'apps/studio/**' - 'pnpm-lock.yaml' - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 if: steps.filter.outputs.relevant == 'true' name: Install pnpm with: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 538d6eb3c2806..fc7643f580952 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -26,7 +26,7 @@ jobs: with: persist-credentials: false - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/ui-patterns-tests.yml b/.github/workflows/ui-patterns-tests.yml index be38c2332d827..6ccfbb0083aca 100644 --- a/.github/workflows/ui-patterns-tests.yml +++ b/.github/workflows/ui-patterns-tests.yml @@ -26,7 +26,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index aff844ce3d13f..056fe71db18bc 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -36,7 +36,7 @@ jobs: packages patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 if: steps.filter.outputs.relevant == 'true' name: Install pnpm with: diff --git a/.github/workflows/update-js-libs.yml b/.github/workflows/update-js-libs.yml index c0a59cc1fb56a..fd91d5d744ef9 100644 --- a/.github/workflows/update-js-libs.yml +++ b/.github/workflows/update-js-libs.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false ref: master - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/update-ssr.yml b/.github/workflows/update-ssr.yml index 542b8c041d1c5..c716a6f851613 100644 --- a/.github/workflows/update-ssr.yml +++ b/.github/workflows/update-ssr.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false ref: master - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/.github/workflows/www-tests.yml b/.github/workflows/www-tests.yml index bbc838989e421..a31cffd6d628e 100644 --- a/.github/workflows/www-tests.yml +++ b/.github/workflows/www-tests.yml @@ -33,7 +33,7 @@ jobs: supabase patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm with: run_install: false diff --git a/apps/docs/components/ContentListings/ContentListings.client.tsx b/apps/docs/components/ContentListings/ContentListings.client.tsx index 3d36bc1ef50ce..f8b3d4ee3107a 100644 --- a/apps/docs/components/ContentListings/ContentListings.client.tsx +++ b/apps/docs/components/ContentListings/ContentListings.client.tsx @@ -78,7 +78,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) { {item.badge} : undefined} > {item.description} diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index f0ff033351aed..45a0a7892cc69 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -5,7 +5,36 @@ description: 'Resources for getting started with Supabase.' hideToc: true --- - +
+ +
+ +
+ + + Develop with Supabase AI-first using plugins, MCP, and skills. + + + + + Learn about the different API keys in Supabase and how to use them. + + + + + Use the Supabase CLI to develop locally and collaborate between teams. + + +
+ +
+ +
### Use cases diff --git a/apps/docs/data/content-listings/getting-started.data.ts b/apps/docs/data/content-listings/getting-started.data.ts deleted file mode 100644 index 33d36e08e8b91..0000000000000 --- a/apps/docs/data/content-listings/getting-started.data.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ContentListingGroup } from '~/lib/content-listings.schema' - -export const gettingStartedGetStarted: ContentListingGroup = { - id: 'getting-started-overview', - type: 'grid', - items: [ - { - title: 'Build with AI tools', - href: '/guides/ai-tools', - description: 'Develop with Supabase AI-first using plugins, MCP, and skills.', - }, - { - title: 'API Keys', - href: '/guides/getting-started/api-keys', - description: 'Learn about the different API keys in Supabase and how to use them.', - }, - { - title: 'Local Development', - href: '/guides/local-development', - description: 'Use the Supabase CLI to develop locally and collaborate between teams.', - }, - ], -} diff --git a/apps/docs/data/content-listings/index.ts b/apps/docs/data/content-listings/index.ts index dc94dece7e8f0..c0e242c02a506 100644 --- a/apps/docs/data/content-listings/index.ts +++ b/apps/docs/data/content-listings/index.ts @@ -10,7 +10,6 @@ import { functionsExamplesWebhooksPayments, functionsGetStarted, } from './functions.data' -import { gettingStartedGetStarted } from './getting-started.data' import { realtimeExamples, realtimeGetStarted, realtimeResources } from './realtime.data' import { selfHostingCommunity, @@ -33,7 +32,6 @@ const ALL_GROUPS: readonly ContentListingGroup[] = [ functionsExamplesAiMedia, functionsExamplesMessaging, functionsExamplesOperations, - gettingStartedGetStarted, realtimeGetStarted, realtimeExamples, realtimeResources, diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 2399351a9598a..7fc5b607de38d 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -146,6 +146,7 @@ Joanna Michniewicz Joaquim Moreno Joel Low Joey Freeland +Joey Lei John Fitzgerald Joel Martin Johan Bergström diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index aa10baf4cf820..34f4efdcbeff5 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -1,372 +1,11 @@ -import { acceptUntrustedSql, untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta' -import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' -import { Loader2 } from 'lucide-react' -import dynamic from 'next/dynamic' -import { useCallback, useEffect, useEffectEvent, useState } from 'react' -import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' - -import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' -import { RunQueryWarningModal } from './RunQueryWarningModal' -import { appendEnableRLSStatements } from './SQLEditor.utils' -import { SQLEditorProvider, useSQLEditorContext } from './SQLEditorContext' -import { useAddDefinitions } from './useAddDefinitions' -import { useEditorMount } from './useEditorMount' -import { usePrettifyQuery } from './usePrettifyQuery' -import { useSnippetIdentity } from './useSnippetIdentity' -import { useSnippetTitleGenerator } from './useSnippetTitleGenerator' -import { useSqlEditorAi } from './useSqlEditorAi' -import { useSqlEditorExecution } from './useSqlEditorExecution' -import { useSqlEditorExplain } from './useSqlEditorExplain' -import { useSqlEditorShortcuts } from './useSqlEditorShortcuts' -import { UtilityActions } from './UtilityPanel/UtilityActions' -import { UtilityPanel } from './UtilityPanel/UtilityPanel' -import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget' -import { isValidConnString } from '@/data/fetchers' -import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { detectOS } from '@/lib/helpers' -import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' -import { - getSqlEditorV2StateSnapshot, - useSqlEditorV2StateSnapshot, -} from '@/state/sql-editor/sql-editor-state' -import { createTabId, useTabsStateSnapshot } from '@/state/tabs' - -// Load the monaco editor client-side only (does not behave well server-side) -const MonacoEditor = dynamic( - () => import('./MonacoEditor').then(({ MonacoEditor }) => MonacoEditor), - { ssr: false } -) -const DiffEditor = dynamic( - () => import('../../ui/DiffEditor').then(({ DiffEditor }) => DiffEditor), - { ssr: false } -) - -const SQLEditorContent = () => { - const { - editorRef, - monacoRef, - diffEditorRef, - scrollTopRef, - refocusEditor, - clearPendingRunRefocus, - markRefocusAfterRun, - getEditorSql: getEditorSqlFromEditor, - } = useSQLEditorContext() - - const os = detectOS() - const { ref } = useParams() - - const { data: project } = useSelectedProjectQuery() - - const tabs = useTabsStateSnapshot() - const snapV2 = useSqlEditorV2StateSnapshot() - const databaseSelectorState = useDatabaseSelectorStateSnapshot() - - // [Ali] Kill switch to hide the SQL Editor Explain tab and its entry points - const disablePrettyExplain = useFlag('DisablePrettyExplainOnSqlEditor') - - const diff = useSqlEditorDiff() - const { isDiffOpen, defaultSqlDiff } = diff - const prompt = useSqlEditorPrompt() - const { promptState, setPromptState, promptInput, setPromptInput, resetPrompt } = prompt - - const [hasSelection, setHasSelection] = useState(false) - const [activeUtilityTab, setActiveUtilityTab] = useState('results') - - const { id, urlId, generatedNewSnippetName, isLoading } = useSnippetIdentity() - const { onMount, editorMountCount } = useEditorMount({ id }) - - useAddDefinitions(id, monacoRef.current) - - const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery( - { - projectRef: ref, - }, - { enabled: isValidConnString(project?.connectionString) } - ) - - const { setAiTitle } = useSnippetTitleGenerator() - - const prettifyQuery = usePrettifyQuery({ id, isDiffOpen }) - - // Reads the SQL to run from the editor as an UntrustedSqlFragment. The - // untrusted→safe promotion (acceptUntrustedSql) happens in the run / explain - // gesture + warning-modal handlers below — as close to the user action as - // possible — before the SQL reaches the execute* pipelines. - const readEditorSql = useCallback((): UntrustedSqlFragment | undefined => { - const snippet = getSqlEditorV2StateSnapshot().snippets[id] - return getEditorSqlFromEditor(snippet?.snippet.content?.unchecked_sql) - }, [getEditorSqlFromEditor, id]) - - const { executeQuery, isExecuting, potentialIssues, resetPotentialIssues } = - useSqlEditorExecution({ - id, - isDiffOpen, - hasSelection, - activeUtilityTab, - setActiveUtilityTab, - setAiTitle, - }) - - const { executeExplainQuery, isExplainExecuting } = useSqlEditorExplain({ - id, - isDiffOpen, - setActiveUtilityTab, - }) - - // Run/explain gestures. These are the deliberate user actions, so the - // untrusted→safe promotion (acceptUntrustedSql) happens here — as close to the - // event handler as possible — before the SQL reaches the execute* pipelines. - const executeQueryFromButton = useCallback(() => { - markRefocusAfterRun() - refocusEditor() - const sql = readEditorSql() - if (sql === undefined) return clearPendingRunRefocus() - void executeQuery(acceptUntrustedSql(sql)) - }, [clearPendingRunRefocus, executeQuery, markRefocusAfterRun, readEditorSql, refocusEditor]) - - const handleRunShortcut = useCallback(() => { - const sql = readEditorSql() - if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql)) - }, [executeQuery, readEditorSql]) - - const handleRunExplain = useCallback(() => { - const sql = readEditorSql() - if (sql !== undefined) void executeExplainQuery(acceptUntrustedSql(sql)) - }, [executeExplainQuery, readEditorSql]) - - const { - handlePrompt, - acceptAiHandler, - discardAiHandler, - onDebug, - buildDebugPrompt, - handleDiffEditorMount, - isCompletionLoading, - showWidget, - } = useSqlEditorAi({ id, editorMountCount, diff, prompt }) - - useSqlEditorShortcuts({ - isDiffOpen, - isPromptOpen: promptState.isOpen, - disablePrettyExplain, - prettifyQuery, - runExplain: handleRunExplain, - acceptAiHandler, - discardAiHandler, - resetPrompt, - }) - - /** All useEffects are at the bottom before returning the TSX */ - - const saveScrollPosition = useEffectEvent((snippetId: string) => { - if (ref) { - const tabId = createTabId('sql', { id: snippetId }) - tabs.updateTab(tabId, { scrollTop: scrollTopRef.current }) - } - }) - useEffect(() => { - // Save the departing snippet's scroll position on unmount / snippet switch. - return () => saveScrollPosition(id) - // Temporary until we update eslint to ignore useEffectEvent - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]) - - useEffect(() => { - if (isSuccessReadReplicas) { - const primaryDatabase = databases.find((db) => db.identifier === ref) - databaseSelectorState.setSelectedDatabaseId(primaryDatabase?.identifier) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccessReadReplicas, databases, ref]) - - return ( - <> - { - clearPendingRunRefocus() - resetPotentialIssues() - refocusEditor() - }} - onConfirm={() => { - markRefocusAfterRun() - resetPotentialIssues() - refocusEditor() - // The user has reviewed the warning and confirmed — promote here. - const sql = readEditorSql() - if (sql === undefined) return clearPendingRunRefocus() - void executeQuery(acceptUntrustedSql(sql), true) - }} - onConfirmWithRLS={() => { - const tables = potentialIssues?.createTablesMissingRLS ?? [] - if (tables.length === 0) return - const baseSql = readEditorSql() ?? untrustedSql('') - const rewrittenSql = appendEnableRLSStatements(baseSql, tables) - markRefocusAfterRun() - resetPotentialIssues() - refocusEditor() - // The user has reviewed the warning and confirmed — promote here. - void executeQuery(acceptUntrustedSql(untrustedSql(rewrittenSql)), true) - }} - /> - -
- - - -
- {isLoading ? ( -
- -
- ) : ( - <> - {isDiffOpen && ( -
- - {showWidget && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection || defaultSqlDiff.modified, - afterSelection: promptState.afterSelection, - }) - }} - onAccept={acceptAiHandler} - onReject={discardAiHandler} - onCancel={resetPrompt} - isDiffVisible={true} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} -
- )} -
- { - setPromptState((prev) => ({ - ...prev, - isOpen: true, - selection, - beforeSelection, - afterSelection, - startLineNumber, - endLineNumber, - })) - }} - /> - {editorRef.current && promptState.isOpen && !isDiffOpen && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection, - afterSelection: promptState.afterSelection, - }) - }} - onCancel={resetPrompt} - isDiffVisible={false} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} -
- - )} -
-
- - - - - {isLoading ? ( -
- -
- ) : ( - - )} -
-
-
- - ) -} +import { SQLEditorProvider } from './SQLEditorContext' +import { SQLEditorControllersProvider } from './SQLEditorControllers' +import { SQLEditorLayout } from './SQLEditorLayout' export const SQLEditor = () => ( - + + + ) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts index 185196f6fcc54..8c87cea61a163 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.types.ts @@ -37,3 +37,6 @@ export type PotentialIssues = { hasAlterDatabasePreventConnection?: boolean createTablesMissingRLS?: { schema?: string; tableName: string }[] } + +/** The tabs available in the SQL editor's results/utility panel. */ +export type UtilityTab = 'results' | 'explain' diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx new file mode 100644 index 0000000000000..44b0998b55d2c --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorControllers.tsx @@ -0,0 +1,264 @@ +import { type UntrustedSqlFragment } from '@supabase/pg-meta' +import { useFlag, useParams } from 'common' +import { + createContext, + use, + useCallback, + useEffect, + useEffectEvent, + useMemo, + useState, + type Context, + type Dispatch, + type PropsWithChildren, + type SetStateAction, +} from 'react' + +import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' +import type { UtilityTab } from './SQLEditor.types' +import { useSQLEditorContext } from './SQLEditorContext' +import { useAddDefinitions } from './useAddDefinitions' +import { useEditorMount } from './useEditorMount' +import { usePrettifyQuery } from './usePrettifyQuery' +import { useSnippetIdentity } from './useSnippetIdentity' +import { useSnippetTitleGenerator } from './useSnippetTitleGenerator' +import { useSqlEditorAi } from './useSqlEditorAi' +import { useSqlEditorExecution } from './useSqlEditorExecution' +import { useSqlEditorExplain } from './useSqlEditorExplain' +import { useSqlEditorShortcuts } from './useSqlEditorShortcuts' +import { isValidConnString } from '@/data/fetchers' +import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' +import { + getSqlEditorV2StateSnapshot, + useSqlEditorV2StateSnapshot, +} from '@/state/sql-editor/sql-editor-state' +import { createTabId, useTabsStateSnapshot } from '@/state/tabs' + +/** + * Reactive SQL-editor controllers, distributed via a handful of focused + * contexts so the visual tree consumes exactly the slice it needs instead of + * prop-drilling whole hook bundles. The stable Monaco refs + imperative helpers + * live in `SQLEditorContext`; this provider owns everything that changes with + * state. + * + * SQL that a user runs is NOT promoted to safety here — the provider only + * exposes the already-safe `executeQuery` / `executeExplainQuery` pipelines plus + * `readEditorSql` (which returns an `UntrustedSqlFragment`). Each user-action + * site (the toolbar/editor/results components and the shortcut handlers) + * promotes with `acceptUntrustedSql` right where the user acts, so the promotion + * is auditable. + */ + +type SnippetContextValue = { + id: string + snippetName: string + isLoading: boolean +} + +type AssistantContextValue = { + diff: ReturnType + prompt: ReturnType + ai: ReturnType +} + +type SqlEditorExecution = ReturnType +type SqlEditorExplain = ReturnType +type SqlEditorMount = ReturnType + +/** Running SQL: the safe execute pipelines, their status, and formatting. */ +type RunContextValue = { + executeQuery: SqlEditorExecution['executeQuery'] + executeExplainQuery: SqlEditorExplain['executeExplainQuery'] + readEditorSql: () => UntrustedSqlFragment | undefined + isExecuting: boolean + isExplainExecuting: boolean + potentialIssues: SqlEditorExecution['potentialIssues'] + resetPotentialIssues: () => void + prettifyQuery: () => void +} + +/** Editor-surface UI state: selection, the active results tab, and mount. */ +type UiContextValue = { + hasSelection: boolean + setHasSelection: Dispatch> + activeUtilityTab: UtilityTab + setActiveUtilityTab: Dispatch> + onMount: SqlEditorMount['onMount'] +} + +const SnippetContext = createContext(null) +const AssistantContext = createContext(null) +const RunContext = createContext(null) +const UiContext = createContext(null) + +function useGuardedContext(context: Context, hookName: string): T { + const value = use(context) + if (!value) { + throw new Error(`${hookName} must be used within a SQLEditorControllersProvider`) + } + return value +} + +/** Snippet identity + display name for the current tab. */ +export const useSqlEditorSnippet = () => useGuardedContext(SnippetContext, 'useSqlEditorSnippet') + +/** The Assistant / diff cluster controllers (diff, prompt, ai). */ +export const useSqlEditorAssistant = () => + useGuardedContext(AssistantContext, 'useSqlEditorAssistant') + +/** Run / explain execution, warnings, and query formatting. */ +export const useSqlEditorRun = () => useGuardedContext(RunContext, 'useSqlEditorRun') + +/** Editor-surface UI state (selection, active results tab, editor mount). */ +export const useSqlEditorUi = () => useGuardedContext(UiContext, 'useSqlEditorUi') + +export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) => { + const { monacoRef, scrollTopRef, getEditorSql: getEditorSqlFromEditor } = useSQLEditorContext() + + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + + const tabs = useTabsStateSnapshot() + const snapV2 = useSqlEditorV2StateSnapshot() + const { setSelectedDatabaseId } = useDatabaseSelectorStateSnapshot() + + // [Ali] Kill switch to hide the SQL Editor Explain tab and its entry points + const disablePrettyExplain = useFlag('DisablePrettyExplainOnSqlEditor') + + const diff = useSqlEditorDiff() + const { isDiffOpen } = diff + const prompt = useSqlEditorPrompt() + const { promptState, resetPrompt } = prompt + + const [hasSelection, setHasSelection] = useState(false) + const [activeUtilityTab, setActiveUtilityTab] = useState('results') + + const { id, urlId, generatedNewSnippetName, isLoading } = useSnippetIdentity() + const { onMount, editorMountCount } = useEditorMount({ id }) + + useAddDefinitions(id, monacoRef.current) + + const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery( + { + projectRef: ref, + }, + { enabled: isValidConnString(project?.connectionString) } + ) + + const { setAiTitle } = useSnippetTitleGenerator() + + const prettifyQuery = usePrettifyQuery({ id, isDiffOpen }) + + // Reads the SQL to run from the editor as an UntrustedSqlFragment. Promotion + // to safety (acceptUntrustedSql) happens at each user-action site, never here. + const readEditorSql = useCallback((): UntrustedSqlFragment | undefined => { + const snippet = getSqlEditorV2StateSnapshot().snippets[id] + return getEditorSqlFromEditor(snippet?.snippet.content?.unchecked_sql) + }, [getEditorSqlFromEditor, id]) + + const { executeQuery, isExecuting, potentialIssues, resetPotentialIssues } = + useSqlEditorExecution({ + id, + isDiffOpen, + hasSelection, + activeUtilityTab, + setActiveUtilityTab, + setAiTitle, + }) + + const { executeExplainQuery, isExplainExecuting } = useSqlEditorExplain({ + id, + isDiffOpen, + setActiveUtilityTab, + }) + + const ai = useSqlEditorAi({ id, editorMountCount, diff, prompt }) + const { acceptAiHandler, discardAiHandler } = ai + + useSqlEditorShortcuts({ + isDiffOpen, + isPromptOpen: promptState.isOpen, + disablePrettyExplain, + prettifyQuery, + readEditorSql, + executeExplainQuery, + acceptAiHandler, + discardAiHandler, + resetPrompt, + }) + + const saveScrollPosition = useEffectEvent((snippetId: string) => { + if (ref) { + const tabId = createTabId('sql', { id: snippetId }) + tabs.updateTab(tabId, { scrollTop: scrollTopRef.current }) + } + }) + useEffect(() => { + // Save the departing snippet's scroll position on unmount / snippet switch. + return () => saveScrollPosition(id) + // Temporary until we update eslint to ignore useEffectEvent + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]) + + useEffect(() => { + if (isSuccessReadReplicas) { + const primaryDatabase = databases.find((db) => db.identifier === ref) + setSelectedDatabaseId(primaryDatabase?.identifier) + } + }, [isSuccessReadReplicas, databases, ref, setSelectedDatabaseId]) + + const snippetName = + urlId === 'new' + ? generatedNewSnippetName + : (snapV2.snippets[id]?.snippet.name ?? generatedNewSnippetName) + + const snippetValue = useMemo( + () => ({ id, snippetName, isLoading }), + [id, snippetName, isLoading] + ) + + const assistantValue = useMemo( + () => ({ diff, prompt, ai }), + [diff, prompt, ai] + ) + + const runValue = useMemo( + () => ({ + executeQuery, + executeExplainQuery, + readEditorSql, + isExecuting, + isExplainExecuting, + potentialIssues, + resetPotentialIssues, + prettifyQuery, + }), + [ + executeQuery, + executeExplainQuery, + readEditorSql, + isExecuting, + isExplainExecuting, + potentialIssues, + resetPotentialIssues, + prettifyQuery, + ] + ) + + const uiValue = useMemo( + () => ({ hasSelection, setHasSelection, activeUtilityTab, setActiveUtilityTab, onMount }), + [hasSelection, activeUtilityTab, onMount] + ) + + return ( + + + + {children} + + + + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx new file mode 100644 index 0000000000000..bc29180ca05ab --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx @@ -0,0 +1,189 @@ +import { acceptUntrustedSql } from '@supabase/pg-meta' +import { useFlag } from 'common' +import { Loader2 } from 'lucide-react' +import dynamic from 'next/dynamic' +import { useCallback } from 'react' +import { cn } from 'ui' + +import { useSQLEditorContext } from './SQLEditorContext' +import { + useSqlEditorAssistant, + useSqlEditorRun, + useSqlEditorSnippet, + useSqlEditorUi, +} from './SQLEditorControllers' +import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget' +import { detectOS } from '@/lib/helpers' + +// Load the monaco editor client-side only (does not behave well server-side) +const MonacoEditor = dynamic( + () => import('./MonacoEditor').then(({ MonacoEditor }) => MonacoEditor), + { ssr: false } +) +const DiffEditor = dynamic( + () => import('../../ui/DiffEditor').then(({ DiffEditor }) => DiffEditor), + { ssr: false } +) + +const generatePlaceholder = (os: string | undefined) => + `Hit ${os === 'macos' ? 'CMD+SHIFT+K' : 'CTRL+SHIFT+K'} to generate query or just start typing` + +/** + * The ask-AI widget anchored to the diff editor's modified pane. Reads the diff + * editor ref from context and assembles the prompt context from the current + * selection. + */ +const DiffPromptWidget = () => { + const { diffEditorRef } = useSQLEditorContext() + const { diff, prompt, ai } = useSqlEditorAssistant() + const { defaultSqlDiff } = diff + const { promptState, promptInput, setPromptInput, resetPrompt } = prompt + const { handlePrompt, acceptAiHandler, discardAiHandler, isCompletionLoading } = ai + + return ( + { + handlePrompt(value, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection || defaultSqlDiff.modified, + afterSelection: promptState.afterSelection, + }) + }} + onAccept={acceptAiHandler} + onReject={discardAiHandler} + onCancel={resetPrompt} + isDiffVisible={true} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + ) +} + +/** The diff editor + its ask-AI widget, shown while a diff is open. */ +const SQLEditorDiffView = () => { + const { diff, ai } = useSqlEditorAssistant() + const { defaultSqlDiff } = diff + const { handleDiffEditorMount, showWidget } = ai + + return ( +
+ + {showWidget && } +
+ ) +} + +/** The ask-AI widget anchored to the main editor. */ +const MainPromptWidget = () => { + const { editorRef } = useSQLEditorContext() + const { prompt, ai } = useSqlEditorAssistant() + const { promptState, promptInput, setPromptInput, resetPrompt } = prompt + const { handlePrompt, isCompletionLoading } = ai + + return ( + { + handlePrompt(value, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection, + afterSelection: promptState.afterSelection, + }) + }} + onCancel={resetPrompt} + isDiffVisible={false} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + ) +} + +/** + * The main Monaco editor + its ask-AI widget. Always mounted (hidden behind the + * diff editor while a diff is open) so the editor instance and its model survive + * the diff lifecycle. The `editorRef.current` reads in the placeholder and the + * widget gate happen during render and stay fresh because this component + * subscribes to `promptState` / `isDiffOpen` via context. + */ +const SQLEditorMainView = () => { + const { editorRef, monacoRef } = useSQLEditorContext() + const { id, snippetName } = useSqlEditorSnippet() + const { diff, prompt } = useSqlEditorAssistant() + const { isDiffOpen } = diff + const { promptState, openPrompt } = prompt + const { executeQuery, executeExplainQuery, readEditorSql, prettifyQuery } = useSqlEditorRun() + const { onMount, setHasSelection } = useSqlEditorUi() + + const os = detectOS() + const showExplainAction = !useFlag('DisablePrettyExplainOnSqlEditor') + + // Run/explain gestures from the editor — promote here, at the user action. + const runQuery = useCallback(() => { + const sql = readEditorSql() + if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql)) + }, [executeQuery, readEditorSql]) + + const runExplain = useCallback(() => { + const sql = readEditorSql() + if (sql !== undefined) void executeExplainQuery(acceptUntrustedSql(sql)) + }, [executeExplainQuery, readEditorSql]) + + return ( +
+ + {editorRef.current && promptState.isOpen && !isDiffOpen && } +
+ ) +} + +/** The top (editor) resizable panel: loading state, diff view, and main editor. */ +export const SQLEditorEditorPanel = () => { + const { isLoading } = useSqlEditorSnippet() + const { diff } = useSqlEditorAssistant() + + return ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {diff.isDiffOpen && } + + + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx new file mode 100644 index 0000000000000..85bc92e32717f --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx @@ -0,0 +1,146 @@ +import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta' +import { LOCAL_STORAGE_KEYS, useFlag } from 'common' +import { Loader2 } from 'lucide-react' +import { useCallback } from 'react' +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' + +import { RunQueryWarningModal } from './RunQueryWarningModal' +import type { UtilityTab } from './SQLEditor.types' +import { appendEnableRLSStatements } from './SQLEditor.utils' +import { useSQLEditorContext } from './SQLEditorContext' +import { + useSqlEditorAssistant, + useSqlEditorRun, + useSqlEditorSnippet, + useSqlEditorUi, +} from './SQLEditorControllers' +import { SQLEditorEditorPanel } from './SQLEditorEditorPanel' +import { UtilityActions } from './UtilityPanel/UtilityActions' +import { UtilityPanel } from './UtilityPanel/UtilityPanel' + +const SQLEditorRunWarningModal = () => { + const { refocusEditor, clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext() + const { potentialIssues, resetPotentialIssues, executeQuery, readEditorSql } = useSqlEditorRun() + + return ( + { + clearPendingRunRefocus() + resetPotentialIssues() + refocusEditor() + }} + onConfirm={() => { + markRefocusAfterRun() + resetPotentialIssues() + const sql = readEditorSql() + if (sql === undefined) return clearPendingRunRefocus() + // The user has reviewed the warning and confirmed — promote here. + void executeQuery(acceptUntrustedSql(sql), true) + }} + onConfirmWithRLS={() => { + const tables = potentialIssues?.createTablesMissingRLS ?? [] + if (tables.length === 0) return + const baseSql = readEditorSql() ?? untrustedSql('') + const rewrittenSql = appendEnableRLSStatements(baseSql, tables) + markRefocusAfterRun() + resetPotentialIssues() + // The user has reviewed the warning and confirmed — promote here. + void executeQuery(acceptUntrustedSql(untrustedSql(rewrittenSql)), true) + }} + /> + ) +} + +/** The top-bar controls (run / prettify / etc.). */ +const SQLEditorToolbar = () => { + const { clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext() + const { id } = useSqlEditorSnippet() + const { diff } = useSqlEditorAssistant() + const { executeQuery, readEditorSql, isExecuting, prettifyQuery } = useSqlEditorRun() + const { hasSelection } = useSqlEditorUi() + + // Run gesture from the toolbar button — promote here, at the user action. + const runQuery = useCallback(() => { + markRefocusAfterRun() + const sql = readEditorSql() + if (sql === undefined) return clearPendingRunRefocus() + void executeQuery(acceptUntrustedSql(sql)) + }, [clearPendingRunRefocus, executeQuery, markRefocusAfterRun, readEditorSql]) + + return ( + + ) +} + +/** The bottom (results) resizable panel: loading state + utility panel. */ +const SQLEditorResultsPanel = () => { + const { id, isLoading } = useSqlEditorSnippet() + const { diff, ai } = useSqlEditorAssistant() + const { executeExplainQuery, readEditorSql, isExecuting, isExplainExecuting } = useSqlEditorRun() + const { activeUtilityTab, setActiveUtilityTab } = useSqlEditorUi() + + const showExplainTab = !useFlag('DisablePrettyExplainOnSqlEditor') + + // Explain gesture from the results panel — promote here, at the user action. + const runExplain = useCallback(() => { + const sql = readEditorSql() + if (sql !== undefined) void executeExplainQuery(acceptUntrustedSql(sql)) + }, [executeExplainQuery, readEditorSql]) + + return isLoading ? ( +
+ +
+ ) : ( + setActiveUtilityTab(tab as UtilityTab)} + /> + ) +} + +/** The SQL editor shell: warning modal, toolbar, and the editor/results split. */ +export const SQLEditorLayout = () => { + return ( + <> + + +
+ + + + + + + + + + + + +
+ + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/hooks.ts b/apps/studio/components/interfaces/SQLEditor/hooks.ts index 7c7b2e391ce18..3f531c4aba4dc 100644 --- a/apps/studio/components/interfaces/SQLEditor/hooks.ts +++ b/apps/studio/components/interfaces/SQLEditor/hooks.ts @@ -95,17 +95,20 @@ export function useSqlEditorDiff() { setSelectedDiffType(undefined) }, []) - return { - sourceSqlDiff, - setSourceSqlDiff, - selectedDiffType, - setSelectedDiffType, - isAcceptDiffLoading, - setIsAcceptDiffLoading, - isDiffOpen, - defaultSqlDiff, - closeDiff, - } + return useMemo( + () => ({ + sourceSqlDiff, + setSourceSqlDiff, + selectedDiffType, + setSelectedDiffType, + isAcceptDiffLoading, + setIsAcceptDiffLoading, + isDiffOpen, + defaultSqlDiff, + closeDiff, + }), + [sourceSqlDiff, selectedDiffType, isAcceptDiffLoading, isDiffOpen, defaultSqlDiff, closeDiff] + ) } interface PromptState { @@ -136,16 +139,24 @@ export function useSqlEditorPrompt() { } }, [promptState.isOpen]) - const resetPrompt = () => { + const resetPrompt = useCallback(() => { setPromptState(initialPromptState) setPromptInput('') - } + }, []) - return { - promptState, - setPromptState, - promptInput, - setPromptInput, - resetPrompt, - } + const openPrompt = useCallback((context: Omit) => { + setPromptState((prev) => ({ ...prev, isOpen: true, ...context })) + }, []) + + return useMemo( + () => ({ + promptState, + setPromptState, + promptInput, + setPromptInput, + resetPrompt, + openPrompt, + }), + [promptState, promptInput, resetPrompt, openPrompt] + ) } diff --git a/apps/studio/components/interfaces/SQLEditor/useEditorMount.ts b/apps/studio/components/interfaces/SQLEditor/useEditorMount.ts index 12714c2e400f2..c5ff808e13539 100644 --- a/apps/studio/components/interfaces/SQLEditor/useEditorMount.ts +++ b/apps/studio/components/interfaces/SQLEditor/useEditorMount.ts @@ -1,8 +1,9 @@ -import { useState } from 'react' +import { useCallback, useContext, useState } from 'react' +import { snapshot } from 'valtio' import type { IStandaloneCodeEditor } from './SQLEditor.types' import { useSQLEditorContext } from './SQLEditorContext' -import { createTabId, useTabsStateSnapshot } from '@/state/tabs' +import { createTabId, TabsStateContext } from '@/state/tabs' /** * Owns the editor `onMount` handler (scroll-position restore + tracking) and the @@ -11,26 +12,31 @@ import { createTabId, useTabsStateSnapshot } from '@/state/tabs' */ export function useEditorMount({ id }: { id: string }) { const { scrollTopRef } = useSQLEditorContext() - const tabs = useTabsStateSnapshot() + // The proxy store (stable reference), read non-reactively in onMount so the + // callback identity doesn't churn on every tab-state change. + const tabsState = useContext(TabsStateContext) // Bumped on every editor mount (including the keyed remount on snippet switch) // so a diff request that arrived before the editor was ready gets re-processed. const [editorMountCount, setEditorMountCount] = useState(0) - const onMount = (editor: IStandaloneCodeEditor) => { - setEditorMountCount((count) => count + 1) + const onMount = useCallback( + (editor: IStandaloneCodeEditor) => { + setEditorMountCount((count) => count + 1) - const tabId = createTabId('sql', { id }) - const tabData = tabs.tabsMap[tabId] + const tabId = createTabId('sql', { id }) + const tabData = snapshot(tabsState).tabsMap[tabId] - // [Joshen] Tiny timeout to give a bit of time for the content to load before scrolling - setTimeout(() => { - if (tabData?.metadata?.scrollTop) { - editor.setScrollTop(tabData.metadata.scrollTop) - } - }, 20) - editor.onDidScrollChange((e) => (scrollTopRef.current = e.scrollTop)) - } + // [Joshen] Tiny timeout to give a bit of time for the content to load before scrolling + setTimeout(() => { + if (tabData?.metadata?.scrollTop) { + editor.setScrollTop(tabData.metadata.scrollTop) + } + }, 20) + editor.onDidScrollChange((e) => (scrollTopRef.current = e.scrollTop)) + }, + [id, scrollTopRef, tabsState] + ) return { onMount, editorMountCount } } diff --git a/apps/studio/components/interfaces/SQLEditor/useSqlEditorAi.ts b/apps/studio/components/interfaces/SQLEditor/useSqlEditorAi.ts index d49d4fa036c4e..ca0fec82805b9 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSqlEditorAi.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSqlEditorAi.ts @@ -1,6 +1,6 @@ import { useParams } from 'common' import { useRouter } from 'next/router' -import { useCallback, useEffect, useEffectEvent, useState } from 'react' +import { useCallback, useEffect, useEffectEvent, useMemo, useState } from 'react' import { toast } from 'sonner' import type { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' @@ -55,7 +55,7 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi } = diff const { promptState, setPromptState, resetPrompt } = prompt - const { editorRef, diffEditorRef } = useSQLEditorContext() + const { editorRef, diffEditorRef, refocusEditor } = useSQLEditorContext() const router = useRouter() const { ref } = useParams() @@ -163,6 +163,7 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi setSelectedDiffType(DiffType.Modification) resetPrompt() closeDiff() + refocusEditor() } finally { setIsAcceptDiffLoading(false) } @@ -178,13 +179,15 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi setSelectedDiffType, resetPrompt, closeDiff, + refocusEditor, ]) const discardAiHandler = useCallback(() => { track('assistant_sql_diff_handler_evaluated', { handlerAccepted: false }) resetPrompt() closeDiff() - }, [closeDiff, resetPrompt, track]) + refocusEditor() + }, [closeDiff, resetPrompt, track, refocusEditor]) const complete = useCallback( async ( @@ -244,46 +247,54 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi ] ) - const handlePrompt = async ( - prompt: string, - context: { - beforeSelection: string - selection: string - afterSelection: string - } - ) => { - try { - setPromptState((prev) => ({ - ...prev, - selection: context.selection, - beforeSelection: context.beforeSelection, - afterSelection: context.afterSelection, - })) - const headerData = await constructHeaders() - - const authorizationHeader = headerData.get('Authorization') - - await complete(prompt, { - ...(authorizationHeader ? { headers: { Authorization: authorizationHeader } } : undefined), - body: { - completionMetadata: { - textBeforeCursor: context.beforeSelection, - textAfterCursor: context.afterSelection, - language: 'pgsql', - prompt, - selection: context.selection, + const handlePrompt = useCallback( + async ( + prompt: string, + context: { + beforeSelection: string + selection: string + afterSelection: string + } + ) => { + try { + setPromptState((prev) => ({ + ...prev, + selection: context.selection, + beforeSelection: context.beforeSelection, + afterSelection: context.afterSelection, + })) + const headerData = await constructHeaders() + + const authorizationHeader = headerData.get('Authorization') + + await complete(prompt, { + ...(authorizationHeader + ? { headers: { Authorization: authorizationHeader } } + : undefined), + body: { + completionMetadata: { + textBeforeCursor: context.beforeSelection, + textAfterCursor: context.afterSelection, + language: 'pgsql', + prompt, + selection: context.selection, + }, }, - }, - }) - } catch (error) { - setPromptState((prev) => ({ ...prev, isLoading: false })) - } - } + }) + } catch (error) { + setPromptState((prev) => ({ ...prev, isLoading: false })) + } + }, + [complete, setPromptState] + ) - const handleDiffEditorMount = (editor: IStandaloneDiffEditor) => { - diffEditorRef.current = editor - setIsDiffEditorMounted(true) - } + const handleDiffEditorMount = useCallback( + (editor: IStandaloneDiffEditor) => { + diffEditorRef.current = editor + setIsDiffEditorMounted(true) + }, + [diffEditorRef] + ) const resetDiff = useEffectEvent(() => { if (id) { @@ -360,14 +371,26 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi } }, [diffEditorRef, isDiffOpen, isDiffEditorMounted]) - return { - handlePrompt, - acceptAiHandler, - discardAiHandler, - onDebug, - buildDebugPrompt, - handleDiffEditorMount, - isCompletionLoading, - showWidget, - } + return useMemo( + () => ({ + handlePrompt, + acceptAiHandler, + discardAiHandler, + onDebug, + buildDebugPrompt, + handleDiffEditorMount, + isCompletionLoading, + showWidget, + }), + [ + handlePrompt, + acceptAiHandler, + discardAiHandler, + onDebug, + buildDebugPrompt, + handleDiffEditorMount, + isCompletionLoading, + showWidget, + ] + ) } diff --git a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts index 2eadbbf5dba51..00a49baf654f2 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExecution.ts @@ -5,7 +5,7 @@ import { useCallback, useState } from 'react' import { toast } from 'sonner' import { untitledSnippetTitle } from './SQLEditor.constants' -import type { PotentialIssues } from './SQLEditor.types' +import type { PotentialIssues, UtilityTab } from './SQLEditor.types' import { checkAlterDatabaseConnection, checkDestructiveQuery, @@ -39,8 +39,8 @@ type UseSqlEditorExecutionArgs = { id: string isDiffOpen: boolean hasSelection: boolean - activeUtilityTab: string - setActiveUtilityTab: (tab: string) => void + activeUtilityTab: UtilityTab + setActiveUtilityTab: (tab: UtilityTab) => void setAiTitle: (id: string, sql: string) => void } diff --git a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExplain.ts b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExplain.ts index 48386d6e400a5..32c4cfe8f00a0 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSqlEditorExplain.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSqlEditorExplain.ts @@ -3,6 +3,7 @@ import { useParams } from 'common' import { useCallback } from 'react' import { toast } from 'sonner' +import type { UtilityTab } from './SQLEditor.types' import { buildExplainSql } from './SQLEditor.utils' import { useSQLEditorContext } from './SQLEditorContext' import { splitSqlStatements } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils' @@ -20,7 +21,7 @@ import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-sessi type UseSqlEditorExplainArgs = { id: string isDiffOpen: boolean - setActiveUtilityTab: (tab: string) => void + setActiveUtilityTab: (tab: UtilityTab) => void } export function useSqlEditorExplain({ diff --git a/apps/studio/components/interfaces/SQLEditor/useSqlEditorShortcuts.ts b/apps/studio/components/interfaces/SQLEditor/useSqlEditorShortcuts.ts index 4f119980d36ef..19204b6b1c4d0 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSqlEditorShortcuts.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSqlEditorShortcuts.ts @@ -1,3 +1,8 @@ +import { + acceptUntrustedSql, + type SafeSqlFragment, + type UntrustedSqlFragment, +} from '@supabase/pg-meta' import { useParams } from 'common' import { useRouter } from 'next/router' import { useCallback, useEffect } from 'react' @@ -12,7 +17,8 @@ type UseSqlEditorShortcutsArgs = { isPromptOpen: boolean disablePrettyExplain: boolean prettifyQuery: () => void - runExplain: () => void + readEditorSql: () => UntrustedSqlFragment | undefined + executeExplainQuery: (sql: SafeSqlFragment) => unknown acceptAiHandler: () => void discardAiHandler: () => void resetPrompt: () => void @@ -28,7 +34,8 @@ export function useSqlEditorShortcuts({ isPromptOpen, disablePrettyExplain, prettifyQuery, - runExplain, + readEditorSql, + executeExplainQuery, acceptAiHandler, discardAiHandler, resetPrompt, @@ -46,6 +53,15 @@ export function useSqlEditorShortcuts({ router.push(`/project/${ref}/sql/new?skip=true`) }, [ref, router]) + // The Explain keyboard shortcut is an explicit user action, so the + // untrusted→safe promotion (acceptUntrustedSql) happens here in the shortcut + // handler itself — right next to where it is registered — before the SQL + // reaches the explain pipeline. + const runExplainShortcut = useCallback(() => { + const sql = readEditorSql() + if (sql !== undefined) void executeExplainQuery(acceptUntrustedSql(sql)) + }, [executeExplainQuery, readEditorSql]) + useShortcut(SHORTCUT_IDS.SQL_EDITOR_FOCUS_EDITOR, refocusEditor, { registerInCommandMenu: true, }) @@ -58,7 +74,7 @@ export function useSqlEditorShortcuts({ registerInCommandMenu: true, }) - useShortcut(SHORTCUT_IDS.SQL_EDITOR_EXPLAIN, runExplain, { + useShortcut(SHORTCUT_IDS.SQL_EDITOR_EXPLAIN, runExplainShortcut, { enabled: !disablePrettyExplain, registerInCommandMenu: true, }) diff --git a/apps/studio/components/layouts/Navigation/LayoutHeader/AdminStudioButton.tsx b/apps/studio/components/layouts/Navigation/LayoutHeader/AdminStudioButton.tsx new file mode 100644 index 0000000000000..70069950c82b2 --- /dev/null +++ b/apps/studio/components/layouts/Navigation/LayoutHeader/AdminStudioButton.tsx @@ -0,0 +1,37 @@ +import { useFlag, useParams } from 'common' +import { Wrench } from 'lucide-react' +import Link from 'next/link' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { ADMIN_STUDIO_URL, IS_PLATFORM } from '@/lib/constants' + +export const AdminStudioButton = () => { + const params = useParams() + const adminStudioLinkEnabled = useFlag('adminStudioLink') + + const isVisible = IS_PLATFORM ? adminStudioLinkEnabled : false + + if (!isVisible || !ADMIN_STUDIO_URL || !params.ref) return null + + return ( + } + tooltip={{ + content: { + text: 'Open in Admin Studio', + side: 'bottom', + align: 'center', + }, + }} + > + + + ) +} diff --git a/apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx index c5dea166e95b1..b1e0b34bd3d85 100644 --- a/apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx @@ -8,6 +8,7 @@ import { ReactNode, useMemo } from 'react' import { Badge, cn } from 'ui' import { CommandMenuTriggerInput } from 'ui-patterns/CommandMenu' +import { AdminStudioButton } from './AdminStudioButton' import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown' import { HomeIcon } from './HomeIcon' @@ -216,6 +217,7 @@ export const LayoutHeader = ({ }} > {IS_PLATFORM && } + )} diff --git a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx index 82db3d0c7d32d..211639e3e9b79 100644 --- a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx +++ b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx @@ -109,6 +109,7 @@ export const HelpPanel = ({