From 7cb7377098551ef55e94949aefb3ab8405df4ddd Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Thu, 13 Aug 2026 21:56:03 +0530 Subject: [PATCH 1/7] Add Satya Rohith Gannamanedi to humans.txt (#48890) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update to add myself (Satya) to humans.txt ## What is the current behavior? NA ## What is the new behavior? Adds new team member ## Additional context Part of my onboarding process. ## Summary by CodeRabbit * **Documentation** * Added Satya Rohith Gannamanedi to the project team listing. --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 4a42be68f318c..99aaeebe6e1f4 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -267,6 +267,7 @@ Samir Ketema Sana Cordeaux Sasi Kanumuri Sara Read +Satya Rohith Gannamanedi Sean Oliver Sean Romberg Sean Thompson From f89c362b2654e7831d49b627d2eebbcd6428a2bf Mon Sep 17 00:00:00 2001 From: Barry Roodt Date: Thu, 13 Aug 2026 18:26:15 +0200 Subject: [PATCH 2/7] fix(docs): accept a GitHub token for docs content reads (#48364) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. Complete App configurations produce the same auth options as before. ## What is the current behavior? Without the docs GitHub App private key, two things fail for a contributor: - `pnpm run embeddings` aborts before doing any work. The lint warnings source throws, and every source shares one `Promise.all` in [`fetchAllSources()`](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/sources/index.ts). - `pnpm --filter docs build` exits 1 in prebuild, so the `npm run build` pre-flight CONTRIBUTING.md asks for cannot run either: ``` Error: DOCS_GITHUB_APP_PRIVATE_KEY environment variable is required at octokit (apps/docs/lib/octokit.ts:21:13) at fetchAiSkills (apps/docs/scripts/federated-content/fetch-federated-content.ts:258:36) ``` Both read public content, so this is a rate-limit guard rather than access control: App auth landed in #43015 because unauthenticated calls (60 req/hr per IP) went flaky on shared runners. ## What is the new behavior? `apps/docs/lib/octokit.auth.ts` adds one rung below the App: a token from `GH_TOKEN`, then `GITHUB_TOKEN` (the precedence [`gh help environment`](https://cli.github.com/manual/gh_help_environment) documents), so `export GH_TOKEN=$(gh auth token)` is enough to build locally. Still authenticated, so #43015's fix holds, and still an authenticated Octokit client, so #44274 holds. A partially configured App is now an error naming the missing vars, rather than falling through to a token. Used by the lint warnings loader and `lib/octokit.ts`. The two token vars are declared in `apps/docs/turbo.jsonc` for `turbo/no-undeclared-env-vars`. ## Additional context With only `GH_TOKEN` set, `turbo run build --filter=docs --force` passes 4/4 and search-index source loading completes. `pnpm test` passes (20 files, 164 tests), and `tsc --noEmit` plus `pnpm run lint` match `origin/master`. For a complete App config the auth options are identical to before. Happy to post the fuller verification as a comment. ## Summary by CodeRabbit - **New Features** - Added flexible GitHub authentication for documentation services, supporting GitHub App credentials or personal access tokens. - GitHub App authentication is preferred when fully configured, with token-based fallback when unavailable. - Added support for both `GH_TOKEN` and `GITHUB_TOKEN`, with clear precedence rules. - **Bug Fixes** - Improved configuration validation with clear errors for missing or incomplete authentication settings. - Standardized authentication across GitHub content and lint-warning retrieval. --- apps/docs/lib/octokit.auth.test.ts | 88 +++++++++++++++++++ apps/docs/lib/octokit.auth.ts | 70 +++++++++++++++ apps/docs/lib/octokit.ts | 23 +---- .../search/sources/lint-warnings-guide.ts | 21 +---- apps/docs/turbo.jsonc | 2 + 5 files changed, 165 insertions(+), 39 deletions(-) create mode 100644 apps/docs/lib/octokit.auth.test.ts create mode 100644 apps/docs/lib/octokit.auth.ts diff --git a/apps/docs/lib/octokit.auth.test.ts b/apps/docs/lib/octokit.auth.test.ts new file mode 100644 index 0000000000000..e34d0e43875c7 --- /dev/null +++ b/apps/docs/lib/octokit.auth.test.ts @@ -0,0 +1,88 @@ +import crypto from 'node:crypto' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { githubAuthOptions } from './octokit.auth.js' + +const APP_ID = '123456' +const INSTALLATION_ID = '7890' +// PKCS1 on purpose: the App rung has to convert it, and universal-github-app-jwt +// only accepts PKCS8. +const PKCS1_PRIVATE_KEY = crypto + .generateKeyPairSync('rsa', { modulusLength: 2048 }) + .privateKey.export({ type: 'pkcs1', format: 'pem' }) + .toString() + +function stubEnv(env: Record) { + // Empty string rather than `undefined`: some Vitest versions stringify the + // latter to 'undefined', which is truthy and would silently defeat these tests. + for (const name of [ + 'DOCS_GITHUB_APP_ID', + 'DOCS_GITHUB_APP_INSTALLATION_ID', + 'DOCS_GITHUB_APP_PRIVATE_KEY', + 'GH_TOKEN', + 'GITHUB_TOKEN', + ]) { + vi.stubEnv(name, env[name] ?? '') + } +} + +const APP_ENV = { + DOCS_GITHUB_APP_ID: APP_ID, + DOCS_GITHUB_APP_INSTALLATION_ID: INSTALLATION_ID, + DOCS_GITHUB_APP_PRIVATE_KEY: PKCS1_PRIVATE_KEY, +} + +describe('githubAuthOptions', () => { + afterEach(() => vi.unstubAllEnvs()) + + it('authenticates as the App and converts the key to PKCS8', () => { + stubEnv(APP_ENV) + const options = githubAuthOptions() + if (!('authStrategy' in options)) throw new Error('expected App auth') + expect(options.auth).toMatchObject({ appId: APP_ID, installationId: INSTALLATION_ID }) + expect(options.auth.privateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/) + }) + + it('prefers the App when a token is also present', () => { + stubEnv({ ...APP_ENV, GITHUB_TOKEN: 'ghp_example' }) + expect(githubAuthOptions()).toHaveProperty('authStrategy') + }) + + it.each(['GH_TOKEN', 'GITHUB_TOKEN'])( + 'authenticates with a token from %s when the App is not configured', + (name) => { + stubEnv({ [name]: 'ghp_example' }) + expect(githubAuthOptions()).toEqual({ auth: 'ghp_example' }) + } + ) + + it('gives GH_TOKEN precedence over GITHUB_TOKEN, as the gh CLI documents', () => { + stubEnv({ GH_TOKEN: 'ghp_from_gh', GITHUB_TOKEN: 'ghp_from_actions' }) + expect(githubAuthOptions()).toEqual({ auth: 'ghp_from_gh' }) + }) + + it('refuses a partially configured App instead of masking it with a token', () => { + stubEnv({ DOCS_GITHUB_APP_ID: APP_ID, GITHUB_TOKEN: 'ghp_example' }) + expect(githubAuthOptions).toThrow(/Incomplete GitHub App configuration/) + // Names what is missing, and not what is already set. + expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_INSTALLATION_ID/) + expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_PRIVATE_KEY/) + expect(githubAuthOptions).not.toThrow(/DOCS_GITHUB_APP_ID\b/) + }) + + it('reports only the missing App var when one is absent', () => { + stubEnv({ + DOCS_GITHUB_APP_ID: APP_ID, + DOCS_GITHUB_APP_INSTALLATION_ID: INSTALLATION_ID, + GITHUB_TOKEN: 'ghp_example', + }) + expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_PRIVATE_KEY not set\. Set all three/) + }) + + it('names every credential option when none is set', () => { + stubEnv({}) + expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_ID/) + expect(githubAuthOptions).toThrow(/GH_TOKEN/) + expect(githubAuthOptions).toThrow(/GITHUB_TOKEN/) + }) +}) diff --git a/apps/docs/lib/octokit.auth.ts b/apps/docs/lib/octokit.auth.ts new file mode 100644 index 0000000000000..0ce042ce336e7 --- /dev/null +++ b/apps/docs/lib/octokit.auth.ts @@ -0,0 +1,70 @@ +import { createAppAuth } from '@octokit/auth-app' +import crypto from 'node:crypto' + +type AppAuth = { appId: string; installationId: string; privateKey: string } + +/** + * Octokit auth options for reading public content from GitHub. + * + * Prefers the docs GitHub App (CI and production). Falls back to a personal + * access token so contributors can run the search index build locally without + * the App's private key: `GH_TOKEN` then `GITHUB_TOKEN`, matching the + * precedence the GitHub CLI documents (`gh help environment`), so an + * already-exported token just works. + * + * Both rungs authenticate on purpose: unauthenticated calls are limited to + * 60 req/hr per IP, which is what caused the flaky CI failures fixed in #43015, + * and callers here fetch one file per request. Env is read on each call rather + * than at module scope so the choice reflects the environment at call time. + * + * A partially configured App is an error rather than a token fall-back: a + * rotated-out or misnamed secret would otherwise be masked by whatever token + * happens to be in the environment, quietly reading as the wrong identity. + * + * Deliberately free of `server-only` imports: the search index scripts use this + * too, and they run outside Next. + */ +export function githubAuthOptions(): + | { authStrategy: typeof createAppAuth; auth: AppAuth } + | { auth: string } { + const appId = process.env.DOCS_GITHUB_APP_ID + const installationId = process.env.DOCS_GITHUB_APP_INSTALLATION_ID + const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY + + if (appId && installationId && privateKey) { + return { + authStrategy: createAppAuth, + auth: { + appId, + installationId, + // https://github.com/gr2m/universal-github-app-jwt?tab=readme-ov-file#converting-pkcs1-to-pkcs8 + privateKey: crypto + .createPrivateKey(privateKey) + .export({ type: 'pkcs8', format: 'pem' }) + .toString(), + }, + } + } + + const appVars: Array<[string, string | undefined]> = [ + ['DOCS_GITHUB_APP_ID', appId], + ['DOCS_GITHUB_APP_INSTALLATION_ID', installationId], + ['DOCS_GITHUB_APP_PRIVATE_KEY', privateKey], + ] + const missing = appVars.filter(([, value]) => !value).map(([name]) => name) + const partiallyConfigured = missing.length < appVars.length + if (partiallyConfigured) { + throw new Error( + `Incomplete GitHub App configuration: ${missing.join(', ')} not set. Set all three, or unset the others to authenticate with GH_TOKEN / GITHUB_TOKEN instead.` + ) + } + + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN + if (token) { + return { auth: token } + } + + throw new Error( + 'Missing GitHub credentials. Set DOCS_GITHUB_APP_ID, DOCS_GITHUB_APP_INSTALLATION_ID, and DOCS_GITHUB_APP_PRIVATE_KEY, or set GH_TOKEN / GITHUB_TOKEN for a local run (export GH_TOKEN=$(gh auth token)).' + ) +} diff --git a/apps/docs/lib/octokit.ts b/apps/docs/lib/octokit.ts index 0c488b0f1c663..5ddf874a37971 100644 --- a/apps/docs/lib/octokit.ts +++ b/apps/docs/lib/octokit.ts @@ -1,11 +1,10 @@ import 'server-only' -import { createAppAuth } from '@octokit/auth-app' import { Octokit } from '@octokit/core' import { retry } from '@octokit/plugin-retry' -import crypto from 'node:crypto' import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { githubAuthOptions } from './octokit.auth' import { OCTOKIT_RETRY_OPTIONS } from './octokit.constants' export { OCTOKIT_RETRY_OPTIONS } @@ -16,25 +15,7 @@ let octokitInstance: InstanceType export function octokit() { if (!octokitInstance) { - const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY - if (!privateKey) { - throw new Error('DOCS_GITHUB_APP_PRIVATE_KEY environment variable is required') - } - - // https://github.com/gr2m/universal-github-app-jwt?tab=readme-ov-file#converting-pkcs1-to-pkcs8 - const privateKeyPkcs8 = crypto.createPrivateKey(privateKey).export({ - type: 'pkcs8', - format: 'pem', - }) - - octokitInstance = new RetryOctokit({ - authStrategy: createAppAuth, - auth: { - appId: process.env.DOCS_GITHUB_APP_ID, - installationId: process.env.DOCS_GITHUB_APP_INSTALLATION_ID, - privateKey: privateKeyPkcs8, - }, - }) + octokitInstance = new RetryOctokit(githubAuthOptions()) } return octokitInstance diff --git a/apps/docs/scripts/search/sources/lint-warnings-guide.ts b/apps/docs/scripts/search/sources/lint-warnings-guide.ts index d28a57a27fde8..43cee19ab7f7d 100644 --- a/apps/docs/scripts/search/sources/lint-warnings-guide.ts +++ b/apps/docs/scripts/search/sources/lint-warnings-guide.ts @@ -1,16 +1,12 @@ -import { createAppAuth } from '@octokit/auth-app' import { Octokit } from '@octokit/core' import { retry } from '@octokit/plugin-retry' -import crypto, { createHash } from 'node:crypto' +import { createHash } from 'node:crypto' +import { githubAuthOptions } from '../../../lib/octokit.auth.js' import { OCTOKIT_RETRY_OPTIONS } from '../../../lib/octokit.constants.js' import { BaseLoader, BaseSource } from './base.js' const RetryOctokit = Octokit.plugin(retry) -const appId = process.env.DOCS_GITHUB_APP_ID -const installationId = process.env.DOCS_GITHUB_APP_INSTALLATION_ID -const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY - const getBasename = (path: string) => path.split('/').at(-1)!.replace(/\.md$/, '') export class LintWarningsGuideLoader extends BaseLoader { @@ -28,18 +24,7 @@ export class LintWarningsGuideLoader extends BaseLoader { } async load() { - if (!appId || !installationId || !privateKey) { - throw new Error('Missing DOCS_GITHUB_APP_* environment variables') - } - - const octokit = new RetryOctokit({ - authStrategy: createAppAuth, - auth: { - appId, - installationId, - privateKey: crypto.createPrivateKey(privateKey).export({ type: 'pkcs8', format: 'pem' }), - }, - }) + const octokit = new RetryOctokit(githubAuthOptions()) const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { owner: this.org, diff --git a/apps/docs/turbo.jsonc b/apps/docs/turbo.jsonc index b9400112caaa0..9d663a7ff71bc 100644 --- a/apps/docs/turbo.jsonc +++ b/apps/docs/turbo.jsonc @@ -64,7 +64,9 @@ "DOCS_REVALIDATION_KEYS", "DOCS_REVALIDATION_OVERRIDE_KEYS", "ENABLED_FEATURES_OVERRIDE_DISABLE_ALL", + "GH_TOKEN", "GITHUB_ACTIONS", + "GITHUB_TOKEN", "FORCE_ASSET_CDN", "LOGFLARE_INGESTION_API_KEY", "LOGFLARE_SOURCE_TOKEN", From 93d9d80535097fead454bc2e30dc364f07cd1f98 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 14 Aug 2026 00:17:29 +0700 Subject: [PATCH 3/7] joshen/fe 4149 explorer support adding removing cells in notebook (#49037) ## Context Related to Explorer/Notebooks - this PR adds the functionality to add / remove cells Separately - am thinking we can shift a lot of the "cell update" logic into notebook-state, mainly so that each UI component doesn't need to be aware of the notebook's `cells` but just its own cell. I'll do it separately though to prevent bloating this PR, already left comments where i think can be refactored image image image image ## Summary by CodeRabbit ## New Features - Add query and Markdown cells directly within notebooks. - Move, remove, and reorder cells using drag-and-drop or cell controls. - Add cells from empty states and notebook toolbar actions. - Edit Markdown cells, mark edits as complete, and see placeholders for empty content. ## Improvements - Cell controls and hover interactions are more consistent and responsive. - Moving cells is disabled at the top or bottom of a notebook. - Sample cells now use standardized content and formatting. --- .../interfaces/Explorer/AddCellDropdown.tsx | 47 ++++++++++ .../interfaces/Explorer/ExplorerToolbar.tsx | 4 +- .../interfaces/Explorer/MarkdownCell.tsx | 51 +++++++---- .../Explorer/MoveCellDropdownContent.tsx | 57 ++++++++++++ .../interfaces/Explorer/NotebookEditor.tsx | 86 ++++++++++++------ .../QueryCell/DisplaySettingsButton.tsx | 2 + .../interfaces/Explorer/QueryCell/index.tsx | 20 ++++- .../components/interfaces/Explorer/hooks.ts | 39 ++++---- .../components/interfaces/Explorer/utils.ts | 31 +++++++ apps/studio/components/ui/SortableSection.tsx | 84 +++++++++++++---- .../studio/state/notebooks/notebooks-state.ts | 90 +++++++++++++++++++ 11 files changed, 420 insertions(+), 91 deletions(-) create mode 100644 apps/studio/components/interfaces/Explorer/AddCellDropdown.tsx create mode 100644 apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx create mode 100644 apps/studio/components/interfaces/Explorer/utils.ts diff --git a/apps/studio/components/interfaces/Explorer/AddCellDropdown.tsx b/apps/studio/components/interfaces/Explorer/AddCellDropdown.tsx new file mode 100644 index 0000000000000..f63f2b83a5de7 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/AddCellDropdown.tsx @@ -0,0 +1,47 @@ +import { FileText, Plus, SquareCode } from 'lucide-react' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui' + +import { createMarkdownCellSkeleton, createQueryCellSkeleton } from './utils' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' + +interface AddCellDropdownProps { + cellId: string +} + +export const AddCellDropdown = ({ cellId }: AddCellDropdownProps) => { + const snap = useNotebooksStateSnapshot() + const currentNotebook = useCurrentNotebook() + + const onSelectAddCell = (type: 'markdown' | 'query') => { + const notebookId = currentNotebook?.notebook.id + if (!notebookId) return + + const cell = type === 'markdown' ? createMarkdownCellSkeleton() : createQueryCellSkeleton() + + snap.insertCellAfter({ id: notebookId, cellId, cell }) + } + + return ( + + + } + tooltip={{ content: { side: 'bottom', text: 'Add cell' } }} + /> + + + onSelectAddCell('query')}> + + Add query cell + + onSelectAddCell('markdown')}> + + Add markdown cell + + + + ) +} diff --git a/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx index 14e434db48823..0c99f82e52d8c 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerToolbar.tsx @@ -100,9 +100,9 @@ const ExplorerToolbarTitle = ({ ) : onSaveTitle ? ( diff --git a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx index b08550a4408c6..3cbf89238be8d 100644 --- a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx +++ b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx @@ -3,6 +3,8 @@ import { useState } from 'react' import { Button, cn } from 'ui' import { Markdown } from '../Markdown' +import { AddCellDropdown } from './AddCellDropdown' +import { MoveCellDropdownContent } from './MoveCellDropdownContent' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { SortableSection } from '@/components/ui/SortableSection' @@ -14,6 +16,8 @@ interface MarkdownCellProps { cell: MarkdownCellSchema } +// [Joshen] handleUpdateMarkdown could be shifted into notebook-state as a updateCell action + export const MarkdownCell = ({ cell }: MarkdownCellProps) => { const snap = useNotebooksStateSnapshot() const currentNotebook = useCurrentNotebook() @@ -45,10 +49,15 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => { const handleUpdateMarkdownRef = useLatest(handleUpdateMarkdown) return ( - + } + gripDropdownContent={} + gripClassName="mt-1.5 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition" + > {isEditing ? (
{ onMouseDown={(e) => e.preventDefault()} onClick={() => handleUpdateMarkdown(cell.id, value)} > - Save + Done
@@ -102,7 +111,7 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => {
{ variant="text" className={cn( 'absolute right-1 top-1 px-1', - 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100' + 'opacity-0 group-hover/mdcell:opacity-100 focus-visible:opacity-100' )} icon={} onClick={handleStartEditing} tooltip={{ content: { side: 'bottom', text: 'Edit' } }} /> - h1]:mb-2 [&>h2]:mb-2', - '[&_ol>li]:pl-3', - '[--tw-prose-body:var(--foreground-muted)]', - '[--tw-prose-headings:var(--foreground-default)]', - '[--tw-prose-links:var(--foreground-muted)]', - '[--tw-prose-bold:var(--foreground-muted)]', - '[--tw-prose-quotes:var(--foreground-muted)]' - )} - > - {cell.text} - + {cell.text ? ( + h1]:mb-2 [&>h2]:mb-2', + '[&_ol>li]:pl-3', + '[--tw-prose-body:var(--foreground-muted)]', + '[--tw-prose-headings:var(--foreground-default)]', + '[--tw-prose-links:var(--foreground-muted)]', + '[--tw-prose-bold:var(--foreground-muted)]', + '[--tw-prose-quotes:var(--foreground-muted)]' + )} + > + {cell.text} + + ) : ( +

This cell has no content

+ )}
)}
diff --git a/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx b/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx new file mode 100644 index 0000000000000..5fb0e392ba77c --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx @@ -0,0 +1,57 @@ +import { ArrowDown, ArrowUp, Trash } from 'lucide-react' +import { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from 'ui' + +import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' + +interface MoveCellDropdownContentProps { + cellId: string +} + +export const MoveCellDropdownContent = ({ cellId }: MoveCellDropdownContentProps) => { + const snap = useNotebooksStateSnapshot() + const currentNotebook = useCurrentNotebook() + + const cells = currentNotebook?.notebook.content?.cells ?? [] + const currentIndex = cells.findIndex((c) => c.id === cellId) + const isFirstCell = currentIndex <= 0 + const isLastCell = currentIndex === -1 || currentIndex === cells.length - 1 + + const onSelectMoveCell = (direction: 'up' | 'down') => { + const notebookId = currentNotebook?.notebook.id + if (!notebookId) return + + snap.moveCell({ id: notebookId, cellId, direction }) + } + + const onSelectRemoveCell = () => { + const notebookId = currentNotebook?.notebook.id + if (!notebookId) return + snap.removeCell({ id: notebookId, cellId }) + } + + return ( + + onSelectMoveCell('up')} + > + +

Move up

+
+ onSelectMoveCell('down')} + > + +

Move down

+
+ + onSelectRemoveCell()}> + +

Remove cell

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx index 6cc223ce9e270..0949b02db7454 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx +++ b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx @@ -7,13 +7,12 @@ import { useSensors, } from '@dnd-kit/core' import { - arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { useParams } from 'common' -import { Notebook, NotebookText, Play, Save } from 'lucide-react' +import { FileText, Notebook, NotebookText, Play, Save, SquareCode } from 'lucide-react' import { AiIconAnimation, Button } from 'ui' import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' @@ -26,6 +25,8 @@ import { } from './ExplorerToolbar' import { MarkdownCell } from './MarkdownCell' import { QueryCell } from './QueryCell' +import { createMarkdownCellSkeleton, createQueryCellSkeleton } from './utils' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' import { createTabId, useTabsStateSnapshot } from '@/state/tabs' @@ -55,11 +56,17 @@ export const NotebookEditor = () => { const { active, over } = event if (!id || !over || active.id === over.id) return - const oldIndex = cells.findIndex((cell) => cell.id === active.id) - const newIndex = cells.findIndex((cell) => cell.id === over.id) - if (oldIndex === -1 || newIndex === -1) return + snap.reorderCells({ id, activeCellId: active.id, overCellId: over.id }) + } + + const onSelectAddCell = (type: 'markdown' | 'query') => { + const notebookId = currentNotebook?.notebook.id + if (!notebookId) return + + const cell = type === 'markdown' ? createMarkdownCellSkeleton() : createQueryCellSkeleton() + const lastCellId = cells[cells.length - 1]?.id - snap.updateCells({ id, cells: arrayMove([...cells], oldIndex, newIndex) }) + snap.insertCellAfter({ id: notebookId, cellId: lastCellId, cell }) } return ( @@ -88,34 +95,57 @@ export const NotebookEditor = () => { contentClassName="[&>h3]:text-sm [&>p]:text-xs" >
- - + +
)} {cells.length > 0 && ( - - cell.id)} - strategy={verticalListSortingStrategy} - > -
- {cells.map((cell) => { - switch (cell._tag) { - case 'markdown_cell': - return + <> + + cell.id)} + strategy={verticalListSortingStrategy} + > +
+ {cells.map((cell) => { + switch (cell._tag) { + case 'markdown_cell': + return - case 'database_cell': - return + case 'database_cell': + return - case 'log_cell': - // [Joshen] Will eventually hook it up - return null - } - })} -
-
-
+ case 'log_cell': + // [Joshen] Will eventually hook it up + return null + } + })} +
+
+
+ +
+ } + className="w-7" + onClick={() => onSelectAddCell('query')} + tooltip={{ content: { side: 'bottom', text: 'Add query cell' } }} + /> + } + className="w-7" + onClick={() => onSelectAddCell('markdown')} + tooltip={{ content: { side: 'bottom', text: 'Add markdown cell' } }} + /> +
+ )} diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/DisplaySettingsButton.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/DisplaySettingsButton.tsx index 70c07a3bae55b..d826537d3723e 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/DisplaySettingsButton.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/DisplaySettingsButton.tsx @@ -32,6 +32,8 @@ interface DisplaySettingsButtonProps { } // [Joshen] TODO support multiple y axis charts +// [Joshen] TODO onUpdateChartConfig can likely be shifted into the notebook-state +// so this component doesn't need to know about other cells export const DisplaySettingsButton = ({ display, diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx index 9553fa6426270..00f63c1c9d1dd 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -2,6 +2,8 @@ import { untrustedSql } from '@supabase/pg-meta' import { useState } from 'react' import { type Snapshot } from 'valtio' +import { AddCellDropdown } from '../AddCellDropdown' +import { MoveCellDropdownContent } from '../MoveCellDropdownContent' import { QueryEditor } from '../QueryEditor' import { type QueryDisplay, type QueryResult } from '../types' import { SortableSection } from '@/components/ui/SortableSection' @@ -12,6 +14,17 @@ interface QueryCellProps { cell: Snapshot } +/** + * [Joshen] Aiming to keep PRs small so the following are deliberating missing for now: + * - Auto limit logic + * - Database selection logic + * + * QueryCell atm minimally supports running queries and rendering results + * + * [Joshen] TODO: handleUpdateCell might be able to shift into notebook-state, so component + * doesn't need to have context of the other cells + */ + type QueryCellUpdate = { sql: string } | { title: string } | { display: QueryDisplay } /** Notebook adapter around the shared QueryEditor. */ @@ -56,7 +69,12 @@ export const QueryCell = ({ cell }: QueryCellProps) => { } return ( - + } + gripDropdownContent={} + gripClassName="mt-2 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition" + > { 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: ` + const sampleMdCell1 = createMarkdownCellSkeleton({ + content: ` # Title A brief description on what this notebook is about - `.trim(), - }, - { - _tag: 'markdown_cell', - id: generateUuid(), - text: ` +`.trim(), + }) + const sampleMdCell2 = createMarkdownCellSkeleton({ + content: ` ## 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(), - view: 'table', - chart: undefined, - unchecked_sql: untrustedSql('select * from colors;'), - row_limit: 100, - }, +`.trim(), + }) + const sampleQueryCell = createQueryCellSkeleton({ sql: 'select * from colors;' }) + + // [Joshen] Just adding sample data to play around with, keep for now - clean up at the end + const DEFAULT_CELLS = [ + sampleMdCell1, + sampleMdCell2, + sampleQueryCell, ] as Notebooks.Content['cells'] const id = idOverride ?? generateUuid() diff --git a/apps/studio/components/interfaces/Explorer/utils.ts b/apps/studio/components/interfaces/Explorer/utils.ts new file mode 100644 index 0000000000000..059c84784d444 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/utils.ts @@ -0,0 +1,31 @@ +import { untrustedSql } from '@supabase/pg-meta' + +import { generateUuid } from '@/lib/api/snippets.browser' + +export const createQueryCellSkeleton = ({ sql }: { sql?: string } = {}) => { + return { + _tag: 'database_cell' as const, + id: generateUuid(), + view: 'table' as const, + chart: undefined, + unchecked_sql: untrustedSql(sql ?? ''), + row_limit: 100, + } +} + +const DEFAULT_MARKDOWN_CONTENT = ` + # New section + Add notes about your queries and results +`.trim() + +export const createMarkdownCellSkeleton = ({ + content = DEFAULT_MARKDOWN_CONTENT, +}: { + content?: string +} = {}) => { + return { + _tag: 'markdown_cell' as const, + id: generateUuid(), + text: content, + } +} diff --git a/apps/studio/components/ui/SortableSection.tsx b/apps/studio/components/ui/SortableSection.tsx index 3484e61e49eb3..53e4425679511 100644 --- a/apps/studio/components/ui/SortableSection.tsx +++ b/apps/studio/components/ui/SortableSection.tsx @@ -1,17 +1,44 @@ +import { useDndMonitor } from '@dnd-kit/core' import { useSortable } from '@dnd-kit/sortable' import { GripVertical } from 'lucide-react' -import type { CSSProperties, PropsWithChildren } from 'react' -import { Button, cn } from 'ui' +import type { CSSProperties, PropsWithChildren, ReactNode } from 'react' +import { useEffect, useRef, useState } from 'react' +import { Button, cn, DropdownMenu, DropdownMenuTrigger } from 'ui' export const SortableSection = ({ id, children, + actions, gripClassName, -}: PropsWithChildren<{ id: string; gripClassName?: string }>) => { + gripDropdownContent, +}: PropsWithChildren<{ + id: string + gripClassName?: string + actions?: ReactNode + gripDropdownContent?: ReactNode +}>) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }) + const [menuOpen, setMenuOpen] = useState(false) + const isDraggingRef = useRef(false) + const openTimeoutRef = useRef>(undefined) + + useDndMonitor({ + onDragStart: (event) => { + if (event.active.id === id) isDraggingRef.current = true + }, + onDragEnd: (event) => { + if (event.active.id === id) isDraggingRef.current = false + }, + onDragCancel: (event) => { + if (event.active.id === id) isDraggingRef.current = false + }, + }) + + useEffect(() => () => clearTimeout(openTimeoutRef.current), []) + const style: CSSProperties = { transform: transform ? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)` @@ -23,22 +50,43 @@ export const SortableSection = ({
-
{children}
) diff --git a/apps/studio/state/notebooks/notebooks-state.ts b/apps/studio/state/notebooks/notebooks-state.ts index f9f3054d93498..cd0c282969691 100644 --- a/apps/studio/state/notebooks/notebooks-state.ts +++ b/apps/studio/state/notebooks/notebooks-state.ts @@ -1,3 +1,5 @@ +import { type UniqueIdentifier } from '@dnd-kit/core' +import { arrayMove } from '@dnd-kit/sortable' import { useParams } from 'common' import { useMemo } from 'react' import { proxy, snapshot, useSnapshot, type Snapshot } from 'valtio' @@ -92,6 +94,94 @@ export const notebooksState = proxy({ if (!skipSave) notebooksState.needsSaving.set(id, false) }, + /** + * Insert a cell right after `cellId` in a notebook's cell array โ€” or at the + * end, if `cellId` is omitted or isn't found (e.g. an empty notebook has no + * cell to insert after). The caller builds the cell to insert (e.g. via + * `createMarkdownCellSkeleton`/`createQueryCellSkeleton`) since deciding + * what kind of cell to create is a UI concern, not a state one. + */ + insertCellAfter: ({ + id, + cellId, + cell, + }: { + id: string + cellId?: string + cell: Notebooks.Cell + }) => { + const stateNotebook = notebooksState.notebooks[id] + if (!stateNotebook?.notebook.content) return + + const cells = stateNotebook.notebook.content.cells + const insertAt = cellId ? cells.findIndex((c) => c.id === cellId) : -1 + const nextCells = [...cells] + nextCells.splice(insertAt === -1 ? cells.length : insertAt + 1, 0, cell) + + notebooksState.updateCells({ id, cells: nextCells }) + }, + + /** + * Remove a single cell from a notebook's cell array. + */ + removeCell: ({ id, cellId }: { id: string; cellId: string }) => { + const stateNotebook = notebooksState.notebooks[id] + if (!stateNotebook?.notebook.content) return + + const nextCells = stateNotebook.notebook.content.cells.filter((c) => c.id !== cellId) + notebooksState.updateCells({ id, cells: nextCells }) + }, + + /** + * Shift a cell one position up or down in a notebook's cell array. No-ops if + * the cell is already at that boundary (or isn't found). + */ + moveCell: ({ + id, + cellId, + direction, + }: { + id: string + cellId: string + direction: 'up' | 'down' + }) => { + const stateNotebook = notebooksState.notebooks[id] + if (!stateNotebook?.notebook.content) return + + const cells = stateNotebook.notebook.content.cells + const currentIndex = cells.findIndex((c) => c.id === cellId) + if (currentIndex === -1) return + + const nextIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1 + if (nextIndex < 0 || nextIndex >= cells.length) return + + notebooksState.updateCells({ id, cells: arrayMove([...cells], currentIndex, nextIndex) }) + }, + + /** + * Reorder a notebook's cell array by moving the cell at `activeCellId` to + * where `overCellId` currently sits (dnd-kit's drag-end positions). + */ + reorderCells: ({ + id, + activeCellId, + overCellId, + }: { + id: string + activeCellId: UniqueIdentifier + overCellId: UniqueIdentifier + }) => { + const stateNotebook = notebooksState.notebooks[id] + if (!stateNotebook?.notebook.content) return + + const cells = stateNotebook.notebook.content.cells + const oldIndex = cells.findIndex((c) => c.id === activeCellId) + const newIndex = cells.findIndex((c) => c.id === overCellId) + if (oldIndex === -1 || newIndex === -1) return + + notebooksState.updateCells({ id, cells: arrayMove([...cells], oldIndex, newIndex) }) + }, + addNeedsSaving: (id: string) => notebooksState.needsSaving.set(id, true), }) From fc3f6aaea8fb60da31f6465e714e5535d6cbee09 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 14 Aug 2026 01:07:20 +0700 Subject: [PATCH 4/7] Add source selector for explorer query tab (#49063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context This is just pulling out the relevant changes from https://github.com/supabase/supabase/pull/49028 as I might have messed up the stack while making changes down the PRs ๐Ÿ™ Builds on the Query Tab in the Explorer UI, adds the source selector component to run either a database query or a logs query - will subsequently be looking into have the source selector component in the QueryCell as well (within notebooks) image image ## Summary by CodeRabbit - **New Features** - Added query-source selection for Explorer and notebook queries. - Supports log queries, database selection, and read-replica connections. - Added validation for custom log time ranges and retention limits, with upgrade guidance when applicable. - Query source choices are saved and restored across sessions. - Changing sources clears previous results to prevent stale data. - **Bug Fixes** - Improved handling of unavailable log querying and missing database connections. - Legacy saved queries now fall back safely to the default database source. --- .../Explorer/ExplorerQuerySourceMenu.tsx | 153 ++++++++++++++++++ .../interfaces/Explorer/QueryEditor.tsx | 77 ++++++++- .../interfaces/Explorer/QueryTab.tsx | 2 + apps/studio/state/explorer-query.test.ts | 45 ++++++ apps/studio/state/explorer-query.ts | 63 ++++++-- 5 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 apps/studio/components/interfaces/Explorer/ExplorerQuerySourceMenu.tsx diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQuerySourceMenu.tsx b/apps/studio/components/interfaces/Explorer/ExplorerQuerySourceMenu.tsx new file mode 100644 index 0000000000000..89102a59e4b51 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/ExplorerQuerySourceMenu.tsx @@ -0,0 +1,153 @@ +import { useFlag, useParams } from 'common' +import dayjs from 'dayjs' +import { Check, ChevronDown } from 'lucide-react' +import { useState } from 'react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' + +import { DatabaseParametersSubMenu } from '@/components/interfaces/QuerySources/DatabaseParametersSubMenu' +import { LogsCustomRangeDialog } from '@/components/interfaces/QuerySources/LogsCustomRangeDialog' +import { LogsTimeRangeSubMenu } from '@/components/interfaces/QuerySources/LogsTimeRangeSubMenu' +import { QuerySourceIcon } from '@/components/interfaces/QuerySources/QuerySourceIcon' +import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils' +import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt' +import { + createDefaultCellSource, + QUERY_SOURCE_LABELS, + QUERY_SOURCES, + type CellSource, +} from '@/data/query-sources/query-source-registry' +import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' + +export type ExplorerQuerySourceMenuProps = { + source: CellSource + onSourceChange: (source: CellSource) => void +} + +/** + * Source binding and parameter controls shared by standalone Explorer queries + * and notebook query-cell toolbars. The consumer owns the binding; this menu + * only emits complete, validated-by-construction `CellSource` values. + */ +export const ExplorerQuerySourceMenu = ({ + source, + onSourceChange, +}: ExplorerQuerySourceMenuProps) => { + const { ref } = useParams() + const isLogsSourceEnabled = useFlag('sqlEditorLogsSource') + const isOtelLogsEnabled = useFlag('otelLegacyLogs') + const [isCustomRangeOpen, setIsCustomRangeOpen] = useState(false) + const [showUpgradePrompt, setShowUpgradePrompt] = useState(false) + const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days') + const entitledToLogDays = getEntitlementNumericValue() + + const availableSources = QUERY_SOURCES.filter( + (candidate) => + candidate.type !== 'logs' || + (isLogsSourceEnabled && isOtelLogsEnabled) || + source.type === 'logs' + ) + + const applyCustomRange = ({ from, to }: { from: Date; to: Date }) => { + const fromIso = dayjs(from).startOf('day').toISOString() + if (maybeShowUpgradePromptIfNotEntitled(fromIso, entitledToLogDays)) { + setShowUpgradePrompt(true) + return + } + + onSourceChange({ + id: 'logs', + type: 'logs', + parameters: { + time_range: { + type: 'absolute', + from: fromIso, + to: dayjs(to).endOf('day').toISOString(), + }, + }, + }) + } + + return ( + <> + + + + + + {availableSources.map((candidate) => ( + { + event.preventDefault() + if (candidate.id !== source.id) { + onSourceChange(createDefaultCellSource(candidate.id)) + } + }} + > + + + {QUERY_SOURCE_LABELS[candidate.id]} + + {source.id === candidate.id && } + + ))} + + + + {source.type === 'database' ? ( + + onSourceChange({ + id: 'database', + type: 'database', + parameters: { identifier }, + }) + } + /> + ) : ( + + onSourceChange({ + id: 'logs', + type: 'logs', + parameters: { time_range: timeRange }, + }) + } + onOpenCustomRange={() => setIsCustomRangeOpen(true)} + onShowUpgrade={() => setShowUpgradePrompt(true)} + /> + )} + + + + {source.type === 'logs' && ( + <> + + + + )} + + ) +} diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor.tsx index d1ecf65fd9f8a..537e858d23de5 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor.tsx @@ -1,8 +1,10 @@ import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta' +import { useFlag } from 'common' import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react' import { useState, type ReactNode } from 'react' import { cn } from 'ui' +import { resolveLogTimeRange } from '../QuerySources/LogTimeRange.utils' import { ExplorerQuery, ExplorerQueryEditor, @@ -10,6 +12,7 @@ import { ExplorerQueryResults, ExplorerQueryViewport, } from './ExplorerQuery' +import { ExplorerQuerySourceMenu } from './ExplorerQuerySourceMenu' import { ExplorerToolbar, ExplorerToolbarAction, @@ -23,6 +26,15 @@ import { QueryResultTable } from './QueryResultTable' import { type QueryDisplay, type QueryResult } from './types' import { applyAutoLimit } from '@/components/interfaces/SQLEditor/SQLEditor.utils' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' +import { isValidConnString } from '@/data/fetchers' +import { useExecuteLogsSqlMutation } from '@/data/logs/execute-logs-sql-mutation' +import { acceptUntrustedLogsSql, untrustedLogSql } from '@/data/logs/safe-analytics-sql' +import { + createDefaultCellSource, + QUERY_SOURCE_REGISTRY, + type CellSource, +} from '@/data/query-sources/query-source-registry' +import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useLatest } from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -32,6 +44,7 @@ export type QueryEditorProps = { variant: 'embedded' | 'viewport' title: string sql: string + source?: CellSource result?: QueryResult rowLimit: number display?: QueryDisplay @@ -39,6 +52,7 @@ export type QueryEditorProps = { onTitleChange: (title: string) => void onSqlChange: (sql: string) => void onSqlCommit?: (sql: string) => void + onSourceChange?: (source: CellSource) => void onResultChange: (result: QueryResult) => void onDisplayChange?: (display: QueryDisplay) => void } @@ -53,6 +67,7 @@ export const QueryEditor = ({ variant, title, sql, + source, result, rowLimit, display, @@ -60,35 +75,88 @@ export const QueryEditor = ({ onTitleChange, onSqlChange, onSqlCommit, + onSourceChange, onResultChange, onDisplayChange, }: QueryEditorProps) => { const sqlRef = useLatest(sql) const onSqlCommitRef = useLatest(onSqlCommit) + const isOtelLogsEnabled = useFlag('otelLegacyLogs') const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery() const view = display?.view ?? 'table' const columns = Object.keys(result?.rows?.[0] ?? {}) + const sourceBinding = source ?? createDefaultCellSource('database') const [showQuery, setShowQuery] = useState(true) - const { mutate: executeSql, isPending: isExecuting } = useExecuteSqlMutation({ + const databaseIdentifier = + sourceBinding.type === 'database' ? sourceBinding.parameters.identifier : undefined + + const { data: databases, isPending: isLoadingDatabases } = useReadReplicasQuery( + { projectRef: project?.ref }, + { + enabled: + databaseIdentifier !== undefined && + project?.ref !== undefined && + databaseIdentifier !== project.ref, + } + ) + + const { mutate: executeSql, isPending: isExecutingSql } = useExecuteSqlMutation({ onSuccess: (data) => onResultChange({ rows: data.result }), onError: (error) => onResultChange({ error }), }) + const { mutate: executeLogsSql, isPending: isExecutingLogs } = useExecuteLogsSqlMutation({ + onSuccess: (data) => onResultChange({ rows: data.rows as readonly Record[] }), + onError: (error) => onResultChange({ error }), + }) + + const isResolvingDatabase = + databaseIdentifier !== undefined && databaseIdentifier !== project?.ref && isLoadingDatabases + const isExecuting = isExecutingSql || isExecutingLogs + const isBusy = isLoadingProject || isResolvingDatabase || isExecuting + const handleRunQuery = (sqlToRun: string = sql) => { - if (!project || isLoadingProject || isExecuting || sqlToRun.trim().length === 0) return + if (!project || isBusy || sqlToRun.trim().length === 0) return onSqlCommit?.(sql) + if (sourceBinding.type === 'logs') { + if (!isOtelLogsEnabled) { + onResultChange({ + error: { message: "Querying logs isn't available for this project yet." }, + }) + return + } + + executeLogsSql({ + projectRef: project.ref, + sql: acceptUntrustedLogsSql(untrustedLogSql(sqlToRun)), + range: resolveLogTimeRange(sourceBinding.parameters.time_range), + endpoint: QUERY_SOURCE_REGISTRY.logs.endpoint, + }) + return + } + const safeSql = acceptUntrustedSql(untrustedSql(sqlToRun)) const limitedSql = applyAutoLimit(safeSql, rowLimit) + const connectionString = + databaseIdentifier === undefined || databaseIdentifier === project.ref + ? project.connectionString + : databases?.find((database) => database.identifier === databaseIdentifier) + ?.connectionString + + if (!isValidConnString(connectionString)) { + onResultChange({ error: { message: 'Unable to run query: Connection string is missing' } }) + return + } executeSql({ projectRef: project.ref, - connectionString: project.connectionString, + connectionString, sql: limitedSql.sql, autoLimit: limitedSql.appendAutoLimit ? rowLimit : undefined, contextualInvalidation: true, @@ -107,6 +175,9 @@ export const QueryEditor = ({ {title} {toolbarActions} + {source && onSourceChange && ( + + )} {display && onDisplayChange && ( { variant="viewport" title={draft.name} sql={draft.uncheckedSql} + source={draft.source} result={result} rowLimit={QUERY_ROW_LIMIT} onTitleChange={(value) => { @@ -83,6 +84,7 @@ export const QueryTab = () => { tabs.updateTab(createTabId('query', { id }), { label: name }) }} onSqlChange={(sql) => explorerQueryState.updateDraft({ id, sql })} + onSourceChange={(source) => explorerQueryState.updateDraft({ id, source })} onResultChange={handleResultChange} /> ) diff --git a/apps/studio/state/explorer-query.test.ts b/apps/studio/state/explorer-query.test.ts index b1c653430ba60..e63364b756c58 100644 --- a/apps/studio/state/explorer-query.test.ts +++ b/apps/studio/state/explorer-query.test.ts @@ -26,12 +26,57 @@ describe('explorer query drafts', () => { expect(secondState.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true) expect(secondState.drafts['query-1']).toMatchObject({ name: 'Active users', + source: { id: 'database', type: 'database', parameters: {} }, uncheckedSql: 'select * from users', projectRef: 'project-a', }) expect(secondState.restoreDraft({ id: 'query-1', projectRef: 'project-b' })).toBe(false) }) + it('persists source parameters and clears stale results when they change', () => { + const storage = createMemoryStorage() + const state = createExplorerQueryState(storage) + + state.createDraft({ id: 'query-1', projectRef: 'project-a', sql: 'select 1' }) + state.setResult({ id: 'query-1', result: { rows: [{ value: 1 }], executedAt: 1 } }) + state.updateDraft({ + id: 'query-1', + source: { + id: 'logs', + type: 'logs', + parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } }, + }, + }) + + expect(state.results['query-1']).toBeUndefined() + + const restored = createExplorerQueryState(storage) + expect(restored.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true) + expect(restored.drafts['query-1'].source).toEqual({ + id: 'logs', + type: 'logs', + parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } }, + }) + }) + + it('restores pre-source drafts as database queries', () => { + const storage = createMemoryStorage() + storage.setItem( + LOCAL_STORAGE_KEYS.EXPLORER_QUERY_DRAFTS('project-a'), + JSON.stringify({ + 'query-1': { name: 'Legacy query', sql: 'select 1', updatedAt: 1 }, + }) + ) + + const state = createExplorerQueryState(storage) + expect(state.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true) + expect(state.drafts['query-1'].source).toEqual({ + id: 'database', + type: 'database', + parameters: {}, + }) + }) + it('removes the persisted draft and its session result when its tab closes', () => { const storage = createMemoryStorage() const state = createExplorerQueryState(storage) diff --git a/apps/studio/state/explorer-query.ts b/apps/studio/state/explorer-query.ts index 28df97f7b59a3..7a10a41f5df88 100644 --- a/apps/studio/state/explorer-query.ts +++ b/apps/studio/state/explorer-query.ts @@ -3,11 +3,17 @@ import { LOCAL_STORAGE_KEYS, safeLocalStorage } from 'common' import { proxy, ref, snapshot, useSnapshot } from 'valtio' import { type QueryResult } from '@/components/interfaces/Explorer/types' +import { + cellSourceSchema, + createDefaultCellSource, + type CellSource, +} from '@/data/query-sources/query-source-registry' export type ExplorerQueryDraft = { id: string projectRef: string name: string + source: CellSource uncheckedSql: UntrustedSqlFragment updatedAt: number } @@ -18,6 +24,7 @@ export type ExplorerQueryResult = QueryResult & { type PersistedExplorerQueryDraft = { name: string + source: CellSource sql: string updatedAt: number } @@ -35,18 +42,27 @@ const readPersistedDrafts = (storage: StorageLike, projectRef: string) => { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {} return Object.fromEntries( - Object.entries(parsed).filter((entry): entry is [string, PersistedExplorerQueryDraft] => { - const draft = entry[1] - return ( - draft !== null && - typeof draft === 'object' && - 'name' in draft && - typeof draft.name === 'string' && - 'sql' in draft && - typeof draft.sql === 'string' && - 'updatedAt' in draft && - typeof draft.updatedAt === 'number' - ) + Object.entries(parsed).flatMap(([id, value]) => { + if ( + value === null || + typeof value !== 'object' || + !('name' in value) || + typeof value.name !== 'string' || + !('sql' in value) || + typeof value.sql !== 'string' || + !('updatedAt' in value) || + typeof value.updatedAt !== 'number' + ) { + return [] + } + + const parsedSource = + 'source' in value ? cellSourceSchema.safeParse(value.source) : { success: false as const } + const source = parsedSource.success + ? parsedSource.data + : createDefaultCellSource('database') + + return [[id, { name: value.name, source, sql: value.sql, updatedAt: value.updatedAt }]] }) ) } catch { @@ -74,23 +90,26 @@ export const createExplorerQueryState = (storage: StorageLike = safeLocalStorage projectRef, name = 'Untitled query', sql = '', + source = createDefaultCellSource('database'), }: { id: string projectRef: string name?: string sql?: string + source?: CellSource }) => { const draft: ExplorerQueryDraft = { id, projectRef, name, + source: cellSourceSchema.parse(source), uncheckedSql: untrustedSql(sql), updatedAt: Date.now(), } state.drafts[id] = draft const persisted = readPersistedDrafts(storage, projectRef) - persisted[id] = { name, sql, updatedAt: draft.updatedAt } + persisted[id] = { name, source: draft.source, sql, updatedAt: draft.updatedAt } writePersistedDrafts(storage, projectRef, persisted) return id @@ -106,23 +125,39 @@ export const createExplorerQueryState = (storage: StorageLike = safeLocalStorage id, projectRef, name: persisted.name, + source: persisted.source, uncheckedSql: untrustedSql(persisted.sql), updatedAt: persisted.updatedAt, } return true }, - updateDraft: ({ id, name, sql }: { id: string; name?: string; sql?: string }) => { + updateDraft: ({ + id, + name, + source, + sql, + }: { + id: string + name?: string + source?: CellSource + sql?: string + }) => { const draft = state.drafts[id] if (!draft) return if (name !== undefined) draft.name = name + if (source !== undefined) { + draft.source = cellSourceSchema.parse(source) + delete state.results[id] + } if (sql !== undefined) draft.uncheckedSql = untrustedSql(sql) draft.updatedAt = Date.now() const persisted = readPersistedDrafts(storage, draft.projectRef) persisted[id] = { name: draft.name, + source: draft.source, sql: draft.uncheckedSql, updatedAt: draft.updatedAt, } From 794e45378c18bf9383e4edd1ada814f7fb0b17e5 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:09:36 -0400 Subject: [PATCH 5/7] test(studio): add get_notebook eval cases (FE-4088) (#49067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature (test coverage) โ€” second PR of the notebooks-evals plan, covering FE-4088 (Evals: Assistant can read notebook). Follows #49010 (FE-4086, list_notebooks). ## What is the current behavior? `dataset.ts` has no coverage for `get_notebook`. Separately, `NOTEBOOKS_PROMPT` only covers *choosing* between `create_notebook`, `update_notebook`, and `execute_sql` โ€” it says nothing about reading or describing an existing notebook. ## What is the new behavior? **Eval cases** (`evals/dataset.ts`), three of them: - Resolve a notebook by name via `list_notebooks`, then `get_notebook`, and report the queries it actually contains. - Summarize a smaller, single-log-cell notebook as a baseline. - Report a nonexistent notebook id as not found instead of hallucinating contents. The mock's `execute` throws, which the AI SDK surfaces to the model as a `tool-error` part rather than failing the eval task. No new scorers or tool changes โ€” `toolUsageScorer` and `correctnessScorer` already cover these, and `get_notebook` has unit coverage in `notebook-tools.test.ts`. **Prompt change** (`lib/ai/prompts.ts`) โ€” please review this one separately, it's the only production behavior change here: Running the first case surfaced a real gap. The assistant transcribed each cell's SQL correctly but was inconsistent about the configuration that changes what a cell returns โ€” dropping the log cell's time range in some runs, miscounting markdown cells as queries in others. Adds one bullet to `NOTEBOOKS_PROMPT` telling it to report a query cell's configuration and not count markdown cells as queries. Gated behind the Explorer flag, same as the rest of that prompt. ## Summary by CodeRabbit * **New Features** * Added notebook evaluation scenarios for database and log queries, including concise summaries and nonexistent notebook handling. * Improved notebook descriptions by reporting query configurations that affect returned results. * Markdown cells are now excluded from query counts. --- apps/studio/evals/dataset.ts | 43 +++++++++++++++++++++++++++++++++++ apps/studio/lib/ai/prompts.ts | 1 + 2 files changed, 44 insertions(+) diff --git a/apps/studio/evals/dataset.ts b/apps/studio/evals/dataset.ts index ab3871c2d9063..9101b66f98ae2 100644 --- a/apps/studio/evals/dataset.ts +++ b/apps/studio/evals/dataset.ts @@ -463,4 +463,47 @@ export const dataset: AssistantEvalCase[] = [ 'Guards against inventing a notebook instead of calling the tool and reporting the real, negative result', }, }, + { + input: { prompt: 'What queries does my Auth health check notebook run?' }, + expected: { + requiredTools: ['list_notebooks', 'get_notebook'], + correctAnswer: + "Reports exactly two queries: a database query for signups per day from auth.users, and a logs query for auth errors against the auth_logs source scoped to the last hour. May (but does not have to) note that the remaining cell is markdown and runs no query. Accurate statements about a cell's configuration โ€” the signups cell's 30-row limit or its line chart, the auth errors cell's relative time range โ€” are acceptable. Does not attribute any query, table, or log source the notebook does not contain, and does not misstate the auth errors cell's time range.", + }, + metadata: { + category: ['general_help'], + description: + 'Resolves a notebook name to an id via list_notebooks, then reads it โ€” guards against paraphrasing cells into queries the notebook does not contain', + }, + }, + { + input: { prompt: "Summarize what's in my Edge function error triage notebook" }, + expected: { + requiredTools: ['list_notebooks', 'get_notebook'], + correctAnswer: + 'Summarizes the notebook as a markdown intro plus one log cell ("hello-world failures") that queries function_edge_logs for TypeError failures over the last day. Log cells hold SQL against a logs source, so describing that cell\'s SQL, columns, or time range is correct and acceptable. Does not attribute a third cell, or any Postgres database cell, to the notebook.', + }, + metadata: { + category: ['general_help'], + description: 'Baseline read of a smaller, single-query notebook', + }, + }, + { + input: { prompt: 'Get the notebook with id 00000000-0000-0000-0000-000000000000' }, + expected: { + requiredTools: [ + { + name: 'get_notebook', + input: { id: { equals: '00000000-0000-0000-0000-000000000000' } }, + }, + ], + correctAnswer: + 'States that no notebook with that id was found, and does not describe any notebook contents.', + }, + metadata: { + category: ['general_help'], + description: + 'Exercises the not-found error path of get_notebook and guards against hallucinating contents for a notebook that does not exist', + }, + }, ] diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts index cdd1cd936030a..b0c637c4d1f80 100644 --- a/apps/studio/lib/ai/prompts.ts +++ b/apps/studio/lib/ai/prompts.ts @@ -767,6 +767,7 @@ export const NOTEBOOKS_PROMPT = ` - Use \`execute_sql\` for a single ad-hoc question with no need to persist it. - When the request clearly calls for a notebook, call \`create_notebook\` or \`update_notebook\` directly; both tools handle user approval. - \`update_notebook\` re-fetches the notebook right before applying edits, so the latest save always wins โ€” it cannot detect edits made by someone else in between. +- When describing an existing notebook, report each query cell's configuration that changes what it returns โ€” a log cell's time range, a database cell's row limit โ€” and don't count markdown cells as queries. ` export const OUTPUT_ONLY_PROMPT = ` From 2f89014f748add0e0446af29a4cced29a60ccd0c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 14 Aug 2026 02:23:06 +0700 Subject: [PATCH 6/7] Add logs cells (#49064) ## Context Related to Explorer/Notebooks - adds the source selector for query cell within a notebook image image ## Summary by CodeRabbit * **New Features** * Added support for displaying log cells in the query editor. * Added switching between database and log query sources. * Log and database cells can display results as tables or charts. * Log queries support optional row limits. * Improved reliability when changing query settings. * Notebook query views now default to table display when unspecified. * **Bug Fixes** * Log cells no longer appear blank or get omitted from notebook views. --- .../interfaces/Explorer/NotebookEditor.tsx | 6 +- .../interfaces/Explorer/QueryCell/index.tsx | 116 +++++++++++++----- .../interfaces/Explorer/QueryEditor.tsx | 10 +- .../studio/data/content/content-remap.test.ts | 2 + .../content/notebooks/notebook-schema.test.ts | 1 + .../data/content/notebooks/notebook-schema.ts | 15 +-- .../lib/ai/tools/notebook-tools.test.ts | 2 + .../studio/state/notebooks/notebooks-state.ts | 24 ++++ 8 files changed, 131 insertions(+), 45 deletions(-) diff --git a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx index 0949b02db7454..3b95637b04965 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx +++ b/apps/studio/components/interfaces/Explorer/NotebookEditor.tsx @@ -116,13 +116,9 @@ export const NotebookEditor = () => { switch (cell._tag) { case 'markdown_cell': return - case 'database_cell': - return - case 'log_cell': - // [Joshen] Will eventually hook it up - return null + return } })} diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx index 00f63c1c9d1dd..57c22e307aa37 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -7,11 +7,19 @@ import { MoveCellDropdownContent } from '../MoveCellDropdownContent' import { QueryEditor } from '../QueryEditor' import { type QueryDisplay, type QueryResult } from '../types' import { SortableSection } from '@/components/ui/SortableSection' -import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema' +import { + type DatabaseCell as DatabaseCellSchema, + type LogCell as LogCellSchema, +} from '@/data/content/notebooks/notebook-schema' +import { untrustedLogSql } from '@/data/logs/safe-analytics-sql' +import { + createDefaultCellSource, + type CellSource, +} from '@/data/query-sources/query-source-registry' import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state' interface QueryCellProps { - cell: Snapshot + cell: Snapshot } /** @@ -20,9 +28,6 @@ interface QueryCellProps { * - Database selection logic * * QueryCell atm minimally supports running queries and rendering results - * - * [Joshen] TODO: handleUpdateCell might be able to shift into notebook-state, so component - * doesn't need to have context of the other cells */ type QueryCellUpdate = { sql: string } | { title: string } | { display: QueryDisplay } @@ -31,41 +36,88 @@ type QueryCellUpdate = { sql: string } | { title: string } | { display: QueryDis export const QueryCell = ({ cell }: QueryCellProps) => { const snap = useNotebooksStateSnapshot() const currentNotebook = useCurrentNotebook() - const cells = currentNotebook?.notebook.content?.cells ?? [] - const [sql, setSql] = useState(cell.unchecked_sql) + const { id, title: cellTitle, view, chart, unchecked_sql } = cell + const rowLimit = 'row_limit' in cell ? cell.row_limit : undefined + const source = + cell._tag === 'database_cell' + ? createDefaultCellSource('database') + : createDefaultCellSource('logs') + + const [sql, setSql] = useState(unchecked_sql) const [result, setResult] = useState() - const title = cell.title ?? 'Untitled snippet' + const title = cellTitle ?? 'Untitled snippet' const display: QueryDisplay = { - view: cell.view ?? 'table', - chart: cell.chart ? { ...cell.chart, y_columns: [...cell.chart.y_columns] } : undefined, + view: view ?? 'table', + chart: chart ? { ...chart, y_columns: [...chart.y_columns] } : undefined, + } + + const handleSourceChange = (source: CellSource) => { + const notebookId = currentNotebook?.notebook.id + if (!notebookId) return + + snap.updateCell({ + id: notebookId, + cellId: id, + updater: (candidate) => { + if (source.type === 'database' && candidate._tag === 'log_cell') { + const { _tag, time_range, unchecked_sql, ...rest } = candidate + return { + ...rest, + _tag: 'database_cell' as const, + row_limit: 100, + unchecked_sql: untrustedSql(unchecked_sql), + } + } + + if (source.type === 'logs' && candidate._tag === 'database_cell') { + const { _tag, row_limit, unchecked_sql, ...rest } = candidate + return { + ...rest, + _tag: 'log_cell' as const, + time_range: { + _tag: 'relative_time_range' as const, + unit: 'hour' as const, + amount: 1, + }, + unchecked_sql: untrustedLogSql(unchecked_sql), + } + } + + return candidate + }, + }) } const handleUpdateCell = (payload: QueryCellUpdate) => { const notebookId = currentNotebook?.notebook.id if (!notebookId) return - const nextCells = cells.map((candidate) => { - if (candidate.id !== cell.id || candidate._tag !== 'database_cell') return candidate + snap.updateCell({ + id: notebookId, + cellId: id, + updater: (candidate) => { + if (candidate._tag !== 'database_cell' && candidate._tag !== 'log_cell') return candidate - if ('sql' in payload) { - return { ...candidate, unchecked_sql: untrustedSql(payload.sql) } - } + if ('sql' in payload) { + return candidate._tag === 'database_cell' + ? { ...candidate, unchecked_sql: untrustedSql(payload.sql) } + : { ...candidate, unchecked_sql: untrustedLogSql(payload.sql) } + } - if ('title' in payload) { - const nextTitle = payload.title.trim() - return nextTitle ? { ...candidate, title: nextTitle } : candidate - } + if ('title' in payload) { + const nextTitle = payload.title.trim() + return nextTitle ? { ...candidate, title: nextTitle } : candidate + } - return { - ...candidate, - view: payload.display.view, - chart: payload.display.chart, - } + return { + ...candidate, + view: payload.display.view, + chart: payload.display.chart, + } + }, }) - - snap.updateCells({ id: notebookId, cells: nextCells }) } return ( @@ -76,18 +128,22 @@ export const QueryCell = ({ cell }: QueryCellProps) => { gripClassName="mt-2 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition" > handleUpdateCell({ title })} onSqlChange={setSql} onSqlCommit={(sql) => handleUpdateCell({ sql })} + onSourceChange={handleSourceChange} onResultChange={setResult} - onDisplayChange={(display) => handleUpdateCell({ display })} + onDisplayChange={ + cell._tag === 'database_cell' ? (display) => handleUpdateCell({ display }) : undefined + } />
) diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor.tsx index 537e858d23de5..8c79ef0077794 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor.tsx @@ -46,7 +46,7 @@ export type QueryEditorProps = { sql: string source?: CellSource result?: QueryResult - rowLimit: number + rowLimit?: number display?: QueryDisplay toolbarActions?: ReactNode onTitleChange: (title: string) => void @@ -238,8 +238,12 @@ export const QueryEditor = ({

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

-

ยท

-

Limit {rowLimit} rows

+ {rowLimit && ( + <> +

ยท

+

Limit {rowLimit} rows

+ + )}
) diff --git a/apps/studio/data/content/content-remap.test.ts b/apps/studio/data/content/content-remap.test.ts index 1c288ea781098..36f4648157168 100644 --- a/apps/studio/data/content/content-remap.test.ts +++ b/apps/studio/data/content/content-remap.test.ts @@ -118,6 +118,7 @@ describe('remapSqlContentField', () => { _tag: 'database_cell', id: DATABASE_CELL.id, row_limit: 100, + view: 'table', unchecked_sql: untrustedSql(DATABASE_CELL.sql), }) expect(databaseCell).not.toHaveProperty('sql') @@ -125,6 +126,7 @@ describe('remapSqlContentField', () => { _tag: 'log_cell', id: LOG_CELL.id, time_range: LOG_CELL.time_range, + view: 'table', unchecked_sql: untrustedLogSql(LOG_CELL.sql), }) expect(logCell).not.toHaveProperty('sql') diff --git a/apps/studio/data/content/notebooks/notebook-schema.test.ts b/apps/studio/data/content/notebooks/notebook-schema.test.ts index f50bd20c9a69e..c6fc30abbaa22 100644 --- a/apps/studio/data/content/notebooks/notebook-schema.test.ts +++ b/apps/studio/data/content/notebooks/notebook-schema.test.ts @@ -225,6 +225,7 @@ describe('notebookDomainSchema', () => { _tag: 'database_cell', id: FULL_NOTEBOOK.cells[1].id, row_limit: 100, + view: 'table', unchecked_sql: untrustedSql('select * from auth.users limit 100'), }) expect(databaseCell).not.toHaveProperty('sql') diff --git a/apps/studio/data/content/notebooks/notebook-schema.ts b/apps/studio/data/content/notebooks/notebook-schema.ts index 545267b75e9b9..c326011f3b7c4 100644 --- a/apps/studio/data/content/notebooks/notebook-schema.ts +++ b/apps/studio/data/content/notebooks/notebook-schema.ts @@ -51,7 +51,7 @@ const databaseCellSchema = z.object({ title: z.string().optional(), sql: z.string(), row_limit: z.number(), - view: z.enum(['table', 'chart']).default('table').optional(), + view: z.enum(['table', 'chart']).optional(), chart: chartConfigSchema.optional(), }) @@ -61,6 +61,7 @@ const logCellSchema = z.object({ title: z.string().optional(), sql: z.string(), time_range: timeRangeSchema, + view: z.enum(['table', 'chart']).optional(), chart: chartConfigSchema.optional(), }) @@ -126,19 +127,19 @@ export const agentNotebookSchema = z.object({ export type AgentNotebook = z.infer export type AgentCell = z.infer -// The domain shape: parses the same wire cell (`cellSchema`) and transforms `sql` into a -// branded `unchecked_sql`. +// The domain shape: parses the same wire cell (`cellSchema`), transforms `sql` into a +// branded `unchecked_sql`, and defaults `view` to 'table' const cellDomainSchema = cellSchema.transform((cell) => { switch (cell._tag) { case 'markdown_cell': return cell case 'database_cell': { - const { sql, ...rest } = cell - return { ...rest, unchecked_sql: untrustedSql(sql) } + const { sql, view, ...rest } = cell + return { ...rest, view: view ?? 'table', unchecked_sql: untrustedSql(sql) } } case 'log_cell': { - const { sql, ...rest } = cell - return { ...rest, unchecked_sql: untrustedLogSql(sql) } + const { sql, view, ...rest } = cell + return { ...rest, view: view ?? 'table', unchecked_sql: untrustedLogSql(sql) } } } }) diff --git a/apps/studio/lib/ai/tools/notebook-tools.test.ts b/apps/studio/lib/ai/tools/notebook-tools.test.ts index 1231cbd509e65..c6889a74a0677 100644 --- a/apps/studio/lib/ai/tools/notebook-tools.test.ts +++ b/apps/studio/lib/ai/tools/notebook-tools.test.ts @@ -208,12 +208,14 @@ describe('ai/tools/notebook-tools', () => { _tag: 'database_cell', id: 'cell-2', row_limit: 100, + view: 'table', sql: 'select * from auth.users limit 100', }, { _tag: 'log_cell', id: 'cell-3', time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 }, + view: 'table', sql: 'select timestamp, event_message from edge_logs limit 10', }, ], diff --git a/apps/studio/state/notebooks/notebooks-state.ts b/apps/studio/state/notebooks/notebooks-state.ts index cd0c282969691..14449cf92796a 100644 --- a/apps/studio/state/notebooks/notebooks-state.ts +++ b/apps/studio/state/notebooks/notebooks-state.ts @@ -121,6 +121,30 @@ export const notebooksState = proxy({ notebooksState.updateCells({ id, cells: nextCells }) }, + /** + * Update a single cell in a notebook's cell array via an updater callback. + * The caller decides how the cell's content should change (e.g. field + * defaults, tag conversion) since that's a UI concern, not a state one โ€” + * this only finds the cell by id and re-saves the array. + */ + updateCell: ({ + id, + cellId, + updater, + }: { + id: string + cellId: string + updater: (cell: Notebooks.Cell) => Notebooks.Cell + }) => { + const stateNotebook = notebooksState.notebooks[id] + if (!stateNotebook?.notebook.content) return + + const nextCells = stateNotebook.notebook.content.cells.map((cell) => + cell.id === cellId ? updater(cell) : cell + ) + notebooksState.updateCells({ id, cells: nextCells }) + }, + /** * Remove a single cell from a notebook's cell array. */ From 4d492db5eb9d8d36ad5d1c1b2a9f444b47a2cee1 Mon Sep 17 00:00:00 2001 From: Maksym Ionutsa <123338468+peekknuf@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:38:04 +0200 Subject: [PATCH 7/7] docs: update settings links after upgrade UI move to General (#49053) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update - Upgrade project button and Postgres/PostgREST version checks moved from Infrastructure to General settings - Updated links across 13 docs pages to match ## Summary by CodeRabbit - **Documentation** - Updated dashboard links throughout the documentation to direct users to **General Settings** instead of **Infrastructure Settings**. - Corrected guidance for Postgres, pgvector, pg_net, and PostgREST upgrades, configuration, and version checks. - Updated monitoring, Grafana, and Log Drains links to current documentation paths. - Fixed troubleshooting links, CLI project path examples, pg_cron terminology, and Markdown formatting. --- apps/docs/content/guides/ai/vector-indexes.mdx | 2 +- .../content/guides/ai/vector-indexes/hnsw-indexes.mdx | 2 +- .../content/guides/api/rest/postgrest-error-codes.mdx | 2 +- apps/docs/content/guides/api/securing-your-api.mdx | 2 +- apps/docs/content/guides/database/extensions.mdx | 2 +- .../docs/content/guides/database/extensions/pg_net.mdx | 2 +- apps/docs/content/guides/getting-started/features.mdx | 2 +- .../metrics/grafana-self-hosted.mdx | 2 +- apps/docs/content/guides/platform/billing-faq.mdx | 2 +- .../content/guides/platform/network-restrictions.mdx | 2 +- apps/docs/content/guides/platform/project-transfer.mdx | 2 +- apps/docs/content/guides/platform/temporary-access.mdx | 2 +- apps/docs/content/guides/platform/upgrading.mdx | 4 ++-- .../forbidden-resource-error-from-the-cli-L6rm6l.mdx | 2 +- .../grafana-not-displaying-data-sXJrMj.mdx | 2 +- ...-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx | 2 +- ...interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx | 2 +- .../troubleshooting/pgcron-debugging-guide-n1KTaz.mdx | 10 +++++----- ...n-does-not-exist-when-using-or-operators-46ff23.mdx | 4 ++-- .../rls-performance-and-best-practices-Z5Jjwv.mdx | 2 +- .../troubleshooting/webhook-debugging-guide-M8sk47.mdx | 4 ++-- 21 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/docs/content/guides/ai/vector-indexes.mdx b/apps/docs/content/guides/ai/vector-indexes.mdx index 99f1d8823b1f0..08488384a2fad 100644 --- a/apps/docs/content/guides/ai/vector-indexes.mdx +++ b/apps/docs/content/guides/ai/vector-indexes.mdx @@ -34,7 +34,7 @@ For pgvector versions 0.7.0 and above, it's possible to create indexes on vector You can check your current pgvector version by running: `SELECT * FROM pg_extension WHERE extname = 'vector';` or by navigating to the [Extensions](/dashboard/project/_/database/extensions) tab in your Supabase project dashboard. -If you are on an earlier version of pgvector, you should [upgrade your project here](/dashboard/project/_/settings/infrastructure). +If you are on an earlier version of pgvector, you should [upgrade your project here](/dashboard/project/_/settings/general). ## Resources diff --git a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx index 38d06d2bb64dc..fb77fe5ffd83e 100644 --- a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx +++ b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx @@ -45,7 +45,7 @@ For pgvector versions 0.7.0 and above, it's possible to create indexes on vector You can check your current pgvector version by running: `SELECT * FROM pg_extension WHERE extname = 'vector';` or by navigating to the [Extensions](/dashboard/project/_/database/extensions) tab in your Supabase project dashboard. -If you are on an earlier version of pgvector, you should [upgrade your project here](/dashboard/project/_/settings/infrastructure). +If you are on an earlier version of pgvector, you should [upgrade your project here](/dashboard/project/_/settings/general). ## Example with high-dimensional vectors diff --git a/apps/docs/content/guides/api/rest/postgrest-error-codes.mdx b/apps/docs/content/guides/api/rest/postgrest-error-codes.mdx index a4a8897deba9e..4e0d3274518dd 100644 --- a/apps/docs/content/guides/api/rest/postgrest-error-codes.mdx +++ b/apps/docs/content/guides/api/rest/postgrest-error-codes.mdx @@ -195,7 +195,7 @@ limit 100; -PostgREST error codes are only captured in the logs for projects running V14+. You can check your PostgREST version and upgrade your project in the [Infrastructure Settings](/dashboard/project/_/settings/infrastructure) +PostgREST error codes are only captured in the logs for projects running V14+. You can check your PostgREST version and upgrade your project in the [General Settings](/dashboard/project/_/settings/general) diff --git a/apps/docs/content/guides/api/securing-your-api.mdx b/apps/docs/content/guides/api/securing-your-api.mdx index cf844f70811f6..5df2faf3cf8c8 100644 --- a/apps/docs/content/guides/api/securing-your-api.mdx +++ b/apps/docs/content/guides/api/securing-your-api.mdx @@ -130,7 +130,7 @@ X-Powered-By: Nerd Rage Use JSON functions and operators to build dynamic responses from exceptions. Include the `status_text` key in the `detail` clause when you use a custom HTTP status code such as 419. See [JSON Functions and Operators](https://www.postgresql.org/docs/current/functions-json.html) in the Postgres documentation. -For PostgREST 11 or earlier, use the legacy syntax for raising errors. [Check your PostgREST version](/dashboard/project/_/settings/infrastructure) in the Dashboard. See [Raise errors with HTTP status codes](https://postgrest.org/en/stable/references/errors.html#raise-errors-with-http-status-codes) in the PostgREST documentation. +For PostgREST 11 or earlier, use the legacy syntax for raising errors. [Check your PostgREST version](/dashboard/project/_/settings/general) in the Dashboard. See [Raise errors with HTTP status codes](https://postgrest.org/en/stable/references/errors.html#raise-errors-with-http-status-codes) in the PostgREST documentation. ## Configure Data API security diff --git a/apps/docs/content/guides/database/extensions.mdx b/apps/docs/content/guides/database/extensions.mdx index a9ed68e173921..f83a7af2be22f 100644 --- a/apps/docs/content/guides/database/extensions.mdx +++ b/apps/docs/content/guides/database/extensions.mdx @@ -56,7 +56,7 @@ In addition to the pre-configured extensions, you can also install your own SQL ## Upgrade extensions -If a new version of an extension becomes available on Supabase, you need to initiate a software upgrade in the [Infrastructure Settings](/dashboard/project/_/settings/infrastructure) to access it. Software upgrades can also be initiated by restarting your server in the [General Settings](/dashboard/project/_/settings/general). +If a new version of an extension becomes available on Supabase, you need to initiate a software upgrade in the [General Settings](/dashboard/project/_/settings/general) to access it. Software upgrades can also be initiated by restarting your server in the same [General Settings](/dashboard/project/_/settings/general) page. ## Full list of extensions diff --git a/apps/docs/content/guides/database/extensions/pg_net.mdx b/apps/docs/content/guides/database/extensions/pg_net.mdx index bb505ceae2063..7d0a3a399c9d1 100644 --- a/apps/docs/content/guides/database/extensions/pg_net.mdx +++ b/apps/docs/content/guides/database/extensions/pg_net.mdx @@ -311,7 +311,7 @@ order by "created" desc; -Supabase supports reconfiguring pg*net starting from v0.12.0+. For the latest release, initiate a Postgres upgrade in the [Infrastructure Settings](/dashboard/project/*/settings/infrastructure). +Supabase supports reconfiguring `pg_net` starting from v0.12.0+. For the latest release, initiate a Postgres upgrade in the [General Settings](/dashboard/project/_/settings/general). diff --git a/apps/docs/content/guides/getting-started/features.mdx b/apps/docs/content/guides/getting-started/features.mdx index 2fd7bbbfe5e51..b573c41d5d161 100644 --- a/apps/docs/content/guides/getting-started/features.mdx +++ b/apps/docs/content/guides/getting-started/features.mdx @@ -68,7 +68,7 @@ Deploy read-only databases across multiple regions, for lower latency and better ### Log drains -Export Supabase logs to third-party providers and external tooling. [Docs](/docs/guides/telemetry/log-drains). +Export Supabase logs to third-party providers and external tooling. [Docs](/docs/guides/monitoring-and-debugging/log-drains). ## Studio diff --git a/apps/docs/content/guides/monitoring-and-debugging/metrics/grafana-self-hosted.mdx b/apps/docs/content/guides/monitoring-and-debugging/metrics/grafana-self-hosted.mdx index dcb7d38db302e..ad4ec6fc6b671 100644 --- a/apps/docs/content/guides/monitoring-and-debugging/metrics/grafana-self-hosted.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/metrics/grafana-self-hosted.mdx @@ -10,7 +10,7 @@ Self-hosting [Prometheus](https://prometheus.io/docs/prometheus/latest/installat Use this guide only if you need full manual control (custom scrape topology, self-hosted Prometheus or non-standard auth). -Otherwise, use the [Grafana Cloud integration](/docs/guides/telemetry/metrics/grafana-cloud#installation) available in the Supabase Dashboard. +Otherwise, use the [Grafana Cloud integration](/docs/guides/monitoring-and-debugging/metrics/grafana-cloud#installation) available in the Supabase Dashboard. diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 4ea5fdb68c7de..30c52f06d4e23 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -12,7 +12,7 @@ subtitle: 'This documentation covers frequently asked questions around subscript ### What are organizations and projects? The Supabase Platform has "organizations" and "projects". An organization may contain multiple projects. Each project is a dedicated Supabase instance with all of its sub-services including Storage, Auth, Functions and Realtime. -Each organization only has a single subscription with a single plan (Free, Pro, Team or Enterprise). Project add-ons such as [Compute](/docs/guides/platform/compute-and-disk), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/telemetry/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone), [Custom Domains](/docs/guides/platform/custom-domains) and [PITR](/docs/guides/platform/backups#point-in-time-recovery) are configured per project and are added to your organization subscription. +Each organization only has a single subscription with a single plan (Free, Pro, Team or Enterprise). Project add-ons such as [Compute](/docs/guides/platform/compute-and-disk), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/monitoring-and-debugging/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone), [Custom Domains](/docs/guides/platform/custom-domains) and [PITR](/docs/guides/platform/backups#point-in-time-recovery) are configured per project and are added to your organization subscription. Read more on [About billing on Supabase](/docs/guides/platform/billing-on-supabase#organization-based-billing). diff --git a/apps/docs/content/guides/platform/network-restrictions.mdx b/apps/docs/content/guides/platform/network-restrictions.mdx index b471ba27eab43..1427441a21560 100644 --- a/apps/docs/content/guides/platform/network-restrictions.mdx +++ b/apps/docs/content/guides/platform/network-restrictions.mdx @@ -8,7 +8,7 @@ This topic explains how to configure network restrictions for your Supabase proj -If you can't find the Network Restrictions section in your [Database Settings](/dashboard/project/_/database/settings), update your Postgres version in [Infrastructure Settings](/dashboard/project/_/settings/infrastructure). +If you can't find the Network Restrictions section in your [Database Settings](/dashboard/project/_/database/settings), update your Postgres version in [General Settings](/dashboard/project/_/settings/general). diff --git a/apps/docs/content/guides/platform/project-transfer.mdx b/apps/docs/content/guides/platform/project-transfer.mdx index e463bf2aa77a5..cc4718c63406a 100644 --- a/apps/docs/content/guides/platform/project-transfer.mdx +++ b/apps/docs/content/guides/platform/project-transfer.mdx @@ -43,7 +43,7 @@ Target organization - the organization you want to move the project to ## Usage-billing and project add-ons -For usage metrics such as disk size, egress or image transformations and project add-ons such as [Compute Add-On](/docs/guides/platform/compute-and-disk), [Point-In-Time-Recovery](/docs/guides/platform/backups#point-in-time-recovery), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/telemetry/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone) or a [Custom Domain](/docs/guides/platform/custom-domains), the source organization will still be charged for the usage up until the transfer. The charges will be added to the invoice when the billing cycle resets. +For usage metrics such as disk size, egress or image transformations and project add-ons such as [Compute Add-On](/docs/guides/platform/compute-and-disk), [Point-In-Time-Recovery](/docs/guides/platform/backups#point-in-time-recovery), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/monitoring-and-debugging/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone) or a [Custom Domain](/docs/guides/platform/custom-domains), the source organization will still be charged for the usage up until the transfer. The charges will be added to the invoice when the billing cycle resets. The target organization will be charged at the end of the billing cycle for usage after the project transfer. diff --git a/apps/docs/content/guides/platform/temporary-access.mdx b/apps/docs/content/guides/platform/temporary-access.mdx index e9cadf3a61f05..2862e3bdcba6d 100644 --- a/apps/docs/content/guides/platform/temporary-access.mdx +++ b/apps/docs/content/guides/platform/temporary-access.mdx @@ -9,7 +9,7 @@ Enabling temporary access only applies to connections to Postgres and Supavisor -Projects need to be at least on Postgres 17.6.1.081 (or higher) to enable temporary access. You can find the Postgres version of your project on the [infrastructure settings](/dashboard/project/_/settings/infrastructure) page. If your project is on an older version, you will need to [upgrade](/docs/guides/platform/upgrading) to use this feature. +Projects need to be at least on Postgres 17.6.1.081 (or higher) to enable temporary access. You can find the Postgres version of your project on the [General Settings](/dashboard/project/_/settings/general) page. If your project is on an older version, you will need to [upgrade](/docs/guides/platform/upgrading) to use this feature. diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index beec25a21692a..1400a906e59ee 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -18,7 +18,7 @@ Free projects will move to the latest minor version when their paused project is The upgrade process is as follows: -1. Use the "Upgrade project" button on the [Infrastructure](/dashboard/project/_/settings/infrastructure) section of your dashboard. +1. Use the "Upgrade project" button on the [General settings](/dashboard/project/_/settings/general) page of your dashboard. 2. An estimate of the time to upgrade is shown and anything that needs to be addressed before you are eligible to upgrade is shown as a warning. Ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. 3. Your project is taken offline and the Dashboard shows the upgrade status. 4. Behind the scenes, a new instance is created running the latest version of Supabase. @@ -39,7 +39,7 @@ When upgrading, a notification will inform you about what is blocking the upgrad 3. Logical replication slots must be dropped. 4. Deprecated/unsupported extensions must be dropped. Extensions can have dependencies, make sure you backup that data to restore it after the upgrade, with the updated extension version. -Newer versions of services can break functionality or change the performance characteristics you rely on. If your project is eligible for an upgrade, you will be able to find your current service versions from within [the Supabase dashboard](/dashboard/project/_/settings/infrastructure). +Newer versions of services can break functionality or change the performance characteristics you rely on. If your project is eligible for an upgrade, you will be able to find your current service versions from within [the Supabase dashboard](/dashboard/project/_/settings/general). Breaking changes are generally only present in major version upgrades of Postgres and PostgREST. You can find their respective release notes at: diff --git a/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx b/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx index 2c8fba0f00f68..5fef06d558635 100644 --- a/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx +++ b/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx @@ -18,7 +18,7 @@ This error typically occurs as a protective measure to prevent unauthorized acce To address this issue, we recommend following these troubleshooting steps: -- Verify Project ID: Ensure the $PROJECT*REF variable in your commands contains the correct Project ID. You can find your Reference ID under [Project -> Settings -> General](/dashboard/project/*/settings/general) in your Supabase Dashboard. A Reference ID looks something like `xvljpkujuwroxcuvossw`. +- Verify Project ID: Ensure the `$PROJECT_REF` variable in your commands contains the correct Project ID. You can find your Reference ID under [Project -> Settings -> General](/dashboard/project/_/settings/general) in your Supabase Dashboard. A Reference ID looks something like `xvljpkujuwroxcuvossw`. - Authorization Check: Confirm that youโ€™ve been properly authorized. You can also generate a new Access Token in your dashboard and use it for login. Generate a new token [here](/dashboard/account/tokens) and use it to [log in](/docs/reference/cli/supabase-login). - Re-link Project: Try [re-linking](/docs/reference/cli/supabase-link) your project with the newly generated token. - Owner/Admin Permissions: Make sure you have [Owner/Admin](/docs/guides/platform/access-control) permissions for the project. diff --git a/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx index b9caf4f6c0fcd..c13e0e40dd27d 100644 --- a/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx +++ b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx @@ -7,7 +7,7 @@ keywords = [ "grafana", "docker", "metrics", "configuration" ] database_id = "76a4099e-450f-4b5b-a539-224760348c18" --- -This guide is for identifying configuration mistakes in [self-hosted Supabase Grafana installations](/docs/guides/telemetry/metrics/grafana-self-hosted) +This guide is for identifying configuration mistakes in [self-hosted Supabase Grafana installations](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) ## Step 1: Ping your Grafana endpoint diff --git a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx index 9312406008910..53fd669bd82fa 100644 --- a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx +++ b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx @@ -32,7 +32,7 @@ Applying an index can be slow and computationally expensive, so there are a few **1. Make sure your pgvector is the latest available version on Supabase.** -Versions 0.6 and later have accelerated HNSW build speeds. You can observe your current version in the [Dashboard's Extensions Page](/dashboard/project/_/database/extensions). You can perform a software upgrade in the [Infrastructure Settings ](/dashboard/project/_/settings/infrastructure)if necessary. +Versions 0.6 and later have accelerated HNSW build speeds. You can observe your current version in the [Dashboard's Extensions Page](/dashboard/project/_/database/extensions). You can perform a software upgrade in the [General Settings](/dashboard/project/_/settings/general) if necessary. **2. Setting up an external connection** diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx index a5828b5c87372..5e86d52777e46 100644 --- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx @@ -7,7 +7,7 @@ keywords = [ "cpu", "grafana", "metrics" ] database_id = "ef05da0a-f8bc-44a4-9719-5ae811dba104" --- -> [Guide](/docs/guides/telemetry/metrics/grafana-self-hosted) for setting up Supabase Grafana +> [Guide](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) for setting up Supabase Grafana ## CPU diff --git a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx index 9223be77fd3d9..7602ce6a06433 100644 --- a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx +++ b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx @@ -25,7 +25,7 @@ If you are trying to make changes, use the cron functions. If the cron functions ## Cron Jobs are not running -> You should consider initiating a software upgrade in the [Infrastructure Settings](/dashboard/project/_/settings/infrastructure) if your Postgres version is below v15.6.1.122. Upgrading will give you access to pg_cron v1.6.4+, which has many bug fixes and auto-revive capabilities. +> You should consider initiating a software upgrade in the [General Settings](/dashboard/project/_/settings/general) if your Postgres version is below v15.6.1.122. Upgrading will give you access to pg_cron v1.6.4+, which has many bug fixes and auto-revive capabilities. ### Debugging steps: @@ -114,7 +114,7 @@ You can view your concurrent peak connection usage throughout the day at the bot Unfortunately, excessive resource strain can slow down or disrupt jobs. -Go to the [reports page](/dashboard/project/_/observability/database) (or [Supabase Grafana](/docs/guides/telemetry/metrics/grafana-self-hosted) if you have it setup), and check for signs of resource exhaustion. If it's clear your database is under pressure, consider upgrading your compute add-on or following the advice from one of the optimization guides: +Go to the [reports page](/dashboard/project/_/observability/database) (or [Supabase Grafana](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) if you have it setup), and check for signs of resource exhaustion. If it's clear your database is under pressure, consider upgrading your compute add-on or following the advice from one of the optimization guides: - [Connections](https://github.com/orgs/supabase/discussions/27141) - [Disk/IO](https://github.com/orgs/supabase/discussions/27003) @@ -127,7 +127,7 @@ It is important to make sure you are running the latest release of pg_cron (1.6. #### Check the log explorer for more information -Although pg*cron records errors in the `cron.job_run_details` table, in rare cases, more information can be found in the general Postgres logs. You can check the [Log Explorer](/dashboard/project/*/logs/explorer) for failure events with the following query +Although `pg_cron` records errors in the `cron.job_run_details` table, in rare cases, more information can be found in the general Postgres logs. You can check the [Log Explorer](/dashboard/project/_/logs/explorer) for failure events with the following query ```sql select @@ -152,7 +152,7 @@ order by timestamp desc limit 100; ``` -If you're interested in modifying the query, there is an advanced [guide](https://github.com/orgs/supabase/discussions/26224) for navigating the Postgres logs and a general purpose [one](/docs/guides/telemetry/advanced-log-filtering) for applying filters. +If you're interested in modifying the query, there is an advanced [guide](https://github.com/orgs/supabase/discussions/26224) for navigating the Postgres logs and a general-purpose [one](/docs/guides/monitoring-and-debugging/advanced-log-filtering) for applying filters.
@@ -190,7 +190,7 @@ You can then search for your custom messages in the [Logs Interface](/dashboard/ #### Upgrading pg_cron version -The current version of pg*cron on Supabase is 1.6.4. It comes with a [few bug fixes](https://github.com/citusdata/pg_cron/releases/tag/v1.6.4). You should consider upgrading to Postgres v15.6.1.122+ in the[ Infrastructure Settings](/dashboard/project/*/settings/infrastructure) to get the latest extension. +The current version of `pg_cron` on Supabase is 1.6.4. It comes with a [few bug fixes](https://github.com/citusdata/pg_cron/releases/tag/v1.6.4). You should consider upgrading to Postgres v15.6.1.122+ in the [General Settings](/dashboard/project/_/settings/general) to get the latest extension.
diff --git a/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx index e132244871a81..aac2981217f21 100644 --- a/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx +++ b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx @@ -17,14 +17,14 @@ The bug is triggered when an `or()` filter is included in a mutation request. Po ## How to confirm 1. **Reproduce the asymmetry** โ€” run the same filter as a `GET` request. If it succeeds but the `PATCH`/`POST`/`DELETE` fails with the same column reference, the bug is the likely cause. -2. **Check your PostgREST version** โ€” go to [Project Settings > Infrastructure](/dashboard/project/_/settings/infrastructure) and note the Postgres version. PostgREST 14.1 and earlier are affected; 14.4+ includes the fix. +2. **Check your PostgREST version** โ€” go to [Project Settings > General](/dashboard/project/_/settings/general) and note the Postgres version. PostgREST 14.1 and earlier are affected; 14.4+ includes the fix. 3. **Check Postgres logs** โ€” if you have logging enabled, you should see `column "example_column" does not exist` errors correlating with the timestamps of the failed mutation requests. ## Resolution ### Option 1: Upgrade (permanent fix) -Navigate to [Project Settings > Infrastructure](/dashboard/project/_/settings/infrastructure) and upgrade to the latest Postgres version. This automatically upgrades PostgREST to 14.5+, where the bug is resolved. +Navigate to [Project Settings > General](/dashboard/project/_/settings/general) and upgrade to the latest Postgres version. This automatically upgrades PostgREST to 14.5+, where the bug is resolved. ### Option 2: Query workaround (immediate) diff --git a/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx b/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx index 00af4c77436b9..7717cc606e8ea 100644 --- a/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx +++ b/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx @@ -123,7 +123,7 @@ Show RLS and before after for above examples. | ---- | --------------------------------- | -------------------------------------------------- | ------- | ------ | | 1 | auth.uid()=user_id | user_id indexed | 171ms | <.1 | | 2a | auth.uid()=user_id | (select auth.uid()) = user_id | 179 | 9 | -| 2b | is*admin() \_table join* | (select is*admin()) \_table join* | 11,000 | 7 | +| 2b | `is_admin()` _table join_ | (select `is_admin()`) _table join_ | 11,000 | 7 | | 2c | is_admin() OR auth.uid()=user_id | (select is_admin()) OR (select auth.uid()=user_id) | 11,000 | 10 | | 2d | has_role()=role | (select has_role())=role | 178,000 | 12 | | 2e | team_id=any(user_teams()) | team_id=any(array(select user_teams())) | 173000 | 16 | diff --git a/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx b/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx index bf32e5ee9b0a9..c871926220cf2 100644 --- a/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx +++ b/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx @@ -7,7 +7,7 @@ keywords = [ "webhook", "timeout", "pg_net", "postgres", "trigger" ] database_id = "a5524182-9dc4-4470-86fe-45ace109a53a" --- -> NOTE: version 0.10.0 of pg*net is out. You should consider updating your database in the[ Infrastructure Settings](/dashboard/project/*/settings/infrastructure) if you are on a prior version. If you do not know your version, you can check the [Extensions Dashboard](/dashboard/project/_/database/extensions). +> NOTE: version 0.10.0 of `pg_net` is out. You should consider updating your database in the [General Settings](/dashboard/project/_/settings/general) if you are on a prior version. If you do not know your version, you can check the [Extensions Dashboard](/dashboard/project/_/database/extensions). **Debugging Steps** @@ -34,7 +34,7 @@ The `net` tables are special and if triggers on them fail or call a pg_net funct **3. Check for timeout errors** -> **NOTE**: The timeout issue has been patched in pg*net v0.11. It is available in Postgres 15.6.1.135 and above. You can upgrade your version of Postgres in the [Infrastructure Settings.](/dashboard/project/*/settings/infrastructure) +> **NOTE**: The timeout issue has been patched in `pg_net` v0.11. It is available in Postgres 15.6.1.135 and above. You can upgrade your version of Postgres in the [General Settings](/dashboard/project/_/settings/general). Go to your [Table Editor](/dashboard/project/_/editor/) and navigate to the net schema. It will contain two tables: