From b2e368b623e31c02a6fa4946ee70e788d187d4d4 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 4 Jun 2025 12:51:57 +0800 Subject: [PATCH 01/19] Chore/api keys cleanup (#36097) * Remove api-keys.tsx, duplicated with api-keys/index.ts * Remove use of newApiKeys feature flag in SettingsMenu.utils.tsx * Refactor use of newApiKeys flag to basicApiKeys * Update logic for shouldDisableUI in useApiKeysVisibility hook * Smol fix --- .../interfaces/APIKeys/LegacyAPIKeys.tsx | 10 -------- .../interfaces/APIKeys/PublishableAPIKeys.tsx | 1 - .../APIKeys/hooks/useApiKeysVisibility.ts | 9 ++++--- .../ProjectSettingsLayout/SettingsLayout.tsx | 4 --- .../SettingsMenu.utils.tsx | 1 - .../ui/ProjectSettings/DisplayApiSettings.tsx | 10 ++++---- .../pages/project/[ref]/settings/api-keys.tsx | 25 ------------------- .../project/[ref]/settings/api-keys/index.tsx | 4 +-- 8 files changed, 12 insertions(+), 52 deletions(-) delete mode 100644 apps/studio/components/interfaces/APIKeys/LegacyAPIKeys.tsx delete mode 100644 apps/studio/pages/project/[ref]/settings/api-keys.tsx diff --git a/apps/studio/components/interfaces/APIKeys/LegacyAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/LegacyAPIKeys.tsx deleted file mode 100644 index 8cac94b5af4..00000000000 --- a/apps/studio/components/interfaces/APIKeys/LegacyAPIKeys.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import DisplayApiSettings from 'components/ui/ProjectSettings/DisplayApiSettings' - -export function LegacyAPIKeys() { - return ( -
- -
- ) - return -} diff --git a/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx index e187ac8b8cd..99627770315 100644 --- a/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx +++ b/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx @@ -5,7 +5,6 @@ import CopyButton from 'components/ui/CopyButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import { useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' -import { Link } from 'lucide-react' import { useMemo } from 'react' import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui' // to add in later with follow up PR diff --git a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts index 0033abcd1fd..caf6c85b010 100644 --- a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts +++ b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts @@ -22,7 +22,7 @@ interface ApiKeysVisibilityState { export function useApiKeysVisibility(): ApiKeysVisibilityState { const { ref: projectRef } = useParams() const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys') - const isInRollout = useFlag('basicApiKeys') + const isBasicApiKeysEnabled = useFlag('basicApiKeys') const { data: apiKeysData, isLoading } = useAPIKeysQuery({ projectRef, @@ -39,12 +39,13 @@ export function useApiKeysVisibility(): ApiKeysVisibilityState { const hasApiKeys = publishableApiKeys.length > 0 // Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet - const canInitApiKeys = hasApiKeys ? false : isInRollout && canReadAPIKeys && !isLoading + const canInitApiKeys = isBasicApiKeysEnabled && canReadAPIKeys && !isLoading && !hasApiKeys - const shouldDisableUI = hasApiKeys ? false : !isInRollout + // Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet + const shouldDisableUI = !isBasicApiKeysEnabled || !hasApiKeys return { - isInRollout, + isInRollout: isBasicApiKeysEnabled, hasApiKeys, isLoading, canReadAPIKeys, diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx index ddbf124d097..574556e5097 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx @@ -7,7 +7,6 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' -import { useFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM } from 'lib/constants' import ProjectLayout from '../ProjectLayout/ProjectLayout' import { generateSettingsMenu } from './SettingsMenu.utils' @@ -46,14 +45,11 @@ const SettingsLayout = ({ title, children }: PropsWithChildren { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx index db92f12ed28..02161bf0e4a 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx +++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react' import { PermissionAction } from '@supabase/shared-types/out/constants' import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' import { useParams } from 'common' @@ -8,9 +7,10 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { AlertCircle, Loader2 } from 'lucide-react' -import { Input } from 'ui' +import { useMemo } from 'react' import { toast } from 'sonner' -import { useLastUsedAPIKeysLogQuery, getLastUsedAPIKeys } from './DisplayApiSettings.utils' +import { Input } from 'ui' +import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils' const DisplayApiSettings = ({ legacy, @@ -21,7 +21,7 @@ const DisplayApiSettings = ({ }) => { const { ref: projectRef } = useParams() - const newApiKeysFlag = useFlag('newApiKeys') + const newApiKeysInRollOut = useFlag('basicApiKeys') const { data: settings, @@ -177,7 +177,7 @@ const DisplayApiSettings = ({ )) )} {showNotice ? ( - newApiKeysFlag ? ( + newApiKeysInRollOut ? ( { - // Redirect to the appropriate page - router.replace(`/project/${ref}/settings/api-keys/${targetView}`) - }, []) - - return null -} diff --git a/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx b/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx index 45dc2517158..723cc7fb139 100644 --- a/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx +++ b/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx @@ -1,11 +1,11 @@ -import { LegacyAPIKeys } from 'components/interfaces/APIKeys/LegacyAPIKeys' import ApiKeysLayout from 'components/layouts/APIKeys/APIKeysLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' +import { DisplayApiSettings } from 'components/ui/ProjectSettings' import type { NextPageWithLayout } from 'types' const ApiKeysLegacyPage: NextPageWithLayout = () => { - return + return } ApiKeysLegacyPage.getLayout = (page) => ( From 80e8c14fa71df3fb39f436a73ef9be0f24a4f4b0 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 4 Jun 2025 17:26:27 +1000 Subject: [PATCH 02/19] fix home counts (#36157) * fix home counts * minor fix * Lint * Add comment --------- Co-authored-by: Joshen Lim --- apps/studio/pages/project/[ref]/index.tsx | 49 ++++++++++++++--------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 4cb8529816b..8622e076030 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs' +import Link from 'next/link' import { useEffect, useRef } from 'react' import { useParams } from 'common' @@ -39,7 +40,7 @@ const Home: NextPageWithLayout = () => { const project = useSelectedProject() const isOrioleDb = useIsOrioleDb() const snap = useAppStateSnapshot() - const { enableBranching } = useParams() + const { ref, enableBranching } = useParams() const hasShownEnableBranchingModalRef = useRef(false) const isPaused = project?.status === PROJECT_STATUS.INACTIVE @@ -70,9 +71,10 @@ const Home: NextPageWithLayout = () => { projectRef: project?.ref, }) - const tablesCount = tablesData?.length ?? 0 - const functionsCount = functionsData?.length ?? 0 - const replicasCount = (replicasData?.length ?? 1) - 1 + const tablesCount = Math.max(0, tablesData?.length ?? 0) + const functionsCount = Math.max(0, functionsData?.length ?? 0) + // [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally + const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1) return (
@@ -108,44 +110,53 @@ const Home: NextPageWithLayout = () => {
{project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
-
-
+
+ Tables -
- + +

{isLoadingTables ? ( ) : ( tablesCount )} - +

-
-
+
+ Functions -
- + +

{isLoadingFunctions ? ( ) : ( functionsCount )} - +

{IS_PLATFORM && ( -
-
+
+ Replicas -
- + +

{isLoadingReplicas ? ( ) : ( replicasCount )} - +

)}
From a8336efa2aceee529093e54cdd37044b65cb5c59 Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:01:42 +0300 Subject: [PATCH 03/19] Fix broken link to replication guide (#36069) From 80b4d9d18ffcc9991ecbc333abf736b627184c4a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 4 Jun 2025 18:30:56 +0800 Subject: [PATCH 04/19] Adjust open new tab SQL Editor CTA (#36163) ADjust open new tab SQL Editor CTA --- .../SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx index 8c97f4303bf..895eaee2bb2 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx @@ -1,6 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { IS_PLATFORM } from 'common' import { useParams } from 'common/hooks/useParams' +import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useSQLSnippetFolderContentsQuery } from 'data/content/sql-folder-contents-query' import { Snippet } from 'data/content/sql-folders-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -82,6 +83,7 @@ export const SQLEditorTreeViewItem = ({ const { profile } = useProfile() const { className, onClick } = getNodeProps() const snapV2 = useSqlEditorV2StateSnapshot() + const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled() const isOwner = profile?.id === element?.metadata.owner_id const isSharedSnippet = element.metadata.visibility === 'project' @@ -265,7 +267,7 @@ export const SQLEditorTreeViewItem = ({ > From 2df44bc0731ee333e33fc7a3feda4f2b8b4ca780 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 4 Jun 2025 18:43:33 +0800 Subject: [PATCH 05/19] Fix cmd i not closing AI assistant (#36164) --- apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index da348ff170e..7f1b25d8ed5 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -125,7 +125,7 @@ const ProjectLayout = forwardRef { const handler = (e: KeyboardEvent) => { if (e.metaKey && e.key === 'i' && !e.altKey && !e.shiftKey) { - aiSnap.openAssistant() + aiSnap.toggleAssistant() e.preventDefault() e.stopPropagation() } From 450803a2a878a96ab7fd2ded4800c57e459ff99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 4 Jun 2025 15:34:04 +0200 Subject: [PATCH 06/19] feat: bump auth-js to `2.71.0-rc.2` (#36170) * feat: experimental testing for split auth storage * fix: rm config changes --- package.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- pnpm-workspace.yaml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 69af2e1c8ed..7dcb1cb1a16 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ }, "pnpm": { "overrides": { - "@supabase/supabase-js>@supabase/auth-js": "2.69.0-rc.3", + "@supabase/supabase-js>@supabase/auth-js": "2.71.0-rc.2", "vinxi>esbuild": "0.25.2", "@tanstack/directive-functions-plugin>vite": "6.2.6", "@tanstack/react-start-plugin>vite": "6.2.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4097bfed3d7..f6b6a393c05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@supabase/auth-js': - specifier: 2.69.0-rc.3 - version: 2.69.0-rc.3 + specifier: 2.71.0-rc.2 + version: 2.71.0-rc.2 '@supabase/realtime-js': specifier: ^2.11.3 version: 2.11.3 @@ -38,7 +38,7 @@ catalogs: version: 1.12.0 overrides: - '@supabase/supabase-js>@supabase/auth-js': 2.69.0-rc.3 + '@supabase/supabase-js>@supabase/auth-js': 2.71.0-rc.2 vinxi>esbuild: 0.25.2 '@tanstack/directive-functions-plugin>vite': 6.2.6 '@tanstack/react-start-plugin>vite': 6.2.6 @@ -712,7 +712,7 @@ importers: version: 5.5.0 '@supabase/auth-js': specifier: 'catalog:' - version: 2.69.0-rc.3 + version: 2.71.0-rc.2 '@supabase/pg-meta': specifier: workspace:* version: link:../../packages/pg-meta @@ -1658,7 +1658,7 @@ importers: dependencies: '@supabase/auth-js': specifier: 'catalog:' - version: 2.69.0-rc.3 + version: 2.71.0-rc.2 '@supabase/supabase-js': specifier: 'catalog:' version: 2.49.3 @@ -8203,8 +8203,8 @@ packages: resolution: {integrity: sha512-lkfjyAd34aeMpTKKcEVfy8IUyEsjuAT3t9EXr5yZDtdIUncnZpedl/xLV16Dkd4z+fQwixScsCCDxSMNtBOgpQ==} engines: {node: '>=12.16'} - '@supabase/auth-js@2.69.0-rc.3': - resolution: {integrity: sha512-Be/5DrT+hI1zTnB1lD8I46aHIXxlA4h84jUhzcd0NcC53M+rw0SMXBNwCWY3reSjOgRX/ep2TtklBaBTyOg4Mg==} + '@supabase/auth-js@2.71.0-rc.2': + resolution: {integrity: sha512-cG44jEZnZGu3FC8QHQF1vojXiBXgV1pCAARLwSrvd8PCNjuppNMzhUzSPXcMPSyvMo0OLwIYyQRxqPOfRKmcJQ==} '@supabase/functions-js@2.4.4': resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==} @@ -25884,7 +25884,7 @@ snapshots: '@stripe/stripe-js@5.5.0': {} - '@supabase/auth-js@2.69.0-rc.3': + '@supabase/auth-js@2.71.0-rc.2': dependencies: '@supabase/node-fetch': 2.6.15 @@ -26021,7 +26021,7 @@ snapshots: '@supabase/supabase-js@2.49.3': dependencies: - '@supabase/auth-js': 2.69.0-rc.3 + '@supabase/auth-js': 2.71.0-rc.2 '@supabase/functions-js': 2.4.4 '@supabase/node-fetch': 2.6.15 '@supabase/postgrest-js': 1.19.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f2ed1040ede..4e42cedf640 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,7 @@ packages: catalog: '@types/node': ^20.0.0 - '@supabase/auth-js': 2.69.0-rc.3 + '@supabase/auth-js': 2.71.0-rc.2 '@supabase/supabase-js': ^2.47.14 '@supabase/realtime-js': ^2.11.3 next: 14.2.26 From 10b5402d1c7813bbdc9c99647955ee6bab7096bd Mon Sep 17 00:00:00 2001 From: Prashant Sridharan <914007+CoolAssPuppy@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:53:58 +0100 Subject: [PATCH 07/19] Added new blog post and modified some docs for SEO (#36171) * Added new blog post and modified some docs for SEO * ci: Autofix updates from GitHub workflow * Fixed some linter errors * Update apps/docs/content/guides/database/connecting-to-postgres.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update apps/docs/content/guides/database/connecting-to-postgres.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../database/connecting-to-postgres.mdx | 46 ++++++++++- apps/docs/content/guides/database/pgadmin.mdx | 12 +++ ...5-05-29-building-on-open-table-formats.mdx | 74 ++++++++++++++++++ .../building-on-open-table-formats.png | Bin 0 -> 44029 bytes apps/www/public/rss.xml | 7 ++ 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 apps/www/_blog/2025-05-29-building-on-open-table-formats.mdx create mode 100644 apps/www/public/images/blog/2025-05-29-building-on-open-table-formats/building-on-open-table-formats.png diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index 8d592378e24..e1379054e3b 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -1,10 +1,10 @@ --- title: 'Connect to your database' description: 'Connect to Postgres from your frontend, backend, or serverless environment' -subtitle: 'Connect to Postgres from your frontend, backend, or serverless environment' +subtitle: 'Supabase provides multiple methods to connect to your Postgres database, whether you’re working on the frontend, backend, or utilizing serverless functions.' --- -## Quick summary +## How to connect to your Postgres databases How you connect to your database depends on where you're connecting from: @@ -52,7 +52,7 @@ The Data APIs allow you to interact with your database using REST or GraphQL req - [REST](/docs/guides/api) - [GraphQL](/docs/guides/graphql/api) -For convenience, you can also use the Supabase client libraries, which wrap the Data APIs with a developer-friendly interface and automatically handle authentication: +For convenience, you can also use the [Supabase client libraries](/docs/reference), which wrap the Data APIs with a developer-friendly interface and automatically handle authentication: - [JavaScript](/docs/reference/javascript) - [Flutter](/docs/reference/dart) @@ -167,3 +167,43 @@ You can obtain your connection info and Server root certificate from your applic ## Resources - [Connection management](/docs/guides/database/connection-management) +- [Connecting with psql](/docs/guides/database/psql) +- [Importing data into Supabase](/docs/guides/database/import-data) + +## Troubleshooting and Postgres connection string FAQs + +Below are answers to common challenges and queries. + +### What is a “connection refused” error? + +A “Connection refused” error typically means your database isn’t reachable. Ensure your Supabase project is running, confirm your database’s connection string, check firewall settings, and validate network permissions. + +### What is the “FATAL: Password authentication failed” error? + +This error occurs when your credentials are incorrect. Double-check your username and password from the Supabase dashboard. If the problem persists, reset your database password from the project settings. + +### How do you connect using IPv4? + +Supabase’s default direct connection supports IPv6 only. To connect over IPv4, consider using the Supavisor session or transaction modes, or a connection pooler (shared or dedicated), which support both IPv4 and IPv6. + +### How do you choose a connection method? + +- Direct connection: Persistent backend services (IPv6 only) +- Supavisor session mode: Persistent backend needing IPv4 +- Supavisor transaction mode: Serverless functions +- Shared pooler: General-purpose connections with IPv4 and IPv6 +- Dedicated pooler: High-performance apps requiring dedicated resources (paid tier) + +### Where is the Postgres connection string in Supabase? + +Your connection string is located in the Supabase Dashboard. Click the "Connect" button at the top of the page. + +### Can you use `psql` with a Supabase database? + +Yes. Use the following command structure, replacing `your_connection_string` with the string from your Supabase dashboard: + +``` +psql "your_connection_string" +``` + +Ensure you have `psql` installed locally before running this command. diff --git a/apps/docs/content/guides/database/pgadmin.mdx b/apps/docs/content/guides/database/pgadmin.mdx index 7608ad02112..a7917fb205b 100644 --- a/apps/docs/content/guides/database/pgadmin.mdx +++ b/apps/docs/content/guides/database/pgadmin.mdx @@ -5,8 +5,12 @@ breadcrumb: 'GUI Quickstarts' hideToc: true --- +## What is pgAdmin? + [`pgAdmin`](https://www.pgadmin.org/) is a GUI tool for managing Postgres databases. You can use it to connect to your database via SSL. +## Connecting pgAdmin with your Postgres database + @@ -83,3 +87,11 @@ hideToc: true + +## Why connect to pgAdmin + +Connecting your Postgres instance to `pgAdmin` gives you a free, cross-platform GUI that makes tasks such as browsing objects, writing queries with autocomplete, running backups, and monitoring performance much faster and safer than using `psql` alone. + +It acts as a single control panel where you can manage multiple servers, inspect locks and slow queries in real time, and perform maintenance operations with a click. + +For scripted migrations or ultra-light remote work you’ll still lean on plain SQL or CLI tools, but most teams find `pgAdmin` invaluable for exploration and routine administration. diff --git a/apps/www/_blog/2025-05-29-building-on-open-table-formats.mdx b/apps/www/_blog/2025-05-29-building-on-open-table-formats.mdx new file mode 100644 index 00000000000..a19d596948a --- /dev/null +++ b/apps/www/_blog/2025-05-29-building-on-open-table-formats.mdx @@ -0,0 +1,74 @@ +--- +title: 'Building on open table formats' +description: Open table formats like Apache Iceberg, Delta Lake, and Apache Hudi are transforming how developers manage large-scale data on object storage systems. +author: prashant +image: 2025-05-29-building-on-open-table-formats/building-on-open-table-formats.png +thumb: 2025-05-29-building-on-open-table-formats/building-on-open-table-formats.png +toc_depth: 3 +categories: + - product + - company +tags: + - postgres + - iceberg + - otel +date: '2025-05-29' +--- + +Open table formats are specifications that define how to store and manage large datasets in a structured manner on distributed storage systems. They provide a layer of abstraction over raw data files, enabling features such as ACID transactions, schema evolution, and time travel. This abstraction allows multiple processing engines to interact with the data consistently and reliably. + +The primary open table formats in use today are Apache Iceberg, Delta Lake, and Apache Hudi. Each offers unique capabilities tailored to specific use cases: + +- [Apache Iceberg](https://iceberg.apache.org) was initiated at Netflix and open sourced. It is designed for high-performance analytics and provides full support for schema and partition evolution, hidden partitioning, and time travel. +- [Delta Lake](https://delta.io) is developed by Databricks and emphasizes ACID transactions. It’s tightly integrated with the Spark ecosystem. +- [Apache Hudi](https://hudi.apache.org) is developed by Uber and is optimized for streaming data and real-time ingestion. It supports incremental data processing and efficient upsets. + +These three open table formats emerged to solve distinct challenges. Iceberg shines in analytics scenarios where you need consistency, flexibility, and compatibility with many data engines. Delta Lake is best when you are in a Spark environment (e.g., Databricks.) And Hudi is good for streaming-centric environments and database change data capture (CDC). + +## The rise of Apache Iceberg + +Iceberg was designed to solve the challenges of managing large, analytical databases stored in object storage systems like [Supabase Storage](/storage), Amazon S3, Google Cloud Storage, and Azure Blob Storage. Iceberg brings database-like capabilities to distributed file systems, enabling reliable, consistent access to data that would otherwise be locked in raw files. + +Open table formats define how data and metadata are organized. They sit on top of [Parquet](https://parquet.apache.org) files (or other files with data) and they make large datasets queryable across multiple engines without sacrificing consistency or performance. Iceberg’s design allows multiple systems to write to and query the same dataset safely. This makes it an essential component for modern data platforms that need to scale. + +Iceberg offers several key features: + +- **ACID transactions**. Multiple users or processes can read and write data concurrently without risking corruption or inconsistencies. This is critical when datasets are shared across teams and tools. +- **Schema evolution**. In real-world systems, schemas change over time. Iceberg allows developers to add or remove columns, rename fields, and evolve the schema without rewriting existing data. Iceberg’s metadata layer tracks these changes, so queries remain consistent and backward-compatible. +- **Time travel**. Iceberg tracks snapshot of the table at different points in time. This enables developers to query the dataset as it existed at a specific version, enabling auditing, debugging, and reproducibility of data scenarios. +- **Interoperability.** Multiple compute engines can read and write data to the same Iceberg table. What used to be a data warehouse world of incompatible silos has quickly become an interoperable world where companies need to compete on features and dependability instead of data lock-in. + +## Object storage is the key + +Iceberg addresses several trends prevalent in our industry today, turning raw object storage into a usable, consistent, and vendor-neutral data layer: + +- **Businesses now routinely store petabytes of data, driven by logs, events, IoT sensors, user interactions, and AI-generated outputs**. The retention of this data, for years or decades, is now standard practice for compliance, analytics, or AI model training. Traditional data lake architectures cannot scale on either operations or price under this load. Iceberg comes at exactly the right time, enabling companies to offload their data to much lower cost object storage systems, while still being able to query and use the data as if it were in a more traditional data environment. +- **Development stacks now routinely separate storage and compute**. Object storage has become the default data substrate because of its low cost, durability, and scalability. At the same time, development teams want to mix and match compute engines based on the task, whether it’s Spark for batch, Flink for streaming, DuckDB for analytics, and so on. Iceberg’s support for object storage and its support for concurrent access and schema evolution makes it ideal for this type of architecture. +- **Telemetry data (logs, metrics, traces, and events) has exploded in volume**, driven by modern distributed systems, microservices, and AI workloads. OpenTelemetry has emerged as the de facto standard for collecting and transmitting this data, but raw telemetry on its own is not enough. Once generated, it needs a scalable, queryable, and consistent backend for analysis and long-term retention. Iceberg provides that backend for telemetry, turning ephemeral streams into durable, structured datasets that can be queried alongside other analytical data. This bridges the gap between observability pipelines and data lakes. +- **AI has changed how data is used**. Models are no longer trained once and deployed indefinitely. They are retrained frequently, sometimes daily, using new data. This requires a reproducible, versioned data pipeline. Teams need to know exactly what data was used to train a model and be able to recreate that state months later. Iceberg’s time travel and snapshot isolation features make this possible. + +Moreover, teams do not want to be locked in to proprietary systems. Data is meant to be free, not stored in formats that provide artificial advantages and perverse incentives to companies. The idea of the lakehouse (combining the scalability and cost efficiency of data lakes with the transactional guarantees of data warehouses) remains popular, while Iceberg makes it feasible for data to be free as companies compete on compute engines. + +## Amazon S3 and Iceberg + +It takes two to tango. The combination of Iceberg and Amazon S3 is a potent alternative to traditional proprietary data lakes and data warehouses. It’s thanks to the significant evolution of Amazon S3 that much of Iceberg’s promise has come to fruition: + +- [**S3 Express**](https://aws.amazon.com/s3/storage-classes/express-one-zone/) is a new storage class designed for high-performance workloads. S3 Express delivers up to 10x lower latency and 10x higher throughput than standard S3, while reducing costs by up to 85 percent. This makes it viable for interactive, near-real-time applications. +- [**Conditional writes**](https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/) offer native support for conditional operations in S3, allowing developers to build data pipelines that safely update data only when it has not been modified by another process. This eliminates the need for complex external systems to manage transactions. +- [**S3 Tables**](https://aws.amazon.com/blogs/aws/new-amazon-s3-tables-storage-optimized-for-analytics-workloads/) offers native support for Apache Iceberg within S3. S3 Tables are optimized for analytics workloads, offering up to 3X faster query performance and up to 10X higher transactions per second compared to self-managed tables. + +## S3 + Iceberg = ETL + +The ETL industry was built around a fundamental problem: moving data from one system to another, transforming it along the way to make it usable for different purposes. For decades, this meant extracting data from operational databases, cleaning and reshaping it through a series of batch processes, and loading it into a data warehouse for analysis. This pipeline was slow, fragile, and expensive. However, it was necessary, because storage and compute were tightly coupled, and operational systems could not support large-scale analytics directly. + +What’s happening now with S3 + Iceberg is a paradigm shift. Open table formats like Iceberg turn object storage into a queryable, versioned, and structured data layer. At the same time, innovations like S3 Express, Conditional Writes, and S3 Tables make it possible to write directly into object storage at scale with transactional guarantees and low latency. + +This means the traditional ETL model—extract, transform, and load—starts to break down. Instead of lifting data out of one system, transforming it, and depositing it into another, teams can write once into Iceberg tables on S3 and access the same dataset across multiple engines. Transformation can happen in place, not as a separate pipeline. The data is already where it needs to be. + +## Supabase and the data cloud + +Supabase has always been more than just a Postgres host. We are the platform for building modern applications. Supabase starts with [a Postgres database](/database) and includes products for [Authentication](/auth), [Storage](/storage), [Edge Functions](/edge-functions), [Realtime](/realtime), [Vectors](/modules/vector), and more. As the industry moves toward open table formats like Iceberg and S3 as the default storage layer, Supabase’s role evolves with it to be less about a database, and more about data. + +Postgres remains the core of the Supabase platform: the system of record for operational data. [As we’ve written](/blog/open-data-standards-postgres-otel-iceberg), we are also building first-class support for OpenTelemetry across our services, enabling developers to collect observability data (logs, metrics, and traces) without managing additional infrastructure. And with [Supabase ETL](https://github.com/supabase/etl), we will provide a lightweight, Postgres-native way to move data into S3 and more, where it can be queried at scale using Iceberg and your choice of analytics engines. + +Our goal is to make Supabase the developer’s data cloud: Postgres for transactions, OpenTelemetry for observability, and Iceberg for analytics, all connected by simple, open tools. To do so, we remain focused on what developers need: a backend that starts simple, grows with your product, and keeps your data open and portable at every stage. diff --git a/apps/www/public/images/blog/2025-05-29-building-on-open-table-formats/building-on-open-table-formats.png b/apps/www/public/images/blog/2025-05-29-building-on-open-table-formats/building-on-open-table-formats.png new file mode 100644 index 0000000000000000000000000000000000000000..77d50c3918ef061fcf6040e2b8114640abbc43c8 GIT binary patch literal 44029 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 K19UU*_`d;%6T3_R literal 0 HcmV?d00001 diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index a90e31bf251..921b4431720 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -8,6 +8,13 @@ Mon, 26 May 2025 00:00:00 -0700 + https://supabase.com/blog/building-on-open-table-formats + Building on open table formats + https://supabase.com/blog/building-on-open-table-formats + Open table formats like Apache Iceberg, Delta Lake, and Apache Hudi are transforming how developers manage large-scale data on object storage systems. + Fri, 29 May 2025 00:00:00 -0700 + + https://supabase.com/blog/open-data-standards-postgres-otel-iceberg Open Data Standards: Postgres, OTel, and Iceberg https://supabase.com/blog/open-data-standards-postgres-otel-iceberg From 8091ef176be236765a03b749bc57f80e1064b6bc Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 4 Jun 2025 15:54:52 +0200 Subject: [PATCH 08/19] careers page ssr (#36166) * fetch careers page server-side * fetch careers page server-side --- apps/www/pages/careers.tsx | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/apps/www/pages/careers.tsx b/apps/www/pages/careers.tsx index 23b76b5704d..ddd928c5323 100644 --- a/apps/www/pages/careers.tsx +++ b/apps/www/pages/careers.tsx @@ -1,4 +1,4 @@ -import { NextPage } from 'next' +import { GetServerSideProps, NextPage } from 'next' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' @@ -16,7 +16,10 @@ import SectionContainer from '~/components/Layouts/SectionContainer' import { groupJobsByTeam, filterGenericJob, JobItemProps, PLACEHOLDER_JOB_ID } from '~/lib/careers' import career from '~/data/career.json' -export async function getStaticProps() { +export const getServerSideProps: GetServerSideProps = (async ({ res }) => { + // refresh every 5 minutes + res.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=300') + const job_res = await fetch('https://api.ashbyhq.com/posting-api/job-board/supabase') const job_data = await job_res.json() @@ -65,23 +68,9 @@ export async function getStaticProps() { } ) - if (!job_data && !contributors) { - return { - props: { - notFound: true, - }, - } - } - - return { - props: { - jobs, - placeholderJob: placeholderJob ?? null, - contributors: contributors, - }, - revalidate: 60 * 5, // 5 minutes, - } -} + // Pass data to the page via props + return { props: { jobs, placeholderJob: placeholderJob ?? null, contributors } } +}) satisfies GetServerSideProps interface CareersPageProps { jobs: Record From 2cd0f3c84d9d85d0b8074dae6b0d7641179472db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 4 Jun 2025 23:37:49 +0800 Subject: [PATCH 09/19] fix: possibly undefined breakdown in plan update modal (#36155) --- .../Subscription/SubscriptionPlanUpdateDialog.tsx | 9 +++++---- .../organization-billing-subscription-preview.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 353e15b4525..0bee353c27b 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -312,7 +312,8 @@ export const SubscriptionPlanUpdateDialog = ({ subscriptionPreview?.breakdown?.filter( (item) => item.description?.toLowerCase().includes('compute') && - item.breakdown?.length > 0 + item.breakdown && + item.breakdown.length > 0 ) || [] const computeCreditsItem = @@ -325,11 +326,11 @@ export const SubscriptionPlanUpdateDialog = ({ ) const allProjects = computeItems.flatMap((item) => - item.breakdown.map((project) => ({ + (item.breakdown || []).map((project) => ({ ...project, computeType: item.description, computeCosts: Math.round( - item.total_price / item.breakdown.length + item.total_price / item.breakdown!.length ), })) ) @@ -412,7 +413,7 @@ export const SubscriptionPlanUpdateDialog = ({
{item.description ?? 'Unknown'} - {item.breakdown.length > 0 && ( + {item.breakdown && item.breakdown.length > 0 && (

Projects using {item.description}:

    diff --git a/apps/studio/data/organizations/organization-billing-subscription-preview.ts b/apps/studio/data/organizations/organization-billing-subscription-preview.ts index 75787a47e67..beb079b520e 100644 --- a/apps/studio/data/organizations/organization-billing-subscription-preview.ts +++ b/apps/studio/data/organizations/organization-billing-subscription-preview.ts @@ -16,7 +16,7 @@ export type OrganizationBillingSubscriptionPreviewResponse = { unit_price_desc?: string quantity?: number total_price: number - breakdown: { + breakdown?: { project_name: string project_ref: string usage: number From 2181fed2cdb9d150462e451ec33d5fbdee32e6ec Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:17:35 +0300 Subject: [PATCH 10/19] Docs: clarify how to enable column level privileges (#36161) --- .../guides/database/postgres/column-level-security.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/docs/content/guides/database/postgres/column-level-security.mdx b/apps/docs/content/guides/database/postgres/column-level-security.mdx index bb8b90922af..9c4e32abef6 100644 --- a/apps/docs/content/guides/database/postgres/column-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/column-level-security.mdx @@ -82,6 +82,12 @@ This time, we are revoking the column-level `UPDATE` privilege of the `title` co ## Manage column privileges in the Dashboard + + +Column-level privileges are a powerful tool, but they're also quite advanced and in many cases, not the best fit for common access control needs. For that reason, we've intentionally moved the UI for this feature under the Feature Preview section in the dashboard. + + + You can view and edit the privileges in the [Supabase Studio](https://supabase.com/dashboard/project/_/database/column-privileges). ![Column level privileges](/docs/img/guides/privileges/column-level-privileges-2.png) From cd573086b11dd045c85363fb229274a79012edee Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:36:26 +0200 Subject: [PATCH 11/19] Log Explorer improvements (#36140) * make create button go directly to log explorer if warehouse is disabled * fix limit clause in uppercase not caught by warnings * make warning tooltip improve spacing * fix date not updated on log explorer --- .../Settings/Logs/LogsQueryPanel.tsx | 52 +++++++++---------- .../layouts/LogsLayout/LogsSidebarMenuV2.tsx | 48 ++++++++++------- apps/studio/hooks/analytics/useLogsQuery.tsx | 18 ++++++- .../project/[ref]/logs/explorer/index.tsx | 2 +- 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx index a09f04964ad..d6bebe2d1db 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx @@ -1,7 +1,6 @@ import Link from 'next/link' import { ReactNode, useState } from 'react' -import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' import { IS_PLATFORM } from 'common' import Table from 'components/to-be-cleaned/Table' import dayjs from 'dayjs' @@ -234,31 +233,32 @@ const LogsQueryPanel = ({ /> )} -
    -
    0 ? 'opacity-100' : 'invisible h-0 w-0 opacity-0' - }`} - > - - - - {warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'} - - - {warnings.map((warning, index) => ( -

    - {warning.text}{' '} - {warning.link && ( - {warning.linkText || 'View'} - )} -

    - ))} -
    -
    -
    -
    +
    0 ? 'opacity-100' : 'invisible h-0 w-0 opacity-0' + }`} + > + + + + {warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'} + + + {warnings.map((warning, index) => ( +

    + {warning.text}{' '} + {warning.link && ( + {warning.linkText || 'View'} + )} +

    + ))} +
    +
    +
    {dataSource === 'logs' && ( diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index f473200c1e1..993484f7197 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -218,22 +218,24 @@ export function LogsSidebarMenuV2() { onChange={(e) => setSearchText(e.target.value)} > - - -
    - {MIGRATION_PAGES.sort((a, b) => a.name.localeCompare(b.name)).map((guide) => { - return ( -
  • - - - -
  • - ) - })} + {MIGRATION_PAGES.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map( + (guide) => { + return ( +
  • + + + +
  • + ) + } + )}
diff --git a/apps/docs/components/ApiSchema.tsx b/apps/docs/components/ApiSchema.tsx index 952d60951f6..020bb8ae6a0 100644 --- a/apps/docs/components/ApiSchema.tsx +++ b/apps/docs/components/ApiSchema.tsx @@ -6,11 +6,11 @@ import { Tabs, TabPanel } from '~/features/ui/Tabs' type IParamProps = any const ApiSchema = ({ schema, id }: IParamProps) => { - let example: string + let example: string | undefined try { example = sample(schema, { skipReadOnly: true, quiet: true }) } catch { - // ignore + example = undefined } return ( { { - setFile({ file: e.target.files[0] }) + setFile({ file: e.target.files?.[0] || null }) }} />
@@ -156,7 +156,7 @@ const AppleSecretGenerator = () => { keyID, teamID, serviceID, - file.file + file.file! ) setKeyID(kid) setSecretKey(jwt) diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx index d41b2b0e941..5eaee5cb0ac 100644 --- a/apps/docs/components/Breadcrumbs.tsx +++ b/apps/docs/components/Breadcrumbs.tsx @@ -181,7 +181,7 @@ function useBreadcrumbs() { return findMenuItemByUrl(menu, pathname, []) } -function findMenuItemByUrl(menu, targetUrl, parents = []) { +function findMenuItemByUrl(menu: any, targetUrl: string, parents: any[] = []) { // If the menu has items, recursively search through them if (menu.items) { for (let item of menu.items) { diff --git a/apps/docs/components/Extensions/Extensions.tsx b/apps/docs/components/Extensions/Extensions.tsx index b2228bb9d74..c9a3c201d5e 100644 --- a/apps/docs/components/Extensions/Extensions.tsx +++ b/apps/docs/components/Extensions/Extensions.tsx @@ -24,7 +24,7 @@ function getLinkTarget(link: string): LinkTarget { } function getUniqueTags(json: Extension[]): string[] { - const tags = [] + const tags: string[] = [] for (const item of json) { if (item.tags) { tags.push(...item.tags) diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index e2aa841aa43..e96f1907158 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -96,6 +96,8 @@ function Feedback({ className }: { className?: string }) { const showNo = unanswered || isNo async function sendFeedbackVote(response: Response) { + if (!supabase) return + const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname, diff --git a/apps/docs/components/JwtGenerator/JwtGenerator.tsx b/apps/docs/components/JwtGenerator/JwtGenerator.tsx index f7e5132550a..e004b3c20ec 100644 --- a/apps/docs/components/JwtGenerator/JwtGenerator.tsx +++ b/apps/docs/components/JwtGenerator/JwtGenerator.tsx @@ -59,7 +59,7 @@ export default function JwtGenerator() { const [jwtSecret, setJwtSecret] = useState(secret) const [token, setToken] = useState(anonToken) const [signedToken, setSignedToken] = useState('') - const [err, setErr] = useState(null) + const [err, setErr] = useState('') const handleKeySelection = (e: ChangeEvent) => { const val = e.target.value @@ -71,7 +71,7 @@ export default function JwtGenerator() { try { const newTok = JSON.parse(e.target.value) setToken(newTok) - setErr(null) + setErr('') } catch (err) { const errMessage = !!err && typeof err === 'object' && 'message' in err && typeof err.message === 'string' diff --git a/apps/docs/components/MultiSelect.client.tsx b/apps/docs/components/MultiSelect.client.tsx deleted file mode 100644 index a56994cab30..00000000000 --- a/apps/docs/components/MultiSelect.client.tsx +++ /dev/null @@ -1,196 +0,0 @@ -'use client' - -import { ChevronsUpDown } from 'lucide-react' -import { - createContext, - forwardRef, - useCallback, - useContext, - useId, - useState, - PropsWithChildren, - useRef, - useEffect, -} from 'react' - -import { - cn, - Badge, - Checkbox_Shadcn_ as Checkbox, - Popover_Shadcn_ as Popover, - PopoverContent_Shadcn_ as PopoverContent, - PopoverTrigger_Shadcn_ as PopoverTrigger, -} from 'ui' - -interface Item { - value: string - label: string -} - -const MultiSelectContext = createContext<{ - selected: Item[] - handleSelect: (item: Item) => void - setOpen: (open: boolean) => void -}>({ - selected: [], - handleSelect: () => {}, - setOpen: () => {}, -}) - -function MultiSelectProvider({ - selected, - onSelectedChange, - setOpen, - children, -}: PropsWithChildren<{ - selected: Item[] - onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void - setOpen: (open: boolean) => void -}>) { - const handleSelect = useCallback( - (item: Item) => { - onSelectedChange((current: Item[]) => { - const isSelected = current.some((currItem) => currItem.value === item.value) - if (isSelected) { - return current.filter((currItem) => currItem.value !== item.value) - } else { - return [...current, item] - } - }) - }, - [onSelectedChange] - ) - - return ( - - {children} - - ) -} - -function useMultiSelect() { - const context = useContext(MultiSelectContext) - if (!context) { - throw new Error('useMultiSelect must be used within a MultiSelectProvider') - } - return context -} - -export function MultiSelect({ - open: _controlledOpen, - setOpen: _setControlledOpen, - selected, - onSelectedChange, - children, - ...props -}: PropsWithChildren< - { - open?: boolean - setOpen?: React.Dispatch> - selected?: Item[] - onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void - } & React.ComponentProps ->) { - const [_internalOpen, _setInternalOpen] = useState(false) - const open = _controlledOpen ?? _internalOpen - const setOpen = _setControlledOpen ?? _setInternalOpen - - return ( - - - {children} - - - ) -} - -const Trigger = forwardRef< - HTMLButtonElement, - { label?: string; className?: string } & React.ComponentProps ->(({ label, className, ...props }, ref) => { - const { selected } = useMultiSelect() - - const [measuredWidth, setMeasuredWidth] = useState(0) - const measuredRef = (node: HTMLElement) => { - if (node) { - setMeasuredWidth(node.getBoundingClientRect().width) - } - } - - const badgeWidth = 100 // Approximate width of a badge in pixels - const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth)) - const visibleBadges = selected.slice(0, maxBadges) - const extraBadgesCount = Math.max(0, selected.length - maxBadges) - - const badgeClasses = 'bg-200' - - return ( - - - - ) -}) -Trigger.displayName = 'Trigger' -MultiSelect.Trigger = Trigger - -const Content = forwardRef< - HTMLDivElement, - PropsWithChildren<{ className?: string } & React.ComponentProps> ->(({ children, className, ...props }, ref) => { - return ( - - {children} - - ) -}) -Content.displayName = 'Content' -MultiSelect.Content = Content - -export function Item({ item, className }: { item: Item; className?: string }) { - const id = useId() - const { selected, handleSelect } = useMultiSelect() - - return ( -
- sel.value === item.value)} - onCheckedChange={() => handleSelect(item)} - /> - -
- ) -} -MultiSelect.Item = Item diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index f34c38f6830..5f57d3c6d74 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -71,7 +71,7 @@ const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => { ) : ( {section[0].label} diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx index 3adebfe9d72..ce620239cf0 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx @@ -81,7 +81,7 @@ const GlobalNavigationMenu: FC = () => { )} > {section[0].label === 'Home' ? ( - + ) : ( section[0].label )} @@ -125,7 +125,7 @@ const GlobalNavigationMenu: FC = () => { > { )} > {section[0].label === 'Home' ? ( - + ) : ( section[0].label )} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx index dd26ebb55c4..6de03119722 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx @@ -3,7 +3,7 @@ import { createContext, useContext } from 'react' interface ContextProps { - activeRefItem: string + activeRefItem: string | undefined setActiveRefItem: (x: string) => void } @@ -15,7 +15,7 @@ interface Provider extends ContextProps { // createContext matches the shape that the consumers expect! const NavMenuContext = createContext({ activeRefItem: undefined, - setActiveRefItem: undefined, + setActiveRefItem: () => {}, }) export const NavigationMenuContextProvider = (props: Provider) => { diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx index d33b0c9a105..7e9056aad82 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx @@ -1,9 +1,9 @@ import { memo } from 'react' import type { NavMenuSection } from '../Navigation.types' +import { useCloseMenuOnRouteChange } from './NavigationMenu.utils' import NavigationMenuGuideList from './NavigationMenuGuideList' import NavigationMenuRefList from './NavigationMenuRefList' -import { useCloseMenuOnRouteChange } from './NavigationMenu.utils' enum MenuId { GettingStarted = 'gettingstarted', @@ -251,6 +251,8 @@ function getMenuById(id: MenuId) { } function getMenuElement(menu: Menu | undefined, props?: any) { + if (!menu) return null + const menuType = menu?.type switch (menuType) { case 'guide': @@ -260,7 +262,7 @@ function getMenuElement(menu: Menu | undefined, props?: any) { ) default: @@ -283,5 +285,5 @@ const NavigationMenu = ({ return getMenuElement(menu, { additionalNavItems }) } -export { MenuId, getMenuById } +export { getMenuById, MenuId } export default memo(NavigationMenu) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts index d8cc3c0b782..4f50c7ee18c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts @@ -1,7 +1,7 @@ 'use client' -import { useEffect, useState } from 'react' import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' import type { ICommonItem } from '~/components/reference/Reference.types' import type { Json } from '~/features/helpers.types' @@ -52,7 +52,10 @@ export function deepFilterSections( * * See https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import */ -export function useCommonSections(commonSectionsFile: string) { +export function useCommonSections( + commonSectionsFile: string, + { enabled = true }: { enabled: boolean } +) { const [commonSections, setCommonSections] = useState() useEffect(() => { @@ -67,6 +70,10 @@ export function useCommonSections(commonSectionsFile: string) { fetchCommonSections() }, [commonSectionsFile]) + if (!enabled) { + return null + } + return commonSections } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx index 9725daa229b..c760bc10b51 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx @@ -16,7 +16,7 @@ const NavigationMenuRefList = ({ commonSectionsFile, specFile, }: NavigationMenuRefListProps) => { - const commonSections = useCommonSections(commonSectionsFile) + const commonSections = useCommonSections(commonSectionsFile, { enabled: !!commonSectionsFile }) const spec = useSpec(specFile) if (!commonSections) { diff --git a/apps/docs/components/Navigation/SideBar.tsx b/apps/docs/components/Navigation/SideBar.tsx index 7e02c14665f..d754fadbc7a 100644 --- a/apps/docs/components/Navigation/SideBar.tsx +++ b/apps/docs/components/Navigation/SideBar.tsx @@ -28,7 +28,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => { if (foundItem) return group }) - const currentSubSection: NavMenuSection = + const currentSubSection: NavMenuSection | undefined = currentSection !== undefined ? currentSection.items.find((section) => { if (section.items.length === 0) { @@ -113,7 +113,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => { {group.items.map((section: NavMenuSection) => { if (section.items.length === 0) { return ( - +
{ {section.items.map((item: NavMenuSection) => ( - +
stateSummary !== 'loggedIn.dataSuccess.hasData' ? [] - : projects.map((project) => { - const organization = organizations.find((org) => org.id === project.organization_id) - return { - id: project.ref, - value: toOrgProjectValue(organization, project), - displayName: toDisplayNameOrgProject(organization, project), - } - }), + : (projects! + .map((project) => { + const organization = organizations!.find((org) => org.id === project.organization_id)! + return { + id: project.ref, + value: toOrgProjectValue(organization, project), + displayName: toDisplayNameOrgProject(organization, project), + } + }) + .filter(Boolean) as ComboBoxOption[]), [organizations, projects, stateSummary] ) @@ -138,18 +140,18 @@ function OrgProjectSelector() { const storedMaybeOrgId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_ORG) const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT) - let storedOrg: Org - let storedProject: Project + let storedOrg: Org | undefined + let storedProject: Project | undefined if (storedMaybeOrgId && storedMaybeProjectRef) { - storedOrg = organizations.find((org) => org.id === Number(storedMaybeOrgId)) - storedProject = projects.find((project) => project.ref === storedMaybeProjectRef) + storedOrg = organizations!.find((org) => org.id === Number(storedMaybeOrgId)) + storedProject = projects!.find((project) => project.ref === storedMaybeProjectRef) } if (storedOrg && storedProject && storedProject.organization_id === storedOrg.id) { setSelectedOrgProject(storedOrg, storedProject) - } else { - const firstProject = projects[0] - const matchingOrg = organizations.find((org) => org.id === firstProject.organization_id) + } else if (projects!.length > 0) { + const firstProject = projects![0] + const matchingOrg = organizations!.find((org) => org.id === firstProject.organization_id) if (matchingOrg) setSelectedOrgProject(matchingOrg, firstProject) } } @@ -172,8 +174,8 @@ function OrgProjectSelector() { const [orgId, projectRef] = fromOrgProjectValue(optionValue) if (!orgId || !projectRef) return - const org = organizations.find((org) => org.id === orgId) - const project = projects.find((project) => project.ref === projectRef) + const org = organizations?.find((org) => org.id === orgId) + const project = projects?.find((project) => project.ref === projectRef) if (org && project && project.organization_id === org.id) { setSelectedOrgProject(org, project) @@ -214,7 +216,7 @@ function BranchSelector() { const formattedData: ComboBoxOption[] = stateSummary !== 'loggedIn.branches.dataSuccess.hasData' ? [] - : data.map((branch) => ({ + : data!.map((branch) => ({ id: branch.id, displayName: branch.name, value: toBranchValue(branch), @@ -224,18 +226,18 @@ function BranchSelector() { if (stateSummary === 'loggedIn.branches.dataSuccess.hasData' && !selectedBranch) { const storedMaybeBranchId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_BRANCH) - let storedBranch: Branch + let storedBranch: Branch | undefined if (storedMaybeBranchId) { - storedBranch = data.find((branch) => branch.id === storedMaybeBranchId) + storedBranch = data!.find((branch) => branch.id === storedMaybeBranchId) } if (storedBranch) { setSelectedBranch(storedBranch) } else { - const productionBranch = data.find( + const productionBranch = data!.find( (branch) => branch.project_ref === branch.parent_project_ref ) - setSelectedBranch(productionBranch ?? data[0]) + setSelectedBranch(productionBranch ?? data![0]) } } }, [data, selectedBranch, setSelectedBranch, stateSummary]) @@ -255,7 +257,7 @@ function BranchSelector() { onSelectOption={(option) => { const [branchId] = fromBranchValue(option) if (branchId) { - const branch = data.find((branch) => branch.id === branchId) + const branch = data?.find((branch) => branch.id === branchId) if (branch) setSelectedBranch(branch) } }} @@ -322,23 +324,23 @@ function VariableView({ variable, className }: { variable: Variable; className?: ? 'loggedIn.selectedProject.dataPending' : ( needsApiQuery - ? isApiError || isInvalidApiData(apiData) - : isSupavisorError || isInvalidSupavisorData(supavisorConfig) + ? isApiError || isInvalidApiData(apiData!) + : isSupavisorError || isInvalidSupavisorData(supavisorConfig!) ) ? 'loggedIn.selectedProject.dataError' : 'loggedIn.selectedProject.dataSuccess' - let variableValue: string = null + let variableValue: string = '' if (stateSummary === 'loggedIn.selectedProject.dataSuccess') { switch (variable) { case 'url': - variableValue = `https://${apiData.app_config!.endpoint}` + variableValue = `https://${apiData?.app_config?.endpoint}` break case 'anonKey': - variableValue = apiData.service_api_keys!.find((key) => key.tags === 'anon')!.api_key + variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || '' break case 'sessionPooler': - variableValue = supavisorConfig[0].connection_string + variableValue = supavisorConfig?.[0]?.connection_string || '' } } diff --git a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx index 97787e0cfee..de07c73bdf0 100644 --- a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx +++ b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx @@ -137,7 +137,11 @@ export default function RealtimeLimitsEstimater({}) {