diff --git a/apps/docs/components/AuthProviders.tsx b/apps/docs/components/AuthProviders.tsx index 1c1950ff87c..c9b1ae80263 100644 --- a/apps/docs/components/AuthProviders.tsx +++ b/apps/docs/components/AuthProviders.tsx @@ -7,13 +7,13 @@ export default function AuthProviders({ type }: { type: string }) { return ( <> -
+
{filterProviders.map((x) => ( diff --git a/apps/docs/content/guides/ai/vector-columns.mdx b/apps/docs/content/guides/ai/vector-columns.mdx index 5a1cc7ef803..235eb47169b 100644 --- a/apps/docs/content/guides/ai/vector-columns.mdx +++ b/apps/docs/content/guides/ai/vector-columns.mdx @@ -24,18 +24,18 @@ Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvec 1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. -3. Search for "vector" and enable the extension. +3. Search for "pgvector" and enable the extension. ```sql - -- Example: enable the "vector" extension. + -- Example: enable the pgvector extension. create extension vector with schema extensions; --- Example: disable the "vector" extension +-- Example: disable the pgvector extension drop extension if exists vector; ``` diff --git a/apps/docs/content/guides/auth/auth-deep-dive/auth-google-oauth.mdx b/apps/docs/content/guides/auth/auth-deep-dive/auth-google-oauth.mdx index a33ce52d973..325c44d7ff3 100644 --- a/apps/docs/content/guides/auth/auth-deep-dive/auth-google-oauth.mdx +++ b/apps/docs/content/guides/auth/auth-deep-dive/auth-google-oauth.mdx @@ -88,7 +88,7 @@ Note however that your app will usually have to be verified by Google before you The only thing left to implement is the UI, but if you prefer to use something pre-built, we have a handy [Auth Widget](https://github.com/supabase/ui/#using-supabase-ui-auth), where you can enable/disable whichever auth providers you want to support. -For any support please get in touch at beta at [supabase.com](https://supabase.com) or for feature requests open an issue in the [backend](https://github.com/supabase/gotrue) or [frontend](https://github.com/supabase/gotrue-js) repos. +For any support see [supabase.com/support](https://supabase.com/support) or for feature requests open an issue in the [backend](https://github.com/supabase/auth) or [frontend](https://github.com/supabase/auth-js) repos. ### Resources diff --git a/apps/docs/content/guides/database/arrays.mdx b/apps/docs/content/guides/database/arrays.mdx index a7579573c17..84a410d0770 100644 --- a/apps/docs/content/guides/database/arrays.mdx +++ b/apps/docs/content/guides/database/arrays.mdx @@ -172,11 +172,11 @@ console.log(JSON.stringify(data, null, 2)) returns: -```js -;[ +```json +[ { - textarray: ['Harry', 'Larry', 'Moe'], - }, + "textarray": ["Harry", "Larry", "Moe"] + } ] ``` diff --git a/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx b/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx index faf7ee4dc67..c071df2b88a 100644 --- a/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx +++ b/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx @@ -10,11 +10,12 @@ The `pg-safeupdate` extension is a useful tool for protecting data integrity and ## Enable the extension -`pg-safeupdate` can be enabled on a per connection basis: +`pg-safeupdate` can be enabled on a role basis (where `anon` is the role we want to modify in this example): {/* prettier-ignore */} ```sql -load 'safeupdate'; +GRANT SET ON PARAMETER session_preload_libraries TO anon; +ALTER ROLE anon SET session_preload_libraries = 'safeupdate'; ``` or for all connections: @@ -30,8 +31,6 @@ Let's take a look at an example to see how pg-safeupdate works. Suppose we have {/* prettier-ignore */} ```sql -load 'safeupdate'; - update employees set date_of_birth = '1987-01-28'; ``` @@ -52,3 +51,4 @@ update employees set date_of_birth = '1987-01-28' where id = 12345; ## Resources - Official [pg-safeupdate documentation](https://github.com/eradman/pg-safeupdate) +- Using [pg-safeupdate with PostgREST](https://postgrest.org/en/v12/integrations/pg-safeupdate.html) diff --git a/apps/docs/content/guides/database/extensions/pg_cron.mdx b/apps/docs/content/guides/database/extensions/pg_cron.mdx index 7dd38aaafac..9526c823a62 100644 --- a/apps/docs/content/guides/database/extensions/pg_cron.mdx +++ b/apps/docs/content/guides/database/extensions/pg_cron.mdx @@ -174,7 +174,7 @@ limit 10; -The records in cron.job_run_details are not cleaned automatically which will take up disk space in your datbabase. +The records in cron.job_run_details are not cleaned automatically which will take up disk space in your database. diff --git a/apps/docs/content/guides/database/extensions/pgvector.mdx b/apps/docs/content/guides/database/extensions/pgvector.mdx index 5495f587a07..b0b777eb11f 100644 --- a/apps/docs/content/guides/database/extensions/pgvector.mdx +++ b/apps/docs/content/guides/database/extensions/pgvector.mdx @@ -33,18 +33,18 @@ This is particularly useful if you're building on top of OpenAI's [GPT-3](https: 1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. -3. Search for "vector" and enable the extension. +3. Search for "pgvector" and enable the extension. ```sql - -- Example: enable the "vector" extension. + -- Example: enable the pgvector extension. create extension vector with schema extensions; --- Example: disable the "vector" extension +-- Example: disable the pgvector extension drop extension if exists vector; ``` 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 7cdc62d6c2c..6ed691e326c 100644 --- a/apps/docs/content/guides/database/postgres/column-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/column-level-security.mdx @@ -28,7 +28,7 @@ For example, let's assume you have a `posts` table with the following columns: You can restrict updates to just the user who created it using [RLS](/docs/guides/auth#row-level-security), with the following policy: ```sql -create a policy "Allow update for owners" on posts for +create policy "Allow update for owners" on posts for update using ((select auth.uid()) = user_id); ``` @@ -127,7 +127,7 @@ supabase migration new create_posts_table ); -- Add row-level security - create a policy "Allow update for owners" on posts for + create policy "Allow update for owners" on posts for update using ((select auth.uid()) = user_id); diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 609f18a46de..06f768bc8d2 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -40,11 +40,11 @@ Deno.serve(async (req: Request) => { const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', - { global: { headers: { Authorization: req.headers.get('Authorization')! } } } ) // Get the session or user object - const { data } = await supabaseClient.auth.getUser() + const token = authHeader.replace('Bearer ', '') + const { data } = await supabaseClient.auth.getUser(token) const user = data.user return new Response(JSON.stringify({ user }), { diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx index d44da1bbb07..b9eb21ae17b 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx @@ -535,7 +535,7 @@ See the Next.js docs to learn more about [opting out of data caching](https://ne -```js app/login/action.js +```js app/login/actions.js 'use server' import { revalidatePath } from 'next/cache' @@ -596,7 +596,7 @@ export default function ErrorPage() { -```ts app/login/action.ts +```ts app/login/actions.ts 'use server' import { revalidatePath } from 'next/cache' diff --git a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx index a8d81139f41..cc48e134864 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx @@ -336,9 +336,9 @@ export const GET = async ({ url, locals: { supabase } }) => { ```ts title=src/routes/auth/callback/+server.ts // src/routes/auth/callback/+server.ts -import { redirect } from '@sveltejs/kit' +import { redirect, type RequestHandler } from '@sveltejs/kit' -export const GET = async ({ url, locals: { supabase } }) => { +export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { const code = url.searchParams.get('code') if (code) { diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index 8814c1368f1..7b82d21d602 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -41,7 +41,7 @@ You can only read data from a Read Replica. This is in contrast to a Primary dat -Read replicas are available to projects created on the Teams and Enterprise plans after 2024 March 20. They will be progressively rolled out to older projects, and eventually available on the Pro plan. +Read replicas are available to projects created on the Teams and Enterprise plans on or after 2024 January 1. They will be progressively rolled out to older projects, and eventually available on the Pro plan. If your project doesn't meet these criteria, you can join the [early access queue](https://forms.supabase.com/read-replica). diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index f9e9dc3f6e5..e2aa7dfa756 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -206,7 +206,7 @@ If the tool doesn't exist, we build and open source it ourselves. - [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc. - [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. -For the system to work cohesively, some services require additional configuration within the Postgres database. For example, the APIs and Auth system require several [default roles](/docs/guides/database/postgres-roles) and the `pgjwt` Postgres extension. +For the system to work cohesively, some services require additional configuration within the Postgres database. For example, the APIs and Auth system require several [default roles](/docs/guides/database/postgres/roles) and the `pgjwt` Postgres extension. You can find all the default extensions inside the [schema migration scripts repo](https://github.com/supabase/postgres/tree/develop/migrations). These scripts are mounted at `/docker-entrypoint-initdb.d` to run automatically when starting the database container. diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 1c721c74f13..2b127ce16c9 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -48,7 +48,6 @@ Margarita Sandomirskaia Marijana Pavlinić Mark Burggraf Monica El Khoury -Nikita Kotlyarov Oli R Pamela Chia Paul Caselton diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index d17bb6bb29e..f5ed03f07f1 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.163.2 + version: 1.163.6 title: Supabase CLI language: sh source: https://github.com/supabase/cli diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 69ffa9315d0..b715e47a2d7 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -5,7 +5,8 @@ "baseUrl": ".", "paths": { "~/*": ["./*"], - "@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths + "@ui/*": ["./../../packages/ui/src/*"], + "@ui-patterns/*": ["./../../packages/ui-patterns/*"] }, "target": "ES2021", "lib": ["dom", "dom.iterable", "esnext"], diff --git a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx index abe01bb2986..44bd433e9f1 100644 --- a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx @@ -107,7 +107,7 @@ export const DeleteAccountButton = () => { Before submitting an account deletion request, please ensure that your account is - not part of any organization. This can be done by leaving leaving or deleting the + not part of any organization. This can be done by leaving or deleting the organizations that you are a part of.
@@ -56,7 +67,7 @@ const Introduction = ({ selectedLang }: IntroductionProps) => { If you still want to use GraphQL, you can. Supabase provides you with a full Postgres database, so as long as your middleware can connect to the database then you can still use the tools you love. You can find the database connection details{' '} - in the settings. + in the settings.

diff --git a/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx b/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx new file mode 100644 index 00000000000..99be76b020f --- /dev/null +++ b/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx @@ -0,0 +1,37 @@ +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { Info } from 'lucide-react' +import ReactMarkdown from 'react-markdown' +import Link from 'next/link' +import { useParams } from 'common' + +const PublicSchemaNotEnabledAlert = () => { + const { ref: projectRef } = useParams() + + return ( + + + + The `public` schema for this project is not exposed + + + + You will not be able to query tables and views in the `public` schema via supabase-js or + HTTP clients. + + +
+ +
+
+
+ ) +} + +export default PublicSchemaNotEnabledAlert diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index 122e549468a..0ef1e55ef12 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -1,10 +1,11 @@ +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import SVG from 'react-inlinesvg' -import { Button, IconActivity, IconArchive, IconCode, IconExternalLink, IconKey } from 'ui' +import { Button } from 'ui' import Panel from 'components/ui/Panel' import { useIsFeatureEnabled } from 'hooks' +import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons' import APIKeys from './APIKeys' import GetStartedHero from './GetStartedHero' @@ -45,37 +46,13 @@ const NewProjectPanel = () => {

- - -
+ - +

+
+ )} + + + )} + + )} + + )} + Extra search path}> { /> + Pool size}> + + + + )} diff --git a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx index 6a15bfa2ab3..328dcfbaa86 100644 --- a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx +++ b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx @@ -169,7 +169,7 @@ const ServiceList = () => { -
+
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index acb257b643a..cbcc60d981c 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -1,4 +1,5 @@ import { useParams } from 'common' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' @@ -11,17 +12,16 @@ import { SidePanel, } from 'ui' -import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' +import { useEnablePhysicalBackupsMutation } from 'data/database/enable-physical-backups-mutation' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { Region, useReadReplicaSetUpMutation } from 'data/read-replicas/replica-setup-mutation' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSelectedOrganization, useSelectedProject } from 'hooks' import { AWS_REGIONS, AWS_REGIONS_DEFAULT, AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants' +import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' import { AVAILABLE_REPLICA_REGIONS, AWS_REGIONS_VALUES } from './InstanceConfiguration.constants' -import { useBackupsQuery } from 'data/database/backups-query' -import { useEnablePhysicalBackupsMutation } from 'data/database/enable-physical-backups-mutation' -import { ExternalLink } from 'lucide-react' // [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually @@ -44,16 +44,17 @@ const DeployNewReplicaPanel = ({ const [refetchInterval, setRefetchInterval] = useState(false) const { data } = useReadReplicasQuery({ projectRef }) - const { data: backups } = useBackupsQuery( - { projectRef }, + useProjectDetailQuery( + { ref: projectRef }, { refetchInterval, refetchOnWindowFocus: false, onSuccess: (data) => { - if (data.walg_enabled) setRefetchInterval(false) + if (data.is_physical_backups_enabled) setRefetchInterval(false) }, } ) + const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) @@ -83,7 +84,7 @@ const DeployNewReplicaPanel = ({ const reachedMaxReplicas = (data ?? []).filter((db) => db.identifier !== projectRef).length >= 2 const isFreePlan = subscription?.plan.id === 'free' - const isWalgEnabled = backups?.walg_enabled + const isWalgEnabled = project?.is_physical_backups_enabled const currentComputeAddon = addons?.selected_addons.find( (addon) => addon.type === 'compute_instance' ) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx index f3b973fc212..a56372bfe50 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx @@ -1,18 +1,11 @@ import { useParams } from 'common' import toast from 'react-hot-toast' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - IconAlertTriangle, - Modal, -} from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation' -import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useQueryClient } from '@tanstack/react-query' import { replicaKeys } from 'data/read-replicas/keys' +import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' interface DropAllReplicasConfirmationModalProps { visible: boolean @@ -38,20 +31,19 @@ const DropAllReplicasConfirmationModal = ({ const replicas = databases.filter((db) => db.identifier !== projectRef) await Promise.all( replicas.map((db) => - removeReadReplica({ projectRef, identifier: db.identifier, skipInvalidateOnSuccess: true }) + removeReadReplica({ + projectRef, + identifier: db.identifier, + invalidateReplicaQueries: false, + }) ) ) toast.success(`Tearing down all read replicas`) - queryClient.setQueriesData(replicaKeys.list(projectRef), (old: Database[]) => { - return old.filter((db: Database) => db.identifier === projectRef) - }) - queryClient.setQueriesData(replicaKeys.loadBalancers(projectRef), (old: Database[]) => []) - - setTimeout(async () => { - await queryClient.invalidateQueries(replicaKeys.list(projectRef)) - await queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)) - }, 5000) + await Promise.all([ + queryClient.invalidateQueries(replicaKeys.list(projectRef)), + queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)), + ]) onSuccess() onCancel() diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx index 096b5196991..18b6a21fbb2 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx @@ -38,7 +38,11 @@ const DropReplicaConfirmationModal = ({ if (!projectRef) return console.error('Project is required') if (selectedReplica === undefined) return toast.error('No replica selected') - await removeReadReplica({ projectRef, identifier: selectedReplica.identifier }) + await removeReadReplica({ + projectRef, + identifier: selectedReplica.identifier, + invalidateReplicaQueries: true, + }) } return ( diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index 3d9d7be27be..e34538d6d05 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common' import { partition } from 'lodash' -import { Globe2, Loader2, Network } from 'lucide-react' +import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react' import { useTheme } from 'next-themes' import { useEffect, useMemo, useRef, useState } from 'react' import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow' @@ -12,7 +12,6 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - IconChevronDown, } from 'ui' import AlertError from 'components/ui/AlertError' @@ -24,11 +23,12 @@ import { timeout } from 'lib/helpers' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' import ComputeInstanceSidePanel from '../../Addons/ComputeInstanceSidePanel' import DeployNewReplicaPanel from './DeployNewReplicaPanel' +import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' import DropReplicaConfirmationModal from './DropReplicaConfirmationModal' +import { REPLICA_STATUS } from './InstanceConfiguration.constants' import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceConfiguration.utils' import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode' import MapView from './MapView' -import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' // [Joshen] Just FYI, UI assumes single provider for primary + replicas // [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows @@ -38,7 +38,7 @@ const InstanceConfigurationUI = () => { const reactFlow = useReactFlow() const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() - const numComingUp = useRef() + const numTransition = useRef() const snap = useSubscriptionPageStateSnapshot() const [view, setView] = useState<'flow' | 'map'>('flow') @@ -76,13 +76,21 @@ const InstanceConfigurationUI = () => { { refetchInterval: refetchInterval as any, refetchOnWindowFocus: false, - onSuccess: async (data) => { - const comingUpReplicas = data.filter((db) => db.status === 'COMING_UP') - const hasTransientStatus = comingUpReplicas.length > 0 + onSuccess: async (res) => { + const fixedStatues = [ + REPLICA_STATUS.ACTIVE_HEALTHY, + REPLICA_STATUS.ACTIVE_UNHEALTHY, + REPLICA_STATUS.INIT_READ_REPLICA_FAILED, + ] + const replicasInTransition = res.filter((db) => !fixedStatues.includes(db.status)) + const hasTransientStatus = replicasInTransition.length > 0 // If any replica's status has changed, refetch databases - if (numComingUp.current !== comingUpReplicas.length) { - numComingUp.current = comingUpReplicas.length + if ( + numTransition.current !== replicasInTransition.length || + res.length !== (data ?? []).length + ) { + numTransition.current = replicasInTransition.length await refetchReplicas() setTimeout(() => refetchLoadBalancers(), 2000) } @@ -192,7 +200,7 @@ const InstanceConfigurationUI = () => { diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 34cea58a523..0bcc0ca15a7 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -24,7 +24,7 @@ import { useSelectedOrganization } from 'hooks' import useLogsPreview from 'hooks/analytics/useLogsPreview' import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { LOGS_TABLES } from './Logs.constants' +import { LOGS_TABLES, LOG_ROUTES_WITH_REPLICA_SUPPORT } from './Logs.constants' import UpgradePrompt from './UpgradePrompt' /** @@ -110,9 +110,23 @@ export const LogsPreviewer = ({ }, [its, subscription]) useEffect(() => { - if (readReplicasEnabled && db !== undefined) { - const database = databases?.find((d) => d.identifier === db) - if (database !== undefined) state.setSelectedDatabaseId(db) + if (readReplicasEnabled) { + if (db !== undefined) { + const database = databases?.find((d) => d.identifier === db) + if (database !== undefined) state.setSelectedDatabaseId(db) + } else if ( + state.selectedDatabaseId !== undefined && + state.selectedDatabaseId !== projectRef + ) { + if (LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname)) { + router.push({ + pathname: router.pathname, + query: { ...router.query, db: state.selectedDatabaseId }, + }) + } else { + state.setSelectedDatabaseId(projectRef) + } + } } }, [readReplicasEnabled, db, isSuccess]) diff --git a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx index 845d27b9c10..b9ff3938c47 100644 --- a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx @@ -1,25 +1,15 @@ +import { Eye, EyeOff, RefreshCw, Search, Terminal } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { - Button, - IconEye, - IconEyeOff, - IconRefreshCw, - IconSearch, - IconTerminal, - Input, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, -} from 'ui' +import { Button, Input, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import CSVButton from 'components/ui/CSVButton' import DatabaseSelector from 'components/ui/DatabaseSelector' import { Filters, LogSearchCallback, LogTemplate, PREVIEWER_DATEPICKER_HELPERS } from '.' import DatePickers from './Logs.DatePickers' -import { FILTER_OPTIONS, LogsTableName } from './Logs.constants' +import { FILTER_OPTIONS, LOG_ROUTES_WITH_REPLICA_SUPPORT, LogsTableName } from './Logs.constants' import LogsFilterPopover from './LogsFilterPopover' interface PreviewFilterPanelProps { @@ -72,8 +62,7 @@ const PreviewFilterPanel = ({ // [Joshen] These are the routes tested that can show replica logs const showDatabaseSelector = - project?.is_read_replicas_enabled && - ['/project/[ref]/logs/edge-logs', '/project/[ref]/logs/pooler-logs'].includes(router.pathname) + project?.is_read_replicas_enabled && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) const hasEdits = search !== defaultSearchValue @@ -99,11 +88,11 @@ const PreviewFilterPanel = ({ {newCount > 1000 ? `${Math.floor(newCount / 100) / 10}K` : newCount}

-
+
)} - + } loading={isLoading} @@ -147,7 +136,7 @@ const PreviewFilterPanel = ({ }} icon={
- +
} value={search} @@ -205,7 +194,7 @@ const PreviewFilterPanel = ({ @@ -217,7 +206,7 @@ const PreviewFilterPanel = ({
- diff --git a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx index a76fd8a4bf6..46dbd43eb41 100644 --- a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -103,9 +103,6 @@ const DeleteConfirmationDialogs = ({ } toast.success(`Successfully deleted selected row(s)`) }, - onError: (error) => { - toast.error(`Failed to delete row: ${error.message}`) - }, onSettled: () => { snap.closeConfirmationDialog() }, diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx index a1a5c553667..906cfd22fde 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx @@ -36,6 +36,9 @@ const FeedbackWidget = ({ screenshot, setScreenshot, }: FeedbackWidgetProps) => { + const FEEDBACK_STORAGE_KEY = 'feedback_content' + const SCREENSHOT_STORAGE_KEY = 'screenshot' + const router = useRouter() const { ref, slug } = useParams() const inputRef = useRef(null) @@ -45,6 +48,28 @@ const FeedbackWidget = ({ const [isSavingScreenshot, setIsSavingScreenshot] = useState(false) const { mutateAsync: submitFeedback } = useSendFeedbackMutation() + useEffect(() => { + const storedFeedback = localStorage.getItem(FEEDBACK_STORAGE_KEY) + if (storedFeedback) { + setFeedback(storedFeedback) + } + + const storedScreenshot = localStorage.getItem(SCREENSHOT_STORAGE_KEY) + if (storedScreenshot) { + setScreenshot(storedScreenshot) + } + }, []) + + useEffect(() => { + localStorage.setItem(FEEDBACK_STORAGE_KEY, feedback) + }, [feedback]) + + useEffect(() => { + if (screenshot) { + localStorage.setItem(SCREENSHOT_STORAGE_KEY, screenshot) + } + }, [screenshot]) + useEffect(() => { inputRef?.current?.focus() }, [inputRef]) @@ -53,6 +78,13 @@ const FeedbackWidget = ({ setFeedback(e.target.value) } + const clearFeedback = () => { + setFeedback('') + setScreenshot(undefined) + localStorage.removeItem(FEEDBACK_STORAGE_KEY) + localStorage.removeItem(SCREENSHOT_STORAGE_KEY) + } + const captureScreenshot = async () => { setIsSavingScreenshot(true) @@ -66,7 +98,10 @@ const FeedbackWidget = ({ // Give time for dropdown to close await timeout(100) toPng(document.body, { filter }) - .then((dataUrl: any) => setScreenshot(dataUrl)) + .then((dataUrl: any) => { + localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) + setScreenshot(dataUrl) + }) .catch((error: any) => toast.error('Failed to capture screenshot')) .finally(() => { setIsSavingScreenshot(false) @@ -79,7 +114,11 @@ const FeedbackWidget = ({ const reader = new FileReader() reader.onload = function (event) { - setScreenshot(event.target?.result as string) + const dataUrl = event.target?.result + if (typeof dataUrl === 'string') { + setScreenshot(dataUrl) + localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) + } } reader.readAsDataURL(file) event.target.value = '' @@ -105,6 +144,9 @@ const FeedbackWidget = ({ pathname: router.asPath, }) setFeedback('') + setScreenshot(undefined) + localStorage.removeItem(FEEDBACK_STORAGE_KEY) + localStorage.removeItem(SCREENSHOT_STORAGE_KEY) toast.success( 'Feedback sent. Thank you!\n\nPlease be aware that we do not provide responses to feedback. If you require assistance or a reply, consider submitting a support ticket.' ) @@ -133,6 +175,9 @@ const FeedbackWidget = ({ Cancel
+ {screenshot !== undefined ? (
+ +
+

+ Read replica ({region} - {id}) +

+
+
+ + + + + ) + } + return ( - {database.identifier === selectedDatabaseId && } + {database.identifier === selectedDatabaseId && }
) diff --git a/apps/studio/data/config/project-postgrest-config-query.ts b/apps/studio/data/config/project-postgrest-config-query.ts index f278ab01a9b..fcf36806e33 100644 --- a/apps/studio/data/config/project-postgrest-config-query.ts +++ b/apps/studio/data/config/project-postgrest-config-query.ts @@ -13,6 +13,7 @@ export type ProjectPostgrestConfigResponse = { db_schema: string db_anon_role: string db_extra_search_path: string + db_pool: number | null jwt_secret: string } diff --git a/apps/studio/data/config/project-postgrest-config-update-mutation.ts b/apps/studio/data/config/project-postgrest-config-update-mutation.ts index 0e319361257..82f1e3409d0 100644 --- a/apps/studio/data/config/project-postgrest-config-update-mutation.ts +++ b/apps/studio/data/config/project-postgrest-config-update-mutation.ts @@ -11,6 +11,7 @@ export type ProjectPostgrestConfigUpdateVariables = { dbSchema: string maxRows: string dbExtraSearchPath: string + dbPool: string | null } export async function updateProjectPostgrestConfig({ @@ -18,11 +19,13 @@ export async function updateProjectPostgrestConfig({ dbSchema, maxRows, dbExtraSearchPath, + dbPool, }: ProjectPostgrestConfigUpdateVariables) { const response = await patch(`${API_URL}/projects/${projectRef}/config/postgrest`, { db_schema: dbSchema, max_rows: maxRows, db_extra_search_path: dbExtraSearchPath, + db_pool: dbPool, }) if (response.error) throw response.error diff --git a/apps/studio/data/database-extensions/database-extensions-query.ts b/apps/studio/data/database-extensions/database-extensions-query.ts index 2d57aecf21d..bc0c2fb2af9 100644 --- a/apps/studio/data/database-extensions/database-extensions-query.ts +++ b/apps/studio/data/database-extensions/database-extensions-query.ts @@ -2,12 +2,18 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' import { get } from 'data/fetchers' import type { ResponseError } from 'types' import { databaseExtensionsKeys } from './keys' +import { components } from 'api-types' export type DatabaseExtensionsVariables = { projectRef?: string connectionString?: string } +export type Extension = components['schemas']['PostgresExtension'] +export interface DatabaseExtension extends Extension { + altName?: string +} + export async function getDatabaseExtensions( { projectRef, connectionString }: DatabaseExtensionsVariables, signal?: AbortSignal @@ -31,7 +37,7 @@ export async function getDatabaseExtensions( }) if (error) throw error - return data + return data as DatabaseExtension[] } export type DatabaseExtensionsData = Awaited> diff --git a/apps/studio/data/integrations/integrations.types.ts b/apps/studio/data/integrations/integrations.types.ts index 773c8d76dbd..844b531a735 100644 --- a/apps/studio/data/integrations/integrations.types.ts +++ b/apps/studio/data/integrations/integrations.types.ts @@ -248,6 +248,7 @@ export type IntegrationConnectionsCreateVariables = { connection: { foreign_project_id: string supabase_project_ref: string + integration_id: string metadata: any } orgSlug: string | undefined diff --git a/apps/studio/data/lint/lint-query.ts b/apps/studio/data/lint/lint-query.ts index 5c290d45e79..5cc1dfc1697 100644 --- a/apps/studio/data/lint/lint-query.ts +++ b/apps/studio/data/lint/lint-query.ts @@ -80,7 +80,7 @@ union all ( select 'auth_users_exposed' as name, - 'WARN' as level, + 'ERROR' as level, 'EXTERNAL' as facing, array['SECURITY'] as categories, 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in the public schema, potentially compromising user data security.' as description, @@ -90,22 +90,22 @@ select ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as remediation, jsonb_build_object( - 'schema', 'public', + 'schema', n.nspname, 'name', c.relname, 'type', 'view', 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null) ) as metadata, - format('auth_users_exposed_%s_%s', 'public', c.relname) as cache_key + format('auth_users_exposed_%s_%s', n.nspname, c.relname) as cache_key from -- Identify the oid for auth.users - pg_catalog.pg_class auth_users_pg_class + pg_catalog.pg_class auth_users_pg_class join pg_catalog.pg_namespace auth_users_pg_namespace - on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid - and auth_users_pg_class.relname = 'users' - and auth_users_pg_namespace.nspname = 'auth' - -- Depends on auth.users + on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid + and auth_users_pg_class.relname = 'users' + and auth_users_pg_namespace.nspname = 'auth' + -- Depends on auth.users join pg_catalog.pg_depend d - on d.refobjid = auth_users_pg_class.oid + on d.refobjid = auth_users_pg_class.oid join pg_catalog.pg_rewrite r on r.oid = d.objid join pg_catalog.pg_class c @@ -116,7 +116,6 @@ from on d.refobjid = pg_class_auth_users.oid where d.deptype = 'n' - and n.nspname = 'public' and ( pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') @@ -132,7 +131,7 @@ where -- Standard View, accessible to anon or authenticated that is security_definer ( c.relkind = 'v' -- v for view - -- Exclude security invoker views + -- Exclude security invoker views and not ( lower(coalesce(c.reloptions::text,'{}'))::text[] && array[ @@ -147,7 +146,7 @@ where -- Standard View, security invoker, but no RLS enabled on auth.users ( c.relkind in ('v') -- v for view - -- is security invoker + -- is security invoker and ( lower(coalesce(c.reloptions::text,'{}'))::text[] && array[ @@ -157,11 +156,13 @@ where 'security_invoker=on' ] ) - and not pg_class_auth_users.relrowsecurity + and not pg_class_auth_users.relrowsecurity ) ) group by - c.relname, c.oid) + n.nspname, + c.relname, + c.oid) union all ( with policies as ( @@ -219,18 +220,38 @@ where '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( + -- Example: auth.uid() ( - -- Example: auth.uid() - qual ~ '(auth)\\.(uid|jwt|role|email)\\(\\)' - -- Example: select auth.uid() - and lower(qual) !~ 'select\\s+(auth)\\.(uid|jwt|role|email)\\(\\)' + qual like '%auth.uid()%' + and lower(qual) not like '%select auth.uid()%' ) - or - ( - -- Example: auth.uid() - with_check ~ '(auth)\\.(uid|jwt|role|email)\\(\\)' - -- Example: select auth.uid() - and lower(with_check) !~ 'select\\s+(auth)\\.(uid|jwt|role|email)\\(\\)' + or ( + qual like '%auth.jwt()%' + and lower(qual) not like '%select auth.jwt()%' + ) + or ( + qual like '%auth.role()%' + and lower(qual) not like '%select auth.role()%' + ) + or ( + qual like '%auth.email()%' + and lower(qual) not like '%select auth.email()%' + ) + or ( + with_check like '%auth.uid()%' + and lower(with_check) not like '%select auth.uid()%' + ) + or ( + with_check like '%auth.jwt()%' + and lower(with_check) not like '%select auth.jwt()%' + ) + or ( + with_check like '%auth.role()%' + and lower(with_check) not like '%select auth.role()%' + ) + or ( + with_check like '%auth.email()%' + and lower(with_check) not like '%select auth.email()%' ) )) union all @@ -377,6 +398,7 @@ from ) act(cmd) where c.relkind = 'r' -- regular tables + and p.polpermissive -- policy is permissive and n.nspname not in ( '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) @@ -395,7 +417,7 @@ union all ( select 'policy_exists_rls_disabled' as name, - 'INFO' as level, + 'ERROR' as level, 'EXTERNAL' as facing, array['SECURITY'] as categories, 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as description, @@ -539,7 +561,7 @@ union all ( select 'security_definer_view' as name, - 'WARN' as level, + 'ERROR' as level, 'EXTERNAL' as facing, array['SECURITY'] as categories, 'Detects views that are SECURITY DEFINER meaning that they ignore row level security (RLS) policies.' as description, @@ -568,17 +590,23 @@ from and dep.deptype = 'e' where c.relkind = 'v' - and n.nspname = 'public' + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname not in ( + 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgsodium', 'pgsodium_masks', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'storage', 'supabase_functions', 'supabase_migrations', 'vault' + ) and dep.objid is null -- exclude views owned by extensions - and not ( - lower(coalesce(c.reloptions::text,'{}'))::text[] - && array[ - 'security_invoker=1', - 'security_invoker=true', - 'security_invoker=yes', - 'security_invoker=on' - ] - )) + and not ( + lower(coalesce(c.reloptions::text,'{}'))::text[] + && array[ + 'security_invoker=1', + 'security_invoker=true', + 'security_invoker=yes', + 'security_invoker=on' + ] + )) union all ( select @@ -648,12 +676,18 @@ from on c.relnamespace = n.oid where c.relkind = 'r' -- regular tables - and n.nspname = 'public' -- RLS is disabled - and not c.relrowsecurity) + and not c.relrowsecurity + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname not in ( + '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + )) union all ( -select +select 'extension_in_public' as name, 'WARN' as level, 'EXTERNAL' as facing, @@ -679,6 +713,10 @@ where -- plpgsql is installed by default in public and outside user control -- confirmed safe pe.extname not in ('plpgsql') + -- Scoping this to public is not optimal. Ideally we would use the postgres + -- search path. That currently isn't available via SQL. In other lints + -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that + -- is not appropriate here as it would evaluate true for the extensions schema and pe.extnamespace::regnamespace::text = 'public') union all ( @@ -726,13 +764,13 @@ where '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( - -- Example: auth.jwt() -> 'user_metadata' - -- False positives are possible, but it isn't practical to string match - -- If false positive rate is too high, this expression can iterate - qual like '%auth.jwt()%user_metadata%' - or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' - or with_check like '%auth.jwt()%user_metadata%' - or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' + -- Example: auth.jwt() -> 'user_metadata' + -- False positives are possible, but it isn't practical to string match + -- If false positive rate is too high, this expression can iterate + qual like '%auth.jwt()%user_metadata%' + or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' + or with_check like '%auth.jwt()%user_metadata%' + or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' ))`.trim() // Array of all lint rules we handle right now. diff --git a/apps/studio/data/read-replicas/replica-remove-mutation.ts b/apps/studio/data/read-replicas/replica-remove-mutation.ts index 2601e7b5a1d..0bb56ddee17 100644 --- a/apps/studio/data/read-replicas/replica-remove-mutation.ts +++ b/apps/studio/data/read-replicas/replica-remove-mutation.ts @@ -4,12 +4,11 @@ import { toast } from 'react-hot-toast' import { post } from 'data/fetchers' import type { ResponseError } from 'types' import { replicaKeys } from './keys' -import type { Database } from './replicas-query' export type ReadReplicaRemoveVariables = { projectRef: string identifier: string - skipInvalidateOnSuccess?: boolean + invalidateReplicaQueries: boolean } export async function removeReadReplica({ projectRef, identifier }: ReadReplicaRemoveVariables) { @@ -40,18 +39,13 @@ export const useReadReplicaRemoveMutation = ({ (vars) => removeReadReplica(vars), { async onSuccess(data, variables, context) { - const { projectRef, identifier, skipInvalidateOnSuccess } = variables + const { projectRef, invalidateReplicaQueries } = variables - if (skipInvalidateOnSuccess === false) { - // [Joshen] Just FYI, will remove this once API changes to remove the need for optimistic rendering - queryClient.setQueriesData(replicaKeys.list(projectRef), (old: any) => { - return old.filter((db: Database) => db.identifier !== identifier) - }) - - setTimeout(async () => { - await queryClient.invalidateQueries(replicaKeys.list(projectRef)) - await queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)) - }, 5000) + if (invalidateReplicaQueries) { + await Promise.all([ + queryClient.invalidateQueries(replicaKeys.list(projectRef)), + queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)), + ]) } await onSuccess?.(data, variables, context) diff --git a/apps/studio/data/table-rows/table-row-delete-mutation.ts b/apps/studio/data/table-rows/table-row-delete-mutation.tsx similarity index 50% rename from apps/studio/data/table-rows/table-row-delete-mutation.ts rename to apps/studio/data/table-rows/table-row-delete-mutation.tsx index 9ebd685c439..1957b5a3330 100644 --- a/apps/studio/data/table-rows/table-row-delete-mutation.ts +++ b/apps/studio/data/table-rows/table-row-delete-mutation.tsx @@ -1,7 +1,10 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { ExternalLink } from 'lucide-react' import { toast } from 'react-hot-toast' +import { Button, toast as UiToast } from 'ui' import { Query, SupaRow } from 'components/grid' +import { Markdown } from 'components/interfaces/Markdown' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' import type { Table } from 'data/tables/table-query' @@ -78,7 +81,52 @@ export const useTableRowDeleteMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to delete table row: ${data.message}`) + const { table, rows } = variables + const isFkError = data.message.includes('violates foreign key constraint') + const isMultipleRows = rows.length > 1 + + if (isFkError) { + const sourceTable = table.name + const referencingTable = data.message.split('on table ')[2].replaceAll('"', '') + const fkName = data.message + .split('foreign key constraint')[1] + .split('on table')[0] + .replaceAll('"', '') + const initialMessage = isMultipleRows + ? `Unable to delete rows as one of them is currently referenced by a foreign key constraint from the table \`${referencingTable}\`.` + : `Unable to delete row as it is currently referenced by a foreign key constraint from the table \`${referencingTable}\`.` + const resolutionCTA = `Set an on delete behavior on the foreign key relation \`${fkName}\` in the \`${referencingTable}\` table to automatically respond when row(s) are being deleted in the \`${sourceTable}\` table.` + + UiToast({ + variant: 'default', + style: { flexDirection: 'column' }, + title: ( + + ) as any, + description: , + action: ( +
+ {/* [Joshen] Ideally we also are able to add this CTA but we can't guarantee this info without an on-demand fetch */} + {/* */} + +
+ ), + }) + } else { + toast.error(`Failed to delete table row: ${data.message}`) + } } else { onError(data, variables, context) } diff --git a/apps/studio/data/tables/table-query.ts b/apps/studio/data/tables/table-query.ts index ae9b159e9f3..2e8bea2b5dd 100644 --- a/apps/studio/data/tables/table-query.ts +++ b/apps/studio/data/tables/table-query.ts @@ -1,10 +1,9 @@ import type { PostgresTable } from '@supabase/postgres-meta' import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -// import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import { tableKeys } from './keys' import { useCallback } from 'react' + import { get, handleError } from 'data/fetchers' +import { tableKeys } from './keys' export type TableVariables = { id?: number diff --git a/apps/studio/hooks/misc/useTable.ts b/apps/studio/hooks/misc/useTable.ts index 19647e03c88..17bf4042be1 100644 --- a/apps/studio/hooks/misc/useTable.ts +++ b/apps/studio/hooks/misc/useTable.ts @@ -29,6 +29,17 @@ function useTable(id?: number) { } ) + const partitionTableResult = useTableQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: entity?.id, + }, + { + enabled: entity?.type === ENTITY_TYPE.PARTITIONED_TABLE, + } + ) + const viewResult = useViewQuery( { projectRef: project?.ref, @@ -65,6 +76,8 @@ function useTable(id?: number) { switch (entity?.type) { case ENTITY_TYPE.TABLE: return tableResult + case ENTITY_TYPE.PARTITIONED_TABLE: + return partitionTableResult case ENTITY_TYPE.VIEW: return viewResult case ENTITY_TYPE.MATERIALIZED_VIEW: diff --git a/apps/studio/pages/api/auth/[ref]/config.ts b/apps/studio/pages/api/auth/[ref]/config.ts index 19ab6e288d8..1faef0ce619 100644 --- a/apps/studio/pages/api/auth/[ref]/config.ts +++ b/apps/studio/pages/api/auth/[ref]/config.ts @@ -120,6 +120,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { MAILER_SUBJECTS_RECOVERY: 'Reset Your Password', MAILER_SUBJECTS_EMAIL_CHANGE: 'Confirm Email Change', MAILER_SUBJECTS_MAGIC_LINK: 'Your Magic Link', + MAILER_SUBJECTS_REAUTHENTICATION: 'Confirm Reauthentication', MAILER_TEMPLATES_INVITE: null, MAILER_TEMPLATES_INVITE_CONTENT: '

You have been invited

\n\n

You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:

\n

Accept the invite

', @@ -166,6 +167,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { SMS_VONAGE_FROM: null, SMS_TEXTLOCAL_API_KEY: null, SMS_TEXTLOCAL_SENDER: null, + MAILER_TEMPLATES_REAUTHENTICATION: null, + MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: + '

Confirm reauthentication

Enter the code: {{ .Token }}

', }) } diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index e875e8013c4..bc19bf6d3ca 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -23,7 +23,6 @@ import { passwordStrength } from 'lib/helpers' import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils' import { useIntegrationInstallationSnapshot } from 'state/integration-installation' import type { NextPageWithLayout } from 'types' -import * as Sentry from '@sentry/nextjs' const VercelIntegration: NextPageWithLayout = () => { return ( @@ -204,11 +203,12 @@ const CreateProject = () => { const projectDetails = vercelProjects?.find((x: any) => x.id === foreignProjectId) try { - const connection = { + const { id: connectionId } = await createConnections({ organizationIntegrationId: organizationIntegration?.id, connection: { foreign_project_id: foreignProjectId, supabase_project_ref: newProjectRef, + integration_id: '0', metadata: { ...projectDetails, supabaseConfig: { @@ -219,26 +219,8 @@ const CreateProject = () => { }, }, orgSlug: selectedOrganization?.slug, - } + }) - // TODO(kamil): Remove once "missing connection name" bug has been squashed. - if (!projectDetails?.name) { - Sentry.captureMessage('Vercel connection is missing name', { - contexts: { - connection: connection, - data: { - projectDetails, - vercelProjects, - foreignProjectId, - }, - source: { - file: 'deploy-button/new-project.tsx', - }, - }, - }) - } - - const { id: connectionId } = await createConnections(connection) await syncEnvs({ connectionId }) } catch (error) { console.error('An error occurred during createConnections:', error) diff --git a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index 8f9208c843a..d22d3a1f4ba 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -19,7 +19,6 @@ import { BASE_PATH, PROJECT_STATUS } from 'lib/constants' import { EMPTY_ARR } from 'lib/void' import { useIntegrationInstallationSnapshot } from 'state/integration-installation' import type { NextPageWithLayout, Organization } from 'types' -import * as Sentry from '@sentry/nextjs' const VERCEL_ICON = ( Vercel Icon @@ -129,18 +128,6 @@ const VercelIntegration: NextPageWithLayout = () => { const onCreateConnections = useCallback( (vars: any) => { - // TODO(kamil): Remove once "missing connection name" bug has been squashed. - if (!vars.connection?.metadata?.name) { - Sentry.captureMessage('Vercel connection is missing name', { - contexts: { - connection: vars.connection, - source: { - file: 'marketplace/choose-project.tsx', - }, - }, - }) - } - createConnections({ ...vars, connection: { diff --git a/apps/studio/pages/join/index.tsx b/apps/studio/pages/join/index.tsx index 89be3ea773a..cd767fffef3 100644 --- a/apps/studio/pages/join/index.tsx +++ b/apps/studio/pages/join/index.tsx @@ -24,8 +24,14 @@ const JoinOrganizationPage = () => { const [error, setError] = useState() const [tokenValidationInfo, setTokenValidationInfo] = useState(undefined) const [tokenInfoLoaded, setTokenInfoLoaded] = useState(false) - const { token_does_not_exist, email_match, expired_token, organization_name, invite_id } = - tokenValidationInfo || {} + const { + token_does_not_exist, + sso_mismatch, + email_match, + expired_token, + organization_name, + invite_id, + } = tokenValidationInfo || {} const loginRedirectLink = `/?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}` @@ -93,6 +99,7 @@ const JoinOrganizationPage = () => { const isError = error || + !!(tokenInfoLoaded && sso_mismatch) || !!(tokenInfoLoaded && token_does_not_exist) || (tokenInfoLoaded && !email_match) || (tokenInfoLoaded && expired_token) @@ -111,6 +118,14 @@ const JoinOrganizationPage = () => { const message = error ? (

There was an error requesting details for this invitation. ({error.message})

+ ) : sso_mismatch ? ( + <> +

SSO providers do not match.

+

+ This invitation comes from a different identity provider than the one you are currently + logged in with. Please use the same SSO provider instead. +

+ ) : token_does_not_exist ? ( <>

The invite token is invalid.

diff --git a/apps/www/_blog/2024-04-12-supabase-oss-hackathon.mdx b/apps/www/_blog/2024-04-12-supabase-oss-hackathon.mdx index a3f4254c1c9..ec8bd628e6b 100644 --- a/apps/www/_blog/2024-04-12-supabase-oss-hackathon.mdx +++ b/apps/www/_blog/2024-04-12-supabase-oss-hackathon.mdx @@ -4,7 +4,7 @@ description: Build an Open Source Project over 10 days. 5 prize categories. author: ant_wilson image: oss-hackathon/thumbnail.png thumb: oss-hackathon/thumbnail.png -launchweek: 8 +launchweek: 11 categories: - launch-week tags: diff --git a/apps/www/_blog/2024-04-25-nix-postgres.mdx b/apps/www/_blog/2024-04-25-nix-postgres.mdx index bd733d9914f..4e25ed35f4a 100644 --- a/apps/www/_blog/2024-04-25-nix-postgres.mdx +++ b/apps/www/_blog/2024-04-25-nix-postgres.mdx @@ -5,7 +5,7 @@ author: samrose image: nix-postgres.png thumb: nix-postgres.png categories: - - product + - engineering tags: - supabase-engineering date: '2024-04-25' diff --git a/apps/www/_blog/2024-04-26-postgres-bloat.mdx b/apps/www/_blog/2024-04-26-postgres-bloat.mdx new file mode 100644 index 00000000000..773719ea995 --- /dev/null +++ b/apps/www/_blog/2024-04-26-postgres-bloat.mdx @@ -0,0 +1,199 @@ +--- +title: 'Postgres Bloat Minimization' +description: 'Understanding and minimizing Postgres table bloat' +author: pavel +image: postgres-bloat.png +thumb: postgres-bloat.png +categories: + - engineering +tags: + - supabase-engineering + - planetpg +date: '2024-04-26' +toc_depth: 3 +--- + +### How data of Postgres tables stored + +By default, all table data in Postgres are physically stored using the “heap” method. So every database is a set of 1Gb files (”segments”) and each file is logically split into 8Kb pages. Actual table rows are put into any page with enough free space. + +When the row data is updated, a new version of a whole row is constructed and written (to any free space). The old one remains because, at the time of the update, the transaction is not completed and can be rolled back in the future. When the transaction is completed we’ll have two or several versions of the same row in the table. Cleaning old ones is by an asynchronous process called vacuum (and autovacuum). + +### How does the vacuum work? + +Vacuum goes through all table files looking for row versions that were updated or deleted by already completed transactions and frees the space on the pages. + +Then it updates the table’s free-space-map to reflect that some page has a certain amount of free space for row inserts. + +It also updates the visibility map for a page. It marks that all remaining rows are visible. So index scans can skip visibility checks, which is not so for the modified page before vacuuming. This significantly increases the speed of queries using indexes. + +In many cases vacuum runs automatically, cleans everything, and requires little care. But in some scenarios, we need to go deeper and tune the autovacuum parameters or run the vacuum manually. + +### Cleaning relation files + +Vacuum marks space in a relation file as free to use for future row inserts or updates. And it’s not a problem unless we insert many rows, delete many rows at once, and then don’t make any inserts or updates. Space remains reserved in a file but we don’t use it. + +In this case, we could free actual filesystem space by running more aggressive mode: + +```sql +VACUUM FULL mytable; +``` + +It will rebuild the table from live rows and they will be placed compactly so that filesystem space will be freed. The downside is that it needs an exclusive lock and you won’t be able to modify the table while `VACUUM FULL` does it's work. It’s wise to execute that process when the database is least accessed e.g. at night. + +An alternative way that doesn’t need full locks is using pg_repack extension: + +```sh +pg_repack --table mytable test +``` + +pg_repack operates similarly to VACUUM FULL for database `test` and table `mytable`. + +### Table bloating and autovaccuum + +To see database bloat via the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) run: + +```sh +$ supabase inspect db bloat +``` + +or: + +```sql +-- number of dead rows +SELECT + n_dead_tup +FROM + pg_stat_user_tables +WHERE + relname = ‘mytable’; + +-- number of live rows +SELECT + count(*) +FROM + mytable; +``` + +If the numbers differ by more than 2x, chances are that the autovacuum didn’t start or hasn’t completed for a table. There could be several legitimate reasons for this. + +You can see information for the last successful autovacuum by running: + +```sh +$ supabase inspect db vacuum-stats +``` + +or: + +```sql +SELECT + last_vacuum, + last_autovacuum +FROM + pg_stat_user_tables +WHERE + relname = ‘mytable’; +``` + +Let’s turn on autovacuum logging so that all autovacuum events land in the log: + +```sql +ALTER TABLE mytable SET log_autovacuum_min_duration to 0; +``` + +There are two ways we can encounter this situation: + +- autovacuum hasn’t started +- autovacuum did start, (possibly multiple times) but never succeeded + +### Autovacuum hasn’t started for a table + +Autovacuum starts based on several configuration parameters like timeout and pattern of access to a particular table. Maybe it’s even legitimate that it hasn’t started. + +- `autovacuum_vacuum_threshold` - number of rows updated or deleted in a table to invoke autovacuum +- `autovacuum_vacuum_insert_threshold` - number of rows inserted into a table to invoke autovacuum +- `autovacuum_vacuum_scale_factor` - a fraction of table modified by updates or deletes to invoke autovacuum +- `autovacuum_vacuum_insert_scale_factor` - a fraction of table modified by inserts to invoke autovacuum + +With all these parameters set, the autovacuum will start if the number of rows updated or deleted exceeds: + +``` +autovacuum_vacuum_scale_factor * size_of_table + autovacuum_vacuum_threshold +``` + +The same logic applies for inserts. Default scale factors are 20% of a table, which could be too high for big tables. If we want autovacuum to occur on large tables more frequently and take less time each run, decrease the default values for these tables e.g.: + +```jsx +ALTER TABLE mytable SET autovacuum_vacuum_scale_factor to 0.05; +``` + +- `autovacuum_naptime` (default 1 min) - Each 1-minute autovacuum daemon will see the state of all tables in the database and decide whether to start autovacuum for a table. Most often this parameter does not need to be modified. + +To see global vacuum settings for your cluster let’s run: + +```jsx +SELECT * from pg_settings where category like 'Autovacuum'; +``` + +To see current settings for a table (that overrides global settings) run: + +```jsx +SELECT relname, reloptions FROM pg_class WHERE relname='mytable'; +``` + +### Autovacuum started but couldn’t succeed + +The most common reason autovacuum doesn’t succeed is long-running open transactions that access old row versions. In that case, Postgres recognizes that the row versions are still needed so any row versions created after that point can’t be marked as dead. One common cause for this problem is interactive sessions that were left open on accident. When tuples can’t be marked as dead, the database begins to bloat. + +To see all open transactions run: + +```jsx +SELECT xact_start, state FROM pg_stat_activity; +``` + +To close transactions found to be idling: + +```jsx +SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE xact_start = ''; +``` + +For automatically closing idle transactions in a session: + +```jsx +SET idle_in_transaction_session_timeout TO '10000s'; +``` + +The same parameter could be set per role or database as needed. + +Another, less likely, possibility is that autovacuum can’t succeed due to locks. If some of your processes take the `SHARE UPDATE EXCLUSIVE` lock e.g. `ALTER TABLE` clause, this lock will prevent vacuum from processing a table. Lock conflicts in your ordinary transactions could cause `SHARE UPDATE EXCLUSIVE` to be taken for a long time. A good recipe when this happens is to cancel all the open transactions and run VACUUM for a table manually (or wait until the next autovacuum comes for it). + +```jsx +SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE state = 'active'; +VACUUM mytable; +``` + +### Other vacuum optimizations + +There could be too few autovacuum workers or each worker could operate slowly due to the low setting of `autovacuum_work_mem`. + +- `autovacuum_max_workers` (default 3) - Number of parallel workers doing autovacuum for tables. When you have enough cores, increasing the default value is worthwhile. Note that this will decrease the number of possible backends or regular parallel workers running at a time. +- `autovacuum_work_mem` (default equal to `maintenance_work_mem` or 64Mb) - work memory that is used per autovacuum worker. If you see in your logs that autovacuum for some tables starts but takes a long time, that time may be decreased by increasing this value. This parameter can only be modified in the config file. + +### Conclusion + +Vacuum and autovacuum are efficient ways to maintain the tables without bloat. They have several parameters that allow efficient tuning. Some insight into what database does can help prevent the cases where autovacuum becomes problematic and bloat increases i.e.: + +- Long open transactions +- Stuck locks +- Insufficient resources allocated to vacuuming +- Space not freed on a filesystem level after massive table modifications + +For more information about bloat and vacuuming, here are some direct references to resources: + +[Full official documentation for vacuum](https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-BASICS) + +[Per-table vacuum parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) + +[Global autovacuum parameters (most of them need modifying postgresql.conf, but could be overridden by per-table parameters)](https://www.postgresql.org/docs/current/runtime-config-autovacuum.html) + +[Supabase CLI reference](https://supabase.com/docs/reference/cli/global-flags) diff --git a/apps/www/_blog/2024-04-30-supabase-oss-hackathon-winners.mdx b/apps/www/_blog/2024-04-30-supabase-oss-hackathon-winners.mdx new file mode 100644 index 00000000000..3f50869e070 --- /dev/null +++ b/apps/www/_blog/2024-04-30-supabase-oss-hackathon-winners.mdx @@ -0,0 +1,125 @@ +--- +title: 'Open Source Hackathon 2024 winners' +description: Announcing the winners of the Open Source Hackathon 2024! +author: tyler_shukert +image: oss-hackathon-winners/thumbnail.png +thumb: oss-hackathon-winners/thumbnail.png +launchweek: 11 +categories: + - launch-week +tags: + - launch-week + - hackathon +date: '2024-04-30' +toc_depth: 2 +--- + +The [Supabase GA week](https://supabase.com/ga-week) brought us so many cool updates, but the fun isn’t over yet, because we are now announcing the winners of the [Supabase Open Source Hackathon 2024](https://supabase.com/blog/supabase-oss-hackathon)! + +We have enjoyed trying out every single submission we have received! You can find all of the submissions on [madewithsupabase.com](https://www.madewithsupabase.com/). + +Now, without further ado, let us announce the winners of the hackathon! + +## Best overall project + +### Winner + +[vdbs - vision database sql](https://github.com/xavimondev/vdbs) - by [@xavimonp](https://twitter.com/xavimonp) + +vdbs stands for "vision database SQL". It allows you to convert your database diagrams into SQL Schema using the capabilities of Vision API. Once the SQL is ready, you can either copy and paste it right in your Supabase project or run the generated npm command to create the migration file. + +![vdbs](/images/blog/oss-hackathon-winners/vdbs.png) + +### Runner Up + +[Complete Components](https://github.com/gSUz92nc/Complete-Components/) - by [@gSUz92nc](https://github.com/gSUz92nc) + +Complete Components is an open-source project that uses AI to assist developers in quickly creating and integrating HTML components with Supabase as the backend and Tailwind CSS for styling. It aims to streamline the development process by leveraging AI to generate tailored HTML components and seamlessly integrate them with Supabase. + +![Complete Components](/images/blog/oss-hackathon-winners/complete-components.png) + +## Best use of AI + +### Winner + +[ClosetAi | Your virtual closet, endless possibilities.](https://github.com/ineffablesam/closet-ai#closet-ai--revamp-your-style-effortlessly) - by [@samuelP09301972](https://twitter.com/samuelP09301972), [@teamnextvitap](https://twitter.com/teamnextvitap), and two others + +Closet AI: the ultimate style companion. Upload images, choose your desired topwear or bottomwear, and watch as our cutting-edge AI replaces your outfit instantly! Revolutionize your wardrobe with the power of AI and Supabase! + +![ClosetAi](/images/blog/oss-hackathon-winners/closetai.png) + +### Runner Up + +[Plot Twist](https://github.com/NeoFoxxo/plottwist) - by [@NeoFoxMusic](https://twitter.com/NeoFoxMusic), [@bonndubz](https://twitter.com/bonndubz), [@Nidhish18224246](https://twitter.com/Nidhish18224246) and [@SlaviKaraslavov](https://twitter.com/SlaviKaraslavov) + +Generate interactive stories where your choices shape the narrative. Start with a prompt, and let our custom AI generate the story and choices you can make. Want to see what you would do if you were stranded on an island? Or if you were a cat who is a pro skateboarder? Provide the idea and you can experience these scenarios and affect the story! + +![Plot Twist](/images/blog/oss-hackathon-winners/plot-twist.png) + +## Most fun / best easter egg + +### Winner + +[NamePLAT](https://github.com/Git-Prakhar/npat-game) - by [@Abhishek_cpp](https://twitter.com/Abhishek_cpp) and [@BlackStarGames\_](https://twitter.com/BlackStarGames_) + +Name Place Animal Thing (NamePLAT) is a fun and challenging game where players compete to quickly identify a name (country) a place, an animal, and a thing that starts with a given letter, but the twist is you get to choose between 4 Images and the fastest finger gets most points. The game is designed to test players' knowledge, speed, and creativity. + +![NamePLAT](/images/blog/oss-hackathon-winners/nameplat.png) + +### Runner Up + +[Supapaused](https://github.com/jensen/supapaused) - by [@karljensen](https://twitter.com/karljensen) + +Supapaused is an app that allows Supabase users to track their paused Supabase instances. You can view when the projects were paused in a timeline. + +![Supapaused](/images/blog/oss-hackathon-winners/supapaused.png) + +## Most technically impressive + +### Winner + +[Retorithoughts](https://github.com/Jcampillo1207/Retoritoughts) - by [@Chema12071](https://twitter.com/Chema12071), [@alexf505](https://twitter.com/alexf505) and [@Patuqueson](https://twitter.com/Patuqueson) + +Retorithoughts is a trivia game that challenges players to determine the chronological order of historical events. It's fun and an educational tool that tests and expands your knowledge of history. + +![Retorithoughts](/images/blog/oss-hackathon-winners/retorithoughts.png) + +### Runner Up + +[Data Loom](https://github.com/sangonz193/data-loom) - by [@sangonz193](https://github.com/sangonz193) + +Data Loom aims to provide a hassle-free and secure way to share files between devices. The platform leverages WebRTC technology, ensuring that your files are transferred directly and securely, with no intermediary server access. + +![Data Loom](/images/blog/oss-hackathon-winners/data-loom.png) + +## Most visually pleasing + +### Winner + +[SupaWriter](https://github.com/phibersoft/supawriter.git) - by [@phibersoft](https://github.com/phibersoft) + +SupaWriter is a ten-finger typing game that helps you improve your typing speed and accuracy. It also features a leaderboard to see how you are performing compared to others. + +![SupaWriter](/images/blog/oss-hackathon-winners/supawriter.png) + +### Runner Up + +[Echoes of Creation](https://github.com/laznic/echoes-of-creation) - by [@laznic](https://twitter.com/laznic) and [@janireijonen](https://twitter.com/janireijonen) + +Echoes of Creation is a digital experience that portrays the excitement and struggles artists have with their creative processes. + +![Echoes of Creation](/images/blog/oss-hackathon-winners/echos-of-creation.png) + +## The Prizes + +![The prize](/images/blog/oss-hackathon-winners/prize.png) + +The winner of the best overall project will receive an Apple AirPods, and the winners and the runner-ups in other categories will each receive a Supabase swag kit. + +## Getting Started Guides + +If you're inspired to build, check out some of the latest resources: + +- [Quick Start Guides](https://supabase.com/docs/guides/getting-started) +- [AI & Vectors Guide](https://supabase.com/docs/guides/ai) +- [Edge Functions Guide](https://supabase.com/docs/guides/functions) diff --git a/apps/www/public/images/blog/oss-hackathon-winners/closetai.png b/apps/www/public/images/blog/oss-hackathon-winners/closetai.png new file mode 100644 index 00000000000..2523085712e Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/closetai.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/complete-components.png b/apps/www/public/images/blog/oss-hackathon-winners/complete-components.png new file mode 100644 index 00000000000..2dc6632c88c Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/complete-components.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/data-loom.png b/apps/www/public/images/blog/oss-hackathon-winners/data-loom.png new file mode 100644 index 00000000000..6217966fc41 Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/data-loom.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/echos-of-creation.png b/apps/www/public/images/blog/oss-hackathon-winners/echos-of-creation.png new file mode 100644 index 00000000000..50d931f2f7b Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/echos-of-creation.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/nameplat.png b/apps/www/public/images/blog/oss-hackathon-winners/nameplat.png new file mode 100644 index 00000000000..3ff27c3c1ae Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/nameplat.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/plot-twist.png b/apps/www/public/images/blog/oss-hackathon-winners/plot-twist.png new file mode 100644 index 00000000000..2b31501d28c Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/plot-twist.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/prize.png b/apps/www/public/images/blog/oss-hackathon-winners/prize.png new file mode 100644 index 00000000000..45145503785 Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/prize.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/retorithoughts.png b/apps/www/public/images/blog/oss-hackathon-winners/retorithoughts.png new file mode 100644 index 00000000000..ca3f0ebe3b8 Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/retorithoughts.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/supapaused.png b/apps/www/public/images/blog/oss-hackathon-winners/supapaused.png new file mode 100644 index 00000000000..0ee1fe806eb Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/supapaused.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/supawriter.png b/apps/www/public/images/blog/oss-hackathon-winners/supawriter.png new file mode 100644 index 00000000000..79470892cf9 Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/supawriter.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/thumbnail.png b/apps/www/public/images/blog/oss-hackathon-winners/thumbnail.png new file mode 100644 index 00000000000..1f562af2a3a Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/thumbnail.png differ diff --git a/apps/www/public/images/blog/oss-hackathon-winners/vdbs.png b/apps/www/public/images/blog/oss-hackathon-winners/vdbs.png new file mode 100644 index 00000000000..ebbcd6d008d Binary files /dev/null and b/apps/www/public/images/blog/oss-hackathon-winners/vdbs.png differ diff --git a/apps/www/public/images/blog/postgres-bloat.png b/apps/www/public/images/blog/postgres-bloat.png new file mode 100644 index 00000000000..7f660eeaef0 Binary files /dev/null and b/apps/www/public/images/blog/postgres-bloat.png differ diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json index d4dd1b53c89..9a6ba53d48b 100644 --- a/apps/www/tsconfig.json +++ b/apps/www/tsconfig.json @@ -3,7 +3,8 @@ "baseUrl": ".", "paths": { "~/*": ["./*"], - "@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths + "@ui/*": ["./../../packages/ui/src/*"], + "@ui-patterns/*": ["./../../packages/ui-patterns/*"] }, "downlevelIteration": true, "target": "ES2021", diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 63bd8fb8093..ea0e767f7a3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -76,7 +76,7 @@ services: auth: container_name: supabase-auth - image: supabase/gotrue:v2.145.0 + image: supabase/gotrue:v2.149.0 depends_on: db: # Disable this if you are using an external Postgres database @@ -354,7 +354,7 @@ services: # Comment out everything below this point if you are using an external Postgres database db: container_name: supabase-db - image: supabase/postgres:15.1.0.147 + image: supabase/postgres:15.1.1.41 healthcheck: test: pg_isready -U postgres -h localhost interval: 5s diff --git a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts index 5654c97a95b..93fc185c480 100644 --- a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts +++ b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts @@ -28,10 +28,14 @@ Deno.serve(async (req: Request) => { }, } ) + + // First get the token from the Authorization header + const token = req.headers.get('Authorization').replace('Bearer ', '') + // Now we can get the session or user object const { data: { user }, - } = await supabaseClient.auth.getUser() + } = await supabaseClient.auth.getUser(token) // And we can run queries in the context of our authenticated user const { data, error } = await supabaseClient.from('users').select('*') diff --git a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts index cd266975080..9ecac3b9ff9 100644 --- a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts +++ b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts @@ -20,10 +20,14 @@ Deno.serve(async (req) => { }, } ) + + // First get the token from the Authorization header + const token = req.headers.get('Authorization').replace('Bearer ', '') + // Now we can get the session or user object const { data: { user }, - } = await supabaseClient.auth.getUser() + } = await supabaseClient.auth.getUser(token) if (!user) throw new Error('no user') console.log(user.id) diff --git a/packages/shared-data/logConstants.ts b/packages/shared-data/logConstants.ts index 379c706b4d0..28e1e31e8f0 100644 --- a/packages/shared-data/logConstants.ts +++ b/packages/shared-data/logConstants.ts @@ -6,6 +6,7 @@ const schemas = [ { path: 'id', type: 'string' }, { path: 'timestamp', type: 'datetime' }, { path: 'event_message', type: 'string' }, + { path: 'identifier', type: 'string' }, { path: 'metadata.request.cf.asOrganization', type: 'string' }, { path: 'metadata.request.cf.asn', type: 'number' }, { path: 'metadata.request.cf.botManagement.corporateProxy', type: 'boolean' }, @@ -181,6 +182,7 @@ const schemas = [ { path: 'event_message', type: 'string' }, { path: 'id', type: 'string' }, { path: 'timestamp', type: 'datetime' }, + { path: 'identifier', type: 'string' }, { path: 'metadata.host', type: 'string' }, { path: 'metadata.parsed.backend_type', type: 'string' }, { path: 'metadata.parsed.command_tag', type: 'string' }, diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 156deda1aa2..1047f2e1147 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -109,6 +109,16 @@ export const pricing: Pricing = { }, usage_based: false, }, + { + title: 'Branching', + plans: { + free: false, + pro: '$0.32 per branch, per day', + team: '$0.32 per branch, per day', + enterprise: 'Custom', + }, + usage_based: true, + }, { title: 'Bandwidth', tooltips: { diff --git a/packages/ui/src/layout/PortalToast/PortalToast.tsx b/packages/ui/src/layout/PortalToast/PortalToast.tsx index 38b646d3619..69d808ff15e 100644 --- a/packages/ui/src/layout/PortalToast/PortalToast.tsx +++ b/packages/ui/src/layout/PortalToast/PortalToast.tsx @@ -42,7 +42,7 @@ const PortalToast = () => ( ? 'max-w-[380px]' : isConsentToast ? 'max-w-none sm:max-w-[800px]' - : 'max-w-[260px]' + : 'max-w-[280px]' }`} > {message}