From 67c6ef67452dc71f6d74496eb4fca1931e0df3dc Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:28:27 -0400 Subject: [PATCH 01/10] feat(pgmeta): filter by composite values (#39179) * feat(pgmeta): filter by composite values Current behavior: Only single columns can be used as filters: `where version = 2` New behavior: Composite values can be used as filters: `where (id, version) = (1, 2)` Motivation: In order to correct an existing bug when fetching table rows, we need to use cursor pagination on the primary key, but in order to handle composite primary keys, we need to be able to filter by composite values. * refactor(pgmeta): allowlist in-tuple operators --- packages/pg-meta/src/query/Query.utils.ts | 68 ++++++++- packages/pg-meta/src/query/QueryFilter.ts | 2 +- packages/pg-meta/src/query/types.ts | 2 +- packages/pg-meta/test/query/query.test.ts | 168 ++++++++++++++++++++++ 4 files changed, 235 insertions(+), 5 deletions(-) diff --git a/packages/pg-meta/src/query/Query.utils.ts b/packages/pg-meta/src/query/Query.utils.ts index 47e2bb9f78f60..5a4af7e305d50 100644 --- a/packages/pg-meta/src/query/Query.utils.ts +++ b/packages/pg-meta/src/query/Query.utils.ts @@ -166,6 +166,23 @@ function applyFilters(query: string, filters: Filter[]) { if (filters.length === 0) return query query += ` where ${filters .map((filter) => { + // Handle composite values + if (Array.isArray(filter.column)) { + switch (filter.operator) { + case 'in': + return inTupleFilterSql(filter) + case '=': + case '<>': + case '>': + case '<': + case '>=': + case '<=': + return defaultTupleFilterSql(filter) + default: + throw new Error(`Cannot use ${filter.operator} operator in a tuple filter`) + } + } + switch (filter.operator) { case 'in': return inFilterSql(filter) @@ -185,16 +202,61 @@ function applyFilters(query: string, filters: Filter[]) { } function inFilterSql(filter: Filter) { - let values + let values: Array if (Array.isArray(filter.value)) { - values = filter.value.map((x: any) => filterLiteral(x)) + values = filter.value.map((x) => filterLiteral(x)) } else { const filterValueTxt = String(filter.value) - values = filterValueTxt.split(',').map((x: any) => filterLiteral(x)) + values = filterValueTxt.split(',').map((x) => filterLiteral(x)) } return `${ident(filter.column)} ${filter.operator} (${values.join(',')})` } +function defaultTupleFilterSql(filter: Filter) { + if (!Array.isArray(filter.column)) { + throw new Error('Use standard applyFilters for single column') + } + if (!Array.isArray(filter.value)) { + throw new Error('Tuple filter value must be an array') + } + if (filter.value.length !== filter.column.length) { + throw new Error('Tuple filter value must have the same length as the column array') + } + + const columns = `(${filter.column.map((c) => ident(c)).join(', ')})` + const values = `(${filter.value.map((v) => filterLiteral(v)).join(', ')})` + return `${columns} ${filter.operator} ${values}` +} + +function inTupleFilterSql(filter: Filter) { + if (!Array.isArray(filter.column)) { + throw new Error('Use inFilterSql for single columns') + } + if (!Array.isArray(filter.value)) { + throw new Error(`Values for a tuple 'in' filter must be an array`) + } + + const columns = `(${filter.column.map((c) => ident(c)).join(', ')})` + + const values = filter.value.map((v) => { + if (Array.isArray(v)) { + if (v.length !== filter.column.length) { + throw new Error(`Tuple value length must match column length`) + } + return `(${v.map((x) => filterLiteral(x)).join(', ')})` + } else { + const filterValueTxt = String(v) + const currValues = filterValueTxt.split(',') + if (currValues.length !== filter.column.length) { + throw new Error(`Tuple value length must match column length`) + } + return `(${currValues.map((x) => filterLiteral(x)).join(', ')})` + } + }) + + return `${columns} ${filter.operator} (${values.join(', ')})` +} + function isFilterSql(filter: Filter) { const filterValueTxt = String(filter.value) switch (filterValueTxt) { diff --git a/packages/pg-meta/src/query/QueryFilter.ts b/packages/pg-meta/src/query/QueryFilter.ts index b36019c5d2d8d..86e05a71e7b3b 100644 --- a/packages/pg-meta/src/query/QueryFilter.ts +++ b/packages/pg-meta/src/query/QueryFilter.ts @@ -18,7 +18,7 @@ export class QueryFilter implements IQueryFilter, IQueryModifier { protected actionOptions?: { returning: boolean; enumArrayColumns?: string[] } ) {} - filter(column: string, operator: FilterOperator, value: any) { + filter(column: string | string[], operator: FilterOperator, value: any) { this.filters.push({ column, operator, value }) return this } diff --git a/packages/pg-meta/src/query/types.ts b/packages/pg-meta/src/query/types.ts index 7e0045cdb81c6..08cfd6348f00a 100644 --- a/packages/pg-meta/src/query/types.ts +++ b/packages/pg-meta/src/query/types.ts @@ -20,7 +20,7 @@ export type FilterOperator = | 'is' export interface Filter { - column: string + column: string | Array operator: FilterOperator value: any } diff --git a/packages/pg-meta/test/query/query.test.ts b/packages/pg-meta/test/query/query.test.ts index 0302f41efc6fc..b53929d026f79 100644 --- a/packages/pg-meta/test/query/query.test.ts +++ b/packages/pg-meta/test/query/query.test.ts @@ -419,6 +419,174 @@ describe('Query.utils', () => { const result = QueryUtils.selectQuery(table, '*', { filters: filters }) expect(result).toContain("where name = 'O''Reilly'") }) + + test('should error if tuple filter value length does not match column length', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1] }] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError( + 'Tuple filter value must have the same length as the column array' + ) + }) + + test('should error if tuple filter value is not an array', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: 1 }] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError( + 'Tuple filter value must be an array' + ) + }) + + test('should correctly handle tuple filters with equality operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) = (1, 2);') + }) + + test('should correctly handle tuple filters with greater than operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '>', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) > (1, 2);') + }) + + test('should correctly handle tuple filters with greater than or equal operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '>=', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) >= (1, 2);') + }) + + test('should correctly handle tuple filters with less than operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<', value: [10, 5] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) < (10, 5);') + }) + + test('should correctly handle tuple filters with less than or equal operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<=', value: [10, 5] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) <= (10, 5);') + }) + + test('should correctly handle tuple filters with not equal operator (<>)', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<>', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) <> (1, 2);') + }) + + test('should correctly handle tuple filters with in operator', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: [ + [1, 2], + [3, 4], + [5, 6], + ], + }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + 'select * from public.users where (id, version) in ((1, 2), (3, 4), (5, 6));' + ) + }) + + test('should error if tuple filters with in operator do not have matching number of array values', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: [[1, 2], [3, 4], [5]], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should correctly handle tuple filters with in operator using strings', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: ['one,two', 'three,four', 'five,six'], + }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + `select * from public.users where (id, version) in (('one', 'two'), ('three', 'four'), ('five', 'six'));` + ) + }) + + test('should error if tuple filters with in operator do not have matching number of stringified values', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: ['one,two', 'three,four', 'five'], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should correctly handle tuple filters with string values', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '=', value: ['John', 'Doe'] }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + "select * from public.users where (first_name, last_name) = ('John', 'Doe');" + ) + }) + + test('should correctly handle mixed tuple and regular filters', () => { + const filters: Filter[] = [ + { column: ['id', 'version'], operator: '>', value: [1, 2] }, + { column: 'active', operator: '=', value: true }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + 'select * from public.users where (id, version) > (1, 2) and active = true;' + ) + }) + + test('should error when trying to use "is" operator as a tuple filter', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'is', + value: [null, null], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "~~" operator as a tuple filter', () => { + const filters: Filter[] = [ + { + column: ['first_name', 'last_name'], + operator: '~~', + value: ['%John%', '%Doe%'], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "~~*" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '~~*', value: ['%john%', '%doe%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "!~~" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '!~~', value: ['%Admin%', '%System%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "!~~*" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '!~~*', value: ['%admin%', '%system%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) }) describe('applySorts', () => { From e83bc6aaaf6d9ce49baa46ace08af39591e7c3d9 Mon Sep 17 00:00:00 2001 From: Lukas Schmid <31358918+dimschlukas@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:40:05 +0200 Subject: [PATCH 02/10] chore(studio): Fix typo in connect interface (#39489) --- .../Connect/content/ionicangular/supabasejs/content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx index ecb2e2e90be0d..89e7f39f251f5 100644 --- a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx @@ -24,7 +24,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => { {` export const environment = { supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}', - supabaseKey: '${projectKeys.publishableKey ?? ''}', + supabaseKey: '${projectKeys.publishableKey ?? ''}', }; `} From 7d743d4fb3437549619ed6d1c776a6d7cc444219 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:43:32 +0200 Subject: [PATCH 03/10] docs: add a note about secret_key_base to self-hosting with docker (#39460) --- apps/docs/content/guides/self-hosting/docker.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index a704a2ef93ea0..ea07e3eb45eb2 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -221,6 +221,7 @@ Update the `./docker/.env` file with your own secrets. In particular, these are - `SMTP_*`: mail server credentials. You can use any SMTP server. - `POOLER_TENANT_ID`: the tenant-id that will be used by Supavisor pooler for your connection string - `PG_META_CRYPTO_KEY`: encryption key for securing connection strings between Studio and postgres-meta +- `SECRET_KEY_BASE`: encryption key for securing Realtime and Supavisor communications. (Must be at least 64 characters; generate with `openssl rand -base64 48`) You will need to [restart](#restarting-all-services) the services for the changes to take effect. From 671c109fa072f0f05b21a2e3220ace6a62d2b4df Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Thu, 16 Oct 2025 15:48:57 +0200 Subject: [PATCH 04/10] docs: update key dropdowns to use new key values (#39428) * Draft * Draft * Draft * fix: wrong query key * Final tweaks * Add to other pages * Update apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/vue.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/getting-started/quickstarts/refine.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Prettier --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- apps/docs/app/contributing/content.mdx | 6 ++- .../ProjectConfigVariables.tsx | 49 ++++++++++-------- .../ProjectConfigVariables.utils.ts | 5 +- .../auth/server-side/creating-a-client.mdx | 5 +- .../guides/auth/server-side/nextjs.mdx | 6 ++- .../guides/auth/server-side/sveltekit.mdx | 3 +- .../getting-started/quickstarts/flutter.mdx | 3 +- .../getting-started/quickstarts/hono.mdx | 3 +- .../quickstarts/ios-swiftui.mdx | 3 +- .../getting-started/quickstarts/kotlin.mdx | 3 +- .../getting-started/quickstarts/nextjs.mdx | 3 +- .../getting-started/quickstarts/nuxtjs.mdx | 3 +- .../getting-started/quickstarts/reactjs.mdx | 3 +- .../getting-started/quickstarts/refine.mdx | 3 +- .../getting-started/quickstarts/solidjs.mdx | 3 +- .../getting-started/quickstarts/sveltekit.mdx | 3 +- .../getting-started/quickstarts/vue.mdx | 3 +- apps/docs/lib/fetch/fetchWrappers.ts | 1 - apps/docs/lib/fetch/projectApi.ts | 50 +++++++++++++++---- 19 files changed, 107 insertions(+), 51 deletions(-) diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index c3632971a2a52..1bff38834c02a 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -354,11 +354,13 @@ Some guides and tutorials will require that users copy their Supabase project UR ```mdx - + + ``` - + + ### Step Hike diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx index b34ed122426bb..d384746540b0e 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx @@ -6,7 +6,7 @@ import type { Project, Variable, } from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils' -import type { ProjectApiData } from '~/lib/fetch/projectApi' +import type { ProjectKeys, ProjectSettings } from '~/lib/fetch/projectApi' import { Check, Copy } from 'lucide-react' import Link from 'next/link' @@ -34,7 +34,7 @@ import { useCopy } from '~/hooks/useCopy' import { useBranchesQuery } from '~/lib/fetch/branches' import { useOrganizationsQuery } from '~/lib/fetch/organizations' import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler' -import { useProjectApiQuery } from '~/lib/fetch/projectApi' +import { useProjectSettingsQuery, useProjectKeysQuery } from '~/lib/fetch/projectApi' import { isProjectPaused, useProjectsQuery } from '~/lib/fetch/projects' import { retrieve, storeOrRemoveNull } from '~/lib/storage' import { useOnLogout } from '~/lib/userAuth' @@ -274,14 +274,25 @@ function VariableView({ variable, className }: { variable: Variable; className?: const hasBranches = selectedProject?.is_branch_enabled ?? false const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref - const needsApiQuery = variable === 'publishableKey' || variable === 'url' + const needsApiQuery = variable === 'publishable' || variable === 'anon' || variable === 'url' const needsSupavisorQuery = variable === 'sessionPooler' const { - data: apiData, - isPending: isApiPending, - isError: isApiError, - } = useProjectApiQuery( + data: apiSettingsData, + isPending: isApiSettingsPending, + isError: isApiSettingsError, + } = useProjectSettingsQuery( + { + projectRef: ref, + }, + { enabled: isLoggedIn && !!ref && !projectPaused && needsApiQuery } + ) + + const { + data: apiKeysData, + isPending: isApiKeysPending, + isError: isApiKeysError, + } = useProjectKeysQuery( { projectRef: ref, }, @@ -299,15 +310,6 @@ function VariableView({ variable, className }: { variable: Variable; className?: { enabled: isLoggedIn && !!ref && !projectPaused && needsSupavisorQuery } ) - function isInvalidApiData(apiData: ProjectApiData) { - switch (variable) { - case 'url': - return !apiData.app_config?.endpoint - case 'publishableKey': - return !apiData.service_api_keys?.some((key) => key.tags === 'anon') - } - } - function isInvalidSupavisorData(supavisorData: SupavisorConfigData) { return supavisorData.length === 0 } @@ -320,24 +322,29 @@ function VariableView({ variable, className }: { variable: Variable; className?: ? 'loggedIn.noSelectedProject' : projectPaused ? 'loggedIn.selectedProject.projectPaused' - : (needsApiQuery ? isApiPending : isSupavisorPending) + : (needsApiQuery ? isApiSettingsPending || isApiKeysPending : isSupavisorPending) ? 'loggedIn.selectedProject.dataPending' : ( needsApiQuery - ? isApiError || isInvalidApiData(apiData!) + ? isApiSettingsError || isApiKeysError : isSupavisorError || isInvalidSupavisorData(supavisorConfig!) ) ? 'loggedIn.selectedProject.dataError' : 'loggedIn.selectedProject.dataSuccess' let variableValue: string = '' + if (stateSummary === 'loggedIn.selectedProject.dataSuccess') { switch (variable) { case 'url': - variableValue = `https://${apiData?.app_config?.endpoint}` + variableValue = `https://${apiSettingsData?.app_config?.endpoint}` + break + case 'anon': + variableValue = + apiKeysData?.find((key) => key.type === 'legacy' && key.id === 'anon')?.api_key || '' break - case 'publishableKey': - variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || '' + case 'publishable': + variableValue = apiKeysData?.find((key) => key.type === 'publishable')?.api_key || '' break case 'sessionPooler': variableValue = supavisorConfig?.[0]?.connection_string || '' diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts index 2ed28261bedd4..cb06766dbad7b 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts @@ -6,7 +6,7 @@ export type Org = OrganizationsData[number] export type Project = ProjectsData[number] export type Branch = BranchesData[number] -export type Variable = 'url' | 'publishableKey' | 'sessionPooler' +export type Variable = 'url' | 'publishable' | 'anon' | 'sessionPooler' function removeDoubleQuotes(str: string) { return str.replaceAll('"', '') @@ -30,7 +30,8 @@ function unescapeDoubleQuotes(str: string) { export const prettyFormatVariable: Record = { url: 'Project URL', - publishableKey: 'Publishable key', + anon: 'Anon key', + publishable: 'Publishable key', sessionPooler: 'Connection string (pooler session mode)', } diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index bbefe0d40c0c5..4440e53b0e8e7 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -39,10 +39,11 @@ pnpm add @supabase/ssr @supabase/supabase-js ## Set environment variables -In your environment variables file, set your Supabase URL and Supabase Anon Key: +In your environment variables file, set your Supabase URL and Key: - + + diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index ff02120c23259..7dfb218570adb 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -39,7 +39,8 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + @@ -560,7 +561,8 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + diff --git a/apps/docs/content/guides/auth/server-side/sveltekit.mdx b/apps/docs/content/guides/auth/server-side/sveltekit.mdx index b8de7de0425ee..e14fdf6aef94e 100644 --- a/apps/docs/content/guides/auth/server-side/sveltekit.mdx +++ b/apps/docs/content/guides/auth/server-side/sveltekit.mdx @@ -34,7 +34,8 @@ Create a `.env.local` file in your project root directory. Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx index 54142ac73c5b6..b36cbd8f6c992 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx @@ -58,7 +58,8 @@ hideToc: true Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and public API (anon) key: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index 565ee5fae01a7..7efe526cbd8fa 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -50,7 +50,8 @@ hideToc: true Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings. - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx index 9424fd47d9f77..2cda6ab608902 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx @@ -42,7 +42,8 @@ hideToc: true Create a new `Supabase.swift` file add a new Supabase instance using your project URL and public API (anon) key: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx index 59f7b6cf283bc..12711786238cf 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx @@ -79,7 +79,8 @@ hideToc: true Replace the `supabaseUrl` and `supabaseKey` with your own: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index e7fc33911c063..2a58a49ab6fec 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -40,7 +40,8 @@ hideToc: true Rename `.env.example` to `.env.local` and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx index 0da48358c7fc4..d9a6a20cd747b 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx index 1bf67565651f1..d9bf9f3d13892 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx index a6cb4ff632ed0..11214198c801e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx @@ -78,7 +78,8 @@ hideToc: true You now have to update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API. The `supabaseClient` is used in auth provider and data provider methods that allow the refine app to connect to your Supabase backend. - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx index 10aa736884a14..a0d82752f2e8e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index c5b20f469cb02..82061a1386828 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -57,7 +57,8 @@ hideToc: true Create a `.env` file at the root of your project and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx index fb1de8db617fd..715acdd2eafb7 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/lib/fetch/fetchWrappers.ts b/apps/docs/lib/fetch/fetchWrappers.ts index f8a145f5878e1..50fcd93ebf1ea 100644 --- a/apps/docs/lib/fetch/fetchWrappers.ts +++ b/apps/docs/lib/fetch/fetchWrappers.ts @@ -28,7 +28,6 @@ async function constructHeaders(headersInit?: HeadersInit | undefined) { headers.set('Authorization', `Bearer ${accessToken}`) } } - return headers } diff --git a/apps/docs/lib/fetch/projectApi.ts b/apps/docs/lib/fetch/projectApi.ts index 8a88f8062de55..8fe4a68d487d7 100644 --- a/apps/docs/lib/fetch/projectApi.ts +++ b/apps/docs/lib/fetch/projectApi.ts @@ -4,41 +4,73 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' const projectApiKeys = { api: (projectRef: string | undefined) => ['projects', projectRef, 'api'] as const, + settings: (projectRef: string | undefined) => ['projects', projectRef, 'settings'] as const, } export interface ProjectApiVariables { projectRef?: string } +type ProjectApiError = ResponseError + +export type ProjectKeys = Awaited> -async function getProjectApi({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { +export type ProjectSettings = Awaited> + +async function getProjectKeys({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { if (!projectRef) { throw Error('projectRef is required') } - const { data, error } = await get('/platform/projects/{ref}/settings', { + const { data, error } = await get('/v1/projects/{ref}/api-keys', { params: { path: { ref: projectRef }, }, signal, }) if (error) throw error - return data } -export type ProjectApiData = Awaited> -type ProjectApiError = ResponseError +async function getProjectSettings({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { + if (!projectRef) { + throw Error('projectRef is required') + } -export function useProjectApiQuery( + const { data, error } = await get('/platform/projects/{ref}/settings', { + params: { + path: { ref: projectRef }, + }, + signal, + }) + if (error) throw error + return data +} + +export function useProjectKeysQuery( { projectRef }: ProjectApiVariables, { enabled = true, ...options - }: Omit, 'queryKey'> = {} + }: Omit, 'queryKey'> = {} ) { - return useQuery({ + return useQuery({ queryKey: projectApiKeys.api(projectRef), - queryFn: ({ signal }) => getProjectApi({ projectRef }, signal), + queryFn: ({ signal }) => getProjectKeys({ projectRef }, signal), + enabled, + ...options, + }) +} + +export function useProjectSettingsQuery( + { projectRef }: ProjectApiVariables, + { + enabled = true, + ...options + }: Omit, 'queryKey'> = {} +) { + return useQuery({ + queryKey: projectApiKeys.settings(projectRef), + queryFn: ({ signal }) => getProjectSettings({ projectRef }, signal), enabled, ...options, }) From a6225edf2b86e70910b72b35ef8c92a618fd60c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A4=96Esteban=20Dalel=20R?= Date: Thu, 16 Oct 2025 08:53:06 -0500 Subject: [PATCH 05/10] docs: Fix grammar in detach_archive documentation (#39495) Corrected grammatical error in the documentation regarding the 'detach_archive' functionality. --- apps/docs/content/guides/queues/pgmq.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/queues/pgmq.mdx b/apps/docs/content/guides/queues/pgmq.mdx index d5c01326e8fd2..6b7f85d8cb5e0 100644 --- a/apps/docs/content/guides/queues/pgmq.mdx +++ b/apps/docs/content/guides/queues/pgmq.mdx @@ -77,7 +77,7 @@ select pgmq.create_unlogged('my_unlogged'); #### `detach_archive` -Drop the queue's archive table as a member of the PGMQ extension. Useful for preventing the queue's archive table from being drop when `drop extension pgmq` is executed. +Drop the queue's archive table as a member of the PGMQ extension. Useful for preventing the queue's archive table from being dropped when `drop extension pgmq` is executed. This does not prevent the further archives() from appending to the archive table. {/* prettier-ignore */} From 36a4a774f7de8fb5f666f1a42242f45b3a474eb7 Mon Sep 17 00:00:00 2001 From: Devanshu Sharma Date: Thu, 16 Oct 2025 19:34:07 +0530 Subject: [PATCH 06/10] refactor(docs): enhance the docs sidebar UI (#38869) * refactor(docs): enhance NavigationMenuGuideListItems component * cleanup.. * Resolve formatting conflicts in NavigationMenuGuideListItems * Came up with a crazy recursive approach... * some changes. * More cleanups... * more cleanups.... * formatting the stuff * refactor(navigation): remove sample menu data and optimize icon rendering in RecursiveNavigation * cleanup... * feat: enhance navigation with recursive dropdown indicators and performance optimizations - Add visual chevron indicators for expandable sidebar items - Implement recursive navigation system using existing NavMenuSection types - Auto-expand accordion sections containing current active page on load/refresh - Extract LinkContainer as independent component to prevent recreation on renders - Fix React 19 compatibility issues with ref handling using useCallback pattern - Resolve duplicate key warnings with improved key generation strategy - Optimize accordion transition timing (500ms duration with ease-in-out) to prevent element overlap - Preserve existing icon logic with hasLightIcon support for theme variants - Maintain backward compatibility with existing navigation data structures Performance improvements: - Memoized recursive components to prevent unnecessary re-renders - Stable component references for better React reconciliation - Proper TypeScript interfaces with explicit prop definitions UX improvements: - Smooth accordion animations with physics-based easing - Clear visual indicators for expandable menu items - Automatic expansion of sections containing current page - Professional-grade transition animations Technical details: - Uses existing NavMenuSection recursive type structure - Implements containsActivePath helper for active page detection - Leverages Radix UI accordion with proper data-state attributes - Maintains all existing functionality including dynamic menu injection * Cleanup... * feat: implement cookie-based persisted state for navigation accordion - Replace localStorage with cookie-based persistence for SSR compatibility - Add proper cookie helpers with SameSite=Lax and 30-day expiration - Implement initialization state to prevent hydration mismatches - Maintain auto-expand functionality for active page sections - Preserve user's manual accordion state across page refreshes and sessions - Add debounced cookie updates (300ms) for performance optimization - Use proper cookie naming convention: 'supabase-docs-nav-state' Benefits: - SSR compatible: Works with server-side rendering - Cross-session persistence: Maintains state across browser sessions - Better security: Cookies are more secure than localStorage - Performance: Debounced updates prevent excessive cookie writes - User experience: Seamless navigation state preservation * refactor: implement individual item-based persistence following reference pattern - Replace global state management with individual item persistence - Use sessionStorage with 'nav-expansion-' prefix for each item - Follow the exact pattern from reference: usePersistedExpansionState hook - Maintain auto-expansion for active page sections - Simplify state management by removing complex global state - Each accordion item manages its own expansion state independently - Preserve user's manual toggle state across page refreshes - Use sessionStorage instead of cookies for better performance Benefits: - Cleaner architecture: Each item manages its own state - Better performance: No global state updates - Simpler logic: Direct item-to-storage mapping - Reference pattern compliance: Follows established patterns - Individual control: Each section can be toggled independently - Session persistence: Maintains state during browser session * refactor: implement URL-driven navigation state following Supabase docs pattern - Replace sessionStorage persistence with URL-driven expansion state - Follow the established Supabase docs pattern: URL as single source of truth - Remove individual item persistence hooks in favor of pathname-based logic - Implement useUrlDrivenExpansion hook that determines open sections from current URL - Use getSectionsContainingPath to find all sections that should be expanded - Remove manual toggle functionality - sections open/close based on URL navigation - Maintain smooth transitions and visual indicators for expandable sections - Ensure consistency and reliability by using URL as the definitive state source Benefits: - Reliability: URL is always the single source of truth - Consistency: Matches existing Supabase docs navigation behavior - Simplicity: No complex state management or storage concerns - Performance: No localStorage/sessionStorage operations - SSR Compatible: Works perfectly with server-side rendering - Predictable: Navigation state is always consistent with current page * persistant state. * file delete * cleanup.. * file cleanup * cleanup... * cleanup.. * formatting.. * aww shit that it. --------- Co-authored-by: Alan Daniel --- .../NavigationMenuGuideList.tsx | 2 +- .../NavigationMenuGuideListItems.tsx | 107 ++++++++++++------ 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx index 5ce453b67de41..97620f6186e1b 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx @@ -94,7 +94,7 @@ export function NavigationMenuGuideListWrapper({ key={id} type="single" value={firstLevelRoute} - className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150" + className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150 w-full" > {children} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index bf34f7541a4c5..9f9e2a691583e 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -1,6 +1,7 @@ import * as Accordion from '@radix-ui/react-accordion' import { usePathname } from 'next/navigation' import { useTheme } from 'next-themes' +import { ChevronDown } from 'lucide-react' import Image from 'next/legacy/image' import Link from 'next/link' import React, { useEffect, useRef } from 'react' @@ -33,6 +34,9 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any const activeItem = props.subItem.url === pathname const activeItemRef = useRef(null) + const isChildActive = + props.subItem.items && props.subItem.items.some((child: any) => child.url === pathname) + const LinkContainer = (props) => { const isExternal = props.url.startsWith('https://') @@ -67,7 +71,62 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any )} - + {props.subItem.items && props.subItem.items.length > 0 ? ( + + + + +
+ {props.subItem.icon && ( + {props.subItem.name} + )} + {props.subItem.name} +
+ +
+
+ + {props.subItem.items + .filter((subItem) => subItem.enabled !== false) + .map((subSubItem) => { + return ( +
  • + + {subSubItem.name} + +
  • + ) + })} +
    +
    +
    + ) : (
  • - {props.subItem.icon && ( - {props.subItem.name} - )} - {props.subItem.name} +
    + {props.subItem.icon && ( + {props.subItem.name} + )} + {props.subItem.name} +
  • - - {props.subItem.items && props.subItem.items.length > 0 && ( - - {props.subItem.items - .filter((subItem) => subItem.enabled !== false) - .map((subSubItem) => { - return ( -
  • - - {subSubItem.name} - -
  • - ) - })} -
    - )} -
    + )} ) }) From b15b8f931e248b26510595b0d0330b10b50bfce6 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Thu, 16 Oct 2025 16:50:26 +0200 Subject: [PATCH 07/10] fix(studio): remove include data toggle if no pitr (#39533) * fix(ui): remove include data option if no PITR It causes users confusion to see the toggle in such cases * fix: just hide toggle --- .../interfaces/BranchManagement/CreateBranchModal.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 93ce13e3f7367..fc3397f00f205 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -396,11 +396,9 @@ export const CreateBranchModal = () => { description="Clone production data into this branch" > - + {hasPitrEnabled && ( + + )} )} From 6b6b7c048595ee5cddda4d6ea737a514a561f3fa Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:15:20 +0200 Subject: [PATCH 08/10] cleanup: consolidate database report (#39525) * consolidate * fix availableIn --- apps/studio/data/reports/database-charts.ts | 254 +++--------------- .../pages/project/[ref]/reports/database.tsx | 146 ++++------ 2 files changed, 88 insertions(+), 312 deletions(-) diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 3f8efcc622cf9..692edba8947d1 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -7,210 +7,6 @@ import { DiskAttributesData } from '../config/disk-attributes-query' import { MaxConnectionsData } from '../database/max-connections-query' import { Project } from '../projects/project-detail-query' -export const getReportAttributes = (diskConfig?: DiskAttributesData): ReportAttributes[] => { - return [ - { - id: 'ram-usage', - label: 'Memory usage', - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - hideChartType: false, - defaultChartStyle: 'bar', - showMaxValue: false, - showGrid: false, - syncId: 'database-reports', - valuePrecision: 0, - format: '%', - attributes: [ - { - attribute: 'ram_usage', - provider: 'infra-monitoring', - label: 'Memory usage', - format: '%', - tooltip: 'RAM usage by the database', - }, - ], - }, - { - id: 'avg_cpu_usage', - label: 'Average CPU usage', - syncId: 'database-reports', - format: '%', - valuePrecision: 2, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'avg_cpu_usage', - provider: 'infra-monitoring', - label: 'Average CPU usage', - format: '%', - tooltip: 'Average CPU usage', - }, - ], - }, - { - id: 'max_cpu_usage', - label: 'Max CPU usage', - syncId: 'database-reports', - format: '%', - valuePrecision: 2, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'max_cpu_usage', - provider: 'infra-monitoring', - label: 'Max CPU usage', - format: '%', - tooltip: 'Max CPU usage', - }, - ], - }, - { - id: 'disk-iops', - label: 'Disk Input/Output operations per second (IOPS)', - syncId: 'database-reports', - availableIn: ['free', 'pro'], - hide: false, - showTooltip: true, - valuePrecision: 2, - showLegend: true, - hideChartType: false, - showGrid: true, - showMaxValue: false, - YAxisProps: { - width: 35, - tickFormatter: (value: any) => numberFormatter(value, 2), - }, - defaultChartStyle: 'line', - docsUrl: `${DOCS_URL}/guides/platform/compute-and-disk#compute-size`, - attributes: [ - { - attribute: 'disk_iops_write', - provider: 'infra-monitoring', - label: 'Write IOPS', - tooltip: - 'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity', - }, - { - attribute: 'disk_iops_read', - provider: 'infra-monitoring', - label: 'Read IOPS', - tooltip: - 'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching', - }, - { - attribute: 'disk_iops_max', - provider: 'reference-line', - label: 'Max IOPS', - value: diskConfig?.attributes?.iops, - tooltip: - 'Maximum IOPS (Input/Output Operations Per Second) for your current compute size', - isMaxValue: true, - }, - ], - }, - { - id: 'disk-io-usage', - label: 'Disk IO Usage', - syncId: 'database-reports', - availableIn: ['team', 'enterprise'], - hide: false, - format: '%', - attributes: [], - }, - { - id: 'pooler-database-connections', - label: 'Pooler to Database connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - hideChartType: false, - showGrid: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'pg_stat_database_num_backends', - provider: 'infra-monitoring', - label: 'Database connections', - tooltip: 'Number of pooler connections to the database', - }, - ], - }, - { - id: 'supavisor-connections', - label: 'Shared Pooler connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'supavisor_connections_active', - provider: 'infra-monitoring', - label: 'Client to Shared Pooler connections', - tooltip: 'Active connections from clients to the shared pooler', - }, - ], - }, - { - id: 'pgbouncer-connections', - label: 'Dedicated Pooler connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['pro', 'team'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'client_connections_pgbouncer', - provider: 'infra-monitoring', - label: 'Client to Dedicated Pooler connections', - tooltip: 'PgBouncer connections', - }, - ], - }, - { - id: 'disk-size', - label: 'Disk Usage', - syncId: 'database-reports', - availableIn: ['team', 'enterprise'], - hide: false, - attributes: [], - }, - ] -} - export const getReportAttributesV2: ( org: Organization, project: Project, @@ -227,7 +23,7 @@ export const getReportAttributesV2: ( id: 'ram-usage', label: 'Memory usage', docsUrl: `${DOCS_URL}/guides/telemetry/reports#memory-usage`, - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, @@ -272,7 +68,7 @@ export const getReportAttributesV2: ( syncId: 'database-reports', format: '%', valuePrecision: 2, - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, @@ -339,7 +135,7 @@ export const getReportAttributesV2: ( label: 'Disk Input/Output operations per second (IOPS)', docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-inputoutput-operations-per-second-iops`, syncId: 'database-reports', - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, valuePrecision: 2, @@ -408,12 +204,46 @@ export const getReportAttributesV2: ( ], }, { + // Client Connections metric for free tier + id: 'client-connections-basic', + label: 'Database Connections', + syncId: 'database-reports', + valuePrecision: 0, + availableIn: ['free'], + hide: !isFreePlan, + showTooltip: false, + showLegend: false, + showMaxValue: true, + hideChartType: false, + showGrid: true, + YAxisProps: { width: 30 }, + defaultChartStyle: 'line', + docsUrl: `${DOCS_URL}/guides/telemetry/reports#database-connections`, + attributes: [ + { + attribute: 'total_db_connections', + provider: 'infra-monitoring', + label: 'Total connections', + tooltip: 'Total number of active database connections', + }, + { + attribute: 'max_db_connections', + provider: 'reference-line', + label: 'Max connections', + value: maxConnections?.maxConnections, + tooltip: 'Max available connections for your current compute size', + isMaxValue: true, + }, + ], + }, + { + // advanced client connections metric for paid and above id: 'client-connections', label: 'Database Connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['team', 'enterprise'], - hide: false, + availableIn: ['pro', 'team', 'enterprise'], + hide: isFreePlan, showTooltip: true, showLegend: true, showMaxValue: true, @@ -476,7 +306,7 @@ export const getReportAttributesV2: ( label: 'Dedicated Pooler Client Connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['pro', 'team'], + availableIn: ['pro', 'team', 'enterprise'], hide: isFreePlan, showTooltip: true, showLegend: true, @@ -508,7 +338,7 @@ export const getReportAttributesV2: ( label: 'Shared Pooler (Supavisor) client connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['pro', 'team'], + availableIn: ['pro', 'team', 'enterprise'], hide: isFreePlan, showTooltip: false, showLegend: false, @@ -531,7 +361,7 @@ export const getReportAttributesV2: ( label: 'Disk Usage', syncId: 'database-reports', valuePrecision: 2, - availableIn: ['free', 'pro', 'team'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 0ba53523e2e88..5eaca8b089d89 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -6,7 +6,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { useFlag, useParams } from 'common' +import { useParams } from 'common' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' @@ -33,7 +33,7 @@ import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mu import { useDatabaseSizeQuery } from 'data/database/database-size-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' -import { getReportAttributes, getReportAttributesV2 } from 'data/reports/database-charts' +import { getReportAttributesV2 } from 'data/reports/database-charts' import { useDatabaseReport } from 'data/reports/database-report-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -65,7 +65,6 @@ export default DatabaseReport const DatabaseUsage = () => { const { db, chart, ref } = useParams() - const isReportsV2 = useFlag('reportsDatabaseV2') const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() @@ -81,10 +80,6 @@ const DatabaseUsage = () => { handleDatePickerChange, } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) - const isTeamsOrEnterprisePlan = - !isOrgPlanLoading && (orgPlan?.id === 'team' || orgPlan?.id === 'enterprise') - const showChartsV2 = isReportsV2 || isTeamsOrEnterprisePlan - const state = useDatabaseSelectorStateSnapshot() const queryClient = useQueryClient() @@ -130,8 +125,7 @@ const DatabaseUsage = () => { } ) - const REPORT_ATTRIBUTES = getReportAttributes(diskConfig) - const REPORT_ATTRIBUTES_V2 = getReportAttributesV2( + const REPORT_ATTRIBUTES = getReportAttributesV2( org!, project!, diskConfig, @@ -149,51 +143,22 @@ const DatabaseUsage = () => { const onRefreshReport = async () => { if (!selectedDateRange) return - // [Joshen] Since we can't track individual loading states for each chart - // so for now we mock a loading state that only lasts for a second setIsRefreshing(true) refresh() const { period_start, period_end, interval } = selectedDateRange - REPORT_ATTRIBUTES.forEach((attr) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr?.id, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) - if (showChartsV2) { - REPORT_ATTRIBUTES_V2.forEach((chart: any) => { - chart.attributes.forEach((attr: any) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr.attribute, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) - }) - } else { - REPORT_ATTRIBUTES.forEach((chart: any) => { - chart.attributes.forEach((attr: any) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr.attribute, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) + REPORT_ATTRIBUTES.forEach((chart: any) => { + chart.attributes.forEach((attr: any) => { + queryClient.invalidateQueries( + analyticsKeys.infraMonitoring(ref, { + attribute: attr.attribute, + startDate: period_start.date, + endDate: period_end.date, + interval, + databaseIdentifier: state.selectedDatabaseId, + }) + ) }) - } + }) if (isReplicaSelected) { queryClient.invalidateQueries( analyticsKeys.infraMonitoring(ref, { @@ -274,56 +239,37 @@ const DatabaseUsage = () => { > {selectedDateRange && orgPlan?.id && - (showChartsV2 - ? REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => ( - - )) - : REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart, i) => - chart.availableIn?.includes(orgPlan?.id) ? ( - - ) : ( - - ) - ))} + REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => + chart.availableIn?.includes(orgPlan?.id) ? ( + + ) : ( + + ) + )} {selectedDateRange && isReplicaSelected && ( From 5f7a20e65933617f5850576aff070ac53a06094e Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 16 Oct 2025 18:16:15 +0300 Subject: [PATCH 09/10] feat: Add ReactQuery lint rules (#39517) * Add tanstack query lint rules. * Fix the names of the RQ config. --- packages/eslint-config-supabase/next.js | 17 ++++- packages/eslint-config-supabase/package.json | 10 +-- pnpm-lock.yaml | 71 ++++++++++++++++---- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/packages/eslint-config-supabase/next.js b/packages/eslint-config-supabase/next.js index b19ed52548189..667ffba631de0 100644 --- a/packages/eslint-config-supabase/next.js +++ b/packages/eslint-config-supabase/next.js @@ -3,7 +3,8 @@ const js = require('@eslint/js') const { FlatCompat } = require('@eslint/eslintrc') const prettierConfig = require('eslint-config-prettier/flat') const { default: turboConfig } = require('eslint-config-turbo/flat') -const { off } = require('process') +const { fixupPluginRules } = require('@eslint/compat') +const tanstackQuery = require('@tanstack/eslint-plugin-query') const compat = new FlatCompat({ baseDirectory: __dirname, @@ -11,11 +12,25 @@ const compat = new FlatCompat({ allConfig: js.configs.all, }) +// Tanstack Query config is meant for the old non-flat esling configs. This adapts it to work with flat configs. v5 of +// the plugin supports flat configs natively. +const tanstackQueryConfig = { + name: '@tanstack/query', + plugins: { '@tanstack/query': fixupPluginRules(tanstackQuery) }, + rules: { + '@tanstack/query/exhaustive-deps': 'warn', + '@tanstack/query/no-deprecated-options': 'warn', + '@tanstack/query/prefer-query-object-syntax': 'warn', + '@tanstack/query/stable-query-client': 'warn', + }, +} + module.exports = defineConfig([ // Global ignore for the .next folder { ignores: ['.next', 'public'] }, turboConfig, prettierConfig, + tanstackQueryConfig, { extends: compat.extends('next/core-web-vitals'), linterOptions: { diff --git a/packages/eslint-config-supabase/package.json b/packages/eslint-config-supabase/package.json index 5610d5edeef11..4a73e5f229c55 100644 --- a/packages/eslint-config-supabase/package.json +++ b/packages/eslint-config-supabase/package.json @@ -7,14 +7,14 @@ "preinstall": "npx only-allow pnpm", "clean": "rimraf node_modules" }, - "dependencies": { - "eslint-config-next": "^15.5.0", - "eslint-config-prettier": "^10.0.0", - "eslint-config-turbo": "^2.5.0" - }, "devDependencies": { + "@eslint/compat": "^1.4.0", "@eslint/eslintrc": "^3.0.0", "@eslint/js": "^9.0.0", + "@tanstack/eslint-plugin-query": "^4.0.0", + "eslint-config-next": "^15.5.0", + "eslint-config-prettier": "^10.0.0", + "eslint-config-turbo": "^2.5.0", "typescript": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 655a22e78d140..f6797a4e14a98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2055,7 +2055,19 @@ importers: version: 5.9.2 packages/eslint-config-supabase: - dependencies: + devDependencies: + '@eslint/compat': + specifier: ^1.4.0 + version: 1.4.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + '@eslint/eslintrc': + specifier: ^3.0.0 + version: 3.3.1(supports-color@8.1.1) + '@eslint/js': + specifier: ^9.0.0 + version: 9.37.0 + '@tanstack/eslint-plugin-query': + specifier: ^4.0.0 + version: 4.39.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) eslint-config-next: specifier: ^15.5.0 version: 15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) @@ -2065,13 +2077,6 @@ importers: eslint-config-turbo: specifier: ^2.5.0 version: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) - devDependencies: - '@eslint/eslintrc': - specifier: ^3.0.0 - version: 3.3.1(supports-color@8.1.1) - '@eslint/js': - specifier: ^9.0.0 - version: 9.37.0 typescript: specifier: 'catalog:' version: 5.9.2 @@ -2127,7 +2132,7 @@ importers: version: 8.11.11 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -2363,7 +2368,7 @@ importers: version: 15.5.7 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) common: specifier: workspace:* version: link:../common @@ -3770,6 +3775,15 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.4.0': + resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + '@eslint/config-array@0.21.0': resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8974,6 +8988,11 @@ packages: resolution: {integrity: sha512-eXjzg2T7eiEruKXtu1XQ2bHV1oO8muGWs+TuDWH3tIFPwH1hIzahWpcTwz5lc7jW5xqud3gru2tK6wTk2JlIrw==} engines: {node: '>=12'} + '@tanstack/eslint-plugin-query@4.39.1': + resolution: {integrity: sha512-5YDX4mdRC0hllHKp531CnScFWZU7aFrJ1aTyyuaB6+z0/i0JfcKuckSTYaji3vUk82GALM90eWwHFVRAch+7tQ==} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@tanstack/history@1.114.22': resolution: {integrity: sha512-CNwKraj/Xa8H7DUyzrFBQC3wL96JzIxT4i9CW0hxqFNNmLDyUcMJr8264iqqfxC0u1lFSG96URad08T2Qhadpw==} engines: {node: '>=12'} @@ -22286,6 +22305,12 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} + '@eslint/compat@1.4.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + '@eslint/core': 0.16.0 + optionalDependencies: + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@eslint/config-array@0.21.0(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.6 @@ -28779,6 +28804,10 @@ snapshots: - tsx - yaml + '@tanstack/eslint-plugin-query@4.39.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@tanstack/history@1.114.22': {} '@tanstack/match-sorter-utils@8.8.4': @@ -30215,7 +30244,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': + '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -30233,6 +30262,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + debug: 4.4.3(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) + istanbul-reports: 3.1.7 + magic-string: 0.30.19 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.0.9': dependencies: '@vitest/spy': 3.0.9 @@ -30308,7 +30355,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) '@vitest/utils@3.0.4': dependencies: From c55266b425d46abe83789cbb4d0dfe180ed0d5b5 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:16:42 +0200 Subject: [PATCH 10/10] feat: add tz in tooltip FE-1849 (#39596) * add tz in tooltip * clean --- apps/studio/components/ui/Charts/ComposedChart.utils.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index b5c8134bf45c3..25cd293a1edb9 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -173,6 +173,8 @@ const CustomTooltip = ({ ...(maxValueAttribute?.attribute ? [maxValueAttribute.attribute] : []), ] + const localTimeZone = dayjs.tz.guess() + const total = showTotal && calculateTotalChartAggregate(payload, attributesToIgnoreFromTotal) const getIcon = (color: string, isMax: boolean) => @@ -212,6 +214,7 @@ const CustomTooltip = ({ !isActiveHoveredChart && 'opacity-0' )} > +

    {localTimeZone}

    {dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}

    {payload.reverse().map((entry: any, index: number) => (