Merge master resolve conflicts

This commit is contained in:
Joshen Lim committed 2024-05-02 16:39:48 +08:00
commit 3ab6a8e599
80 files changed
+1113 -358

No files matched your search

+2 -2
View File
@@ -7,13 +7,13 @@ export default function AuthProviders({ type }: { type: string }) {
return (
<>
<div className="grid grid-cols-12 gap-10 not-prose py-8">
<div className="grid grid-cols-12 xs:gap-x-10 gap-y-10 not-prose py-8">
{filterProviders.map((x) => (
<Link
href={`${x.href}`}
key={x.name}
passHref
className="col-span-6 lg:col-span-4 xl:col-span-3"
className="col-span-12 xs:col-span-6 lg:col-span-4 xl:col-span-3"
>
<IconPanel title={x.name} icon={x.logo} hasLightIcon={x.hasLightIcon} />
</Link>
@@ -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.
</TabPanel>
<TabPanel id="sql" label="SQL">
```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;
```
@@ -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
+4 -4
View File
@@ -172,11 +172,11 @@ console.log(JSON.stringify(data, null, 2))
returns:
```js
;[
```json
[
{
textarray: ['Harry', 'Larry', 'Moe'],
},
"textarray": ["Harry", "Larry", "Moe"]
}
]
```
@@ -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)
@@ -174,7 +174,7 @@ limit 10;
<Admonition type="note">
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.
</Admonition>
@@ -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.
</TabPanel>
<TabPanel id="sql" label="SQL">
```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;
```
@@ -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);
+2 -2
View File
@@ -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 }), {
@@ -535,7 +535,7 @@ See the Next.js docs to learn more about [opting out of data caching](https://ne
<CH.Code className="max-h-[30rem]">
```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() {
<CH.Code className="max-h-[30rem]">
```ts app/login/action.ts
```ts app/login/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
@@ -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) {
@@ -41,7 +41,7 @@ You can only read data from a Read Replica. This is in contrast to a Primary dat
<Admonition type="note" label="Read Replicas are available on the Team and Enterprise plans">
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).
@@ -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.
-1
View File
@@ -48,7 +48,6 @@ Margarita Sandomirskaia
Marijana Pavlinić
Mark Burggraf
Monica El Khoury
Nikita Kotlyarov
Oli R
Pamela Chia
Paul Caselton
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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"],
@@ -107,7 +107,7 @@ export const DeleteAccountButton = () => {
<DialogSection className="!pt-0">
<span className="text-sm text-foreground flex flex-col gap-y-2">
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.
</span>
<Button
@@ -22,8 +22,11 @@ import {
FormSectionLabel,
} from 'components/ui/Forms'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks'
import { useCheckPermissions, useSelectedOrganization } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import UpgradeToPro from 'components/ui/UpgradeToPro'
const schema = object({
JWT_EXP: number()
@@ -35,7 +38,13 @@ const schema = object({
.required('Must have a Reuse Interval value'),
MFA_MAX_ENROLLED_FACTORS: number()
.min(0, 'Must be be a value more than 0')
.max(30, 'Must be a value less than 30'),
.max(30, 'Must be a value no greater than 30'),
DB_MAX_POOL_SIZE: number()
.min(1, 'Must be 1 or larger')
.max(200, 'Must be a value no greater than 200'),
API_MAX_REQUEST_DURATION: number()
.min(5, 'Must be 5 or larger')
.max(30, 'Must be a value no greater than 30'),
})
const AdvancedAuthSettingsForm = () => {
@@ -52,17 +61,33 @@ const AdvancedAuthSettingsForm = () => {
const formId = 'auth-config-advanced-form'
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const organization = useSelectedOrganization()
const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const isTeamsEnterprisePlan =
isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro'
const promptTeamsEnterpriseUpgrade = IS_PLATFORM && !isTeamsEnterprisePlan
const INITIAL_VALUES = {
SITE_URL: authConfig?.SITE_URL,
JWT_EXP: authConfig?.JWT_EXP,
REFRESH_TOKEN_ROTATION_ENABLED: authConfig?.REFRESH_TOKEN_ROTATION_ENABLED || false,
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL,
MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS || 10,
DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE || 10,
API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION || 10,
}
const onSubmit = (values: any, { resetForm }: any) => {
const payload = { ...values }
if (!isTeamsEnterprisePlan) {
delete payload.DB_MAX_POOL_SIZE
delete payload.API_MAX_REQUEST_DURATION
}
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
@@ -167,10 +192,59 @@ const AdvancedAuthSettingsForm = () => {
size="small"
label="Maximum number of per-user MFA factors"
descriptionText="How many MFA factors can be enrolled at once per user."
actions={<span className="mr-3 text-foreground-lighter">factors</span>}
disabled={!canUpdateConfig}
/>
</FormSectionContent>
</FormSection>
<FormSection
header={<FormSectionLabel>Max Direct Database Connections</FormSectionLabel>}
>
<FormSectionContent loading={isLoading}>
{promptTeamsEnterpriseUpgrade && (
<UpgradeToPro
primaryText="Upgrade to Teams or Enterprise"
secondaryText="Max Direct Database Connections settings are only available on the Teams plan and up."
buttonText="Upgrade to Teams"
projectRef={projectRef!}
organizationSlug={organization!.slug}
/>
)}
<InputNumber
id="DB_MAX_POOL_SIZE"
size="small"
label="Max direct database connections used by Auth"
descriptionText="Auth will take up no more than this number of connections from the total number of available connections to serve requests. These connections are not reserved, so when unused they are released."
actions={<span className="mr-3 text-foreground-lighter">connections</span>}
disabled={!canUpdateConfig || !isTeamsEnterprisePlan}
/>
</FormSectionContent>
</FormSection>
<FormSection header={<FormSectionLabel>Max Request Duration</FormSectionLabel>}>
<FormSectionContent loading={isLoading}>
{promptTeamsEnterpriseUpgrade && (
<UpgradeToPro
primaryText="Upgrade to Teams or Enterprise"
secondaryText="Max Request Duration settings are only available on the Teams plan and up."
buttonText="Upgrade to Teams"
projectRef={projectRef!}
organizationSlug={organization!.slug}
/>
)}
<InputNumber
id="API_MAX_REQUEST_DURATION"
size="small"
label="Maximum time allowed for an Auth request to last"
descriptionText="Number of seconds to wait for an Auth request to complete before canceling it. In certain high-load situations setting a larger or smaller value can be used to control load-shedding. Recommended: 10 seconds."
actions={<span className="mr-3 text-foreground-lighter">seconds</span>}
disabled={!canUpdateConfig || !isTeamsEnterprisePlan}
/>
</FormSectionContent>
</FormSection>
</FormPanel>
</>
)
@@ -178,5 +178,43 @@ const RECOVERY: FormSchema = {
[Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`,
},
}
const REAUTHENTICATION: FormSchema = {
$schema: JSON_SCHEMA_VERSION,
id: 'REAUTHENTICATION',
type: 'object',
title: 'Reauthentication',
properties: {
MAILER_SUBJECTS_REAUTHENTICATION: {
title: 'Subject heading',
type: 'string',
},
MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: {
title: 'Message body',
descriptionOptional: 'HTML body of your email',
type: 'code',
description: `
- \`{{ .Token }}\` : The 6-digit numeric email OTP
- \`{{ .SiteURL }}\` : The URL of the site
- \`{{ .Email }}\` : The user's email address
- \`{{ .Data }}\` : The user's \`user_metadata\`
`,
},
},
validationSchema: object().shape({
MAILER_SUBJECTS_REAUTHENTICATION: string().required('"Subject heading is required.'),
}),
misc: {
iconKey: 'email-icon2',
helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console.
[Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`,
},
}
export const TEMPLATES_SCHEMAS = [CONFIRMATION, INVITE, MAGIC_LINK, EMAIL_CHANGE, RECOVERY]
export const TEMPLATES_SCHEMAS = [
CONFIRMATION,
INVITE,
MAGIC_LINK,
EMAIL_CHANGE,
RECOVERY,
REAUTHENTICATION,
]
@@ -14,6 +14,7 @@ const UNHEALTHY_STATUES: Status[] = [
'INIT_FAILED',
'UNKNOWN',
'MIGRATIONS_FAILED',
'FUNCTIONS_FAILED',
]
const WAITING_STATUSES: Status[] = [
'COMING_UP',
@@ -3,7 +3,7 @@ import Link from 'next/link'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { extensions } from 'shared-data'
import { Badge, IconExternalLink, IconLoader, Modal, Toggle } from 'ui'
import { Badge, IconExternalLink, IconLoader, Toggle } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -72,7 +72,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
title={extension.name}
className="h-5 m-0 text-sm truncate cursor-pointer text-foreground"
>
{extension.name}
{extension.altName || extension.name}
</h3>
<p className="text-sm text-foreground-light">
{extension?.installed_version ?? extension.default_version}
@@ -26,7 +26,7 @@ const Extensions = () => {
})
const formattedExtensions = (data ?? []).map((ext) => {
if (ext.name === 'vector') return { ...ext, name: 'pgvector' }
if (ext.name === 'vector') return { ...ext, altName: 'pgvector' }
else return ext
})
@@ -34,7 +34,7 @@ const Extensions = () => {
filterString.length === 0
? formattedExtensions
: formattedExtensions.filter((ext) =>
ext.name.toLowerCase().includes(filterString.toLowerCase())
(ext?.altName ?? ext.name).toLowerCase().includes(filterString.toLowerCase())
)
const extensionsWithoutHidden = extensions.filter((ext) => !HIDDEN_EXTENSIONS.includes(ext.name))
const [enabledExtensions, disabledExtensions] = partition(
@@ -1,6 +1,10 @@
import type { AutoApiService } from 'data/config/project-api-query'
import { useParams } from 'common'
import Snippets from 'components/interfaces/Docs/Snippets'
import type { AutoApiService } from 'data/config/project-api-query'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import CodeSnippet from './CodeSnippet'
import PublicSchemaNotEnabledAlert from './PublicSchemaNotEnabledAlert'
interface Props {
autoApiService: AutoApiService
@@ -8,16 +12,22 @@ interface Props {
}
export default function Introduction({ autoApiService, selectedLang }: Props) {
const { ref: projectRef } = useParams()
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
const isPublicSchemaEnabled = config?.db_schema
.split(',')
.map((name) => name.trim())
.includes('public')
return (
<>
<div className="doc-section doc-section--client-libraries">
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.init(autoApiService.endpoint)}
/>
</article>
</div>
</>
<div className="doc-section doc-section--client-libraries">
<article className="code">
<CodeSnippet selectedLang={selectedLang} snippet={Snippets.init(autoApiService.endpoint)} />
{!isPublicSchemaEnabled && <PublicSchemaNotEnabledAlert />}
</article>
</div>
)
}
@@ -3,13 +3,22 @@ import Link from 'next/link'
import CodeSnippet from 'components/interfaces/Docs/CodeSnippet'
import GeneratingTypes from 'components/interfaces/Docs/GeneratingTypes'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import PublicSchemaNotEnabledAlert from '../../PublicSchemaNotEnabledAlert'
interface IntroductionProps {
selectedLang: 'bash' | 'js'
}
const Introduction = ({ selectedLang }: IntroductionProps) => {
const { ref } = useParams()
const { ref: projectRef } = useParams()
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
const isPublicSchemaEnabled = config?.db_schema
.split(',')
.map((name) => name.trim())
.includes('public')
return (
<>
@@ -20,6 +29,8 @@ const Introduction = ({ selectedLang }: IntroductionProps) => {
All views and tables in the <code>public</code> schema and accessible by the active
database role for a request are available for querying.
</p>
{!isPublicSchemaEnabled && <PublicSchemaNotEnabledAlert />}
</article>
</div>
@@ -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{' '}
<Link href={`/project/${ref}/settings/database`}>in the settings.</Link>
<Link href={`/project/${projectRef}/settings/database`}>in the settings.</Link>
</p>
</article>
<article className="code">
@@ -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 (
<Alert_Shadcn_ variant="default">
<Info className="h-4 w-4" />
<AlertTitle_Shadcn_ className="!-mt-4">
<ReactMarkdown>The `public` schema for this project is not exposed</ReactMarkdown>
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="grid gap-3 !-mt-7">
<ReactMarkdown>
You will not be able to query tables and views in the `public` schema via supabase-js or
HTTP clients.
</ReactMarkdown>
<div className="!-mt-4 inline-block">
<Button asChild type="default">
<Link
href={`/project/${projectRef}/settings/api#postgrest-config`}
className="!no-underline !hover:bg-surface-100 !text-foreground"
>
View schema settings
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
export default PublicSchemaNotEnabledAlert
@@ -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 = () => {
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
asChild
type="default"
icon={
<SVG
src={`${router.basePath}/img/table-editor.svg`}
style={{ width: `${14}px`, height: `${14}px` }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
}
/>
}
>
<Button asChild type="default" icon={<TableEditor strokeWidth={1.5} />}>
<Link href={`/project/${ref}/editor`}>Table Editor</Link>
</Button>
<Button
asChild
type="default"
icon={
<SVG
src={`${router.basePath}/img/sql-editor.svg`}
style={{ width: `${14}px`, height: `${14}px` }}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
}
/>
}
>
<Button asChild type="default" icon={<SqlEditor strokeWidth={1.5} />}>
<Link href={`/project/${ref}/sql/new`}>SQL editor</Link>
</Button>
<Button asChild type="default" icon={<IconExternalLink size={14} />}>
<Button asChild type="default" icon={<ExternalLink size={14} />}>
<Link
href="https://supabase.com/docs/guides/database"
target="_blank"
@@ -105,7 +82,7 @@ const NewProjectPanel = () => {
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="rounded bg-surface-300 p-1.5 text-foreground-light shadow-sm">
<IconKey strokeWidth={2} size={16} />
<Auth size={16} strokeWidth={1.5} />
</div>
<h5>Authentication</h5>
</div>
@@ -121,7 +98,7 @@ const NewProjectPanel = () => {
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
icon={<ExternalLink size={14} />}
type="default"
asChild
>
@@ -141,7 +118,7 @@ const NewProjectPanel = () => {
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="rounded bg-surface-300 p-1.5 text-foreground-light shadow-sm">
<IconArchive strokeWidth={2} size={16} />
<Storage size={16} strokeWidth={1.5} />
</div>
<h5>Storage</h5>
</div>
@@ -157,7 +134,7 @@ const NewProjectPanel = () => {
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
icon={<ExternalLink size={14} />}
type="default"
asChild
>
@@ -177,7 +154,7 @@ const NewProjectPanel = () => {
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="rounded bg-surface-300 p-1.5 text-foreground-light shadow-sm">
<IconCode strokeWidth={2} size={16} />
<EdgeFunctions size={16} strokeWidth={1.5} />
</div>
<h5>Edge Functions</h5>
</div>
@@ -193,7 +170,7 @@ const NewProjectPanel = () => {
</Button>
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
icon={<ExternalLink size={14} />}
type="default"
asChild
>
@@ -212,7 +189,7 @@ const NewProjectPanel = () => {
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-4">
<div className="rounded bg-surface-300 p-1.5 text-foreground-light shadow-sm">
<IconActivity strokeWidth={2} size={16} />
<Realtime size={16} strokeWidth={1.5} />
</div>
<h5>Realtime</h5>
</div>
@@ -222,9 +199,12 @@ const NewProjectPanel = () => {
</p>
</div>
<div className="flex items-center space-x-2">
<Button type="default" asChild>
<Link href={`/project/${ref}/realtime/inspector`}>Explore Realtime</Link>
</Button>
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
icon={<ExternalLink size={14} />}
type="default"
asChild
>
@@ -265,12 +245,7 @@ const NewProjectPanel = () => {
<Button asChild type="default">
<Link href={`/project/${ref}/settings/api`}>View API settings</Link>
</Button>
<Button
asChild
className="translate-y-[1px]"
type="default"
icon={<IconExternalLink />}
>
<Button asChild className="translate-y-[1px]" type="default" icon={<ExternalLink />}>
<Link
href="https://supabase.com/docs/guides/database/api"
target="_blank"
@@ -132,6 +132,7 @@ const ProjectLinker = ({
connection: {
foreign_project_id: selectedForeignProject?.id,
supabase_project_ref: selectedSupabaseProject?.ref,
integration_id: '0',
metadata: {
...projectDetails,
},
@@ -15,7 +15,6 @@ import { useSelectedOrganization } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { EMPTY_ARR } from 'lib/void'
import { useSidePanelsStateSnapshot } from 'state/side-panels'
import * as Sentry from '@sentry/nextjs'
const VERCEL_ICON = (
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 512 512" className="w-6">
@@ -101,18 +100,6 @@ const SidePanelVercelProjectLinker = () => {
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: 'Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx',
},
},
})
}
createConnections({
...vars,
connection: {
@@ -16,31 +16,52 @@ import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-co
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks'
import { Form, IconAlertCircle, Input, InputNumber } from 'ui'
import MultiSelect from 'ui-patterns/MultiSelect'
import { AlertCircle, Info } from 'lucide-react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Form,
Input,
InputNumber,
} from 'ui'
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import Link from 'next/link'
const PostgrestConfig = () => {
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const { data: schemas } = useSchemasQuery({
const { data: schemas, isLoading: isLoadingSchemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
const { mutate: updatePostgrestConfig, isLoading: isUpdating } =
useProjectPostgrestConfigUpdateMutation({
onSuccess: () => {
toast.success('Successfully saved settings')
},
})
const canUpdatePostgrestConfig = useCheckPermissions(
PermissionAction.UPDATE,
'custom_config_postgrest'
)
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const formId = 'project-postgres-config'
const initialValues = { db_schema: '', max_rows: '', db_extra_search_path: '' }
const initialValues = { db_schema: '', max_rows: '', db_extra_search_path: '', db_pool: '' }
const updateConfig = async (updatedConfig: typeof initialValues) => {
if (!projectRef) return console.error('Project ref is required')
@@ -49,10 +70,10 @@ const PostgrestConfig = () => {
dbSchema: updatedConfig.db_schema,
maxRows: updatedConfig.max_rows,
dbExtraSearchPath: updatedConfig.db_extra_search_path,
dbPool: updatedConfig.db_pool || null,
})
}
const permanentSchema = ['public', 'storage']
const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions']
const schema =
schemas
@@ -65,15 +86,22 @@ const PostgrestConfig = () => {
id: x.id,
value: x.name,
name: x.name,
disabled: indexOf(permanentSchema, x.name) >= 0 ? true : false,
disabled: false,
}
}) ?? []
const isPublicSchemaEnabled = config?.db_schema
.split(',')
.map((name) => name.trim())
.includes('public')
const isGraphqlExtensionEnabled =
(extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null
return (
<Form id={formId} initialValues={initialValues} validate={() => {}} onSubmit={updateConfig}>
{({ handleReset, resetForm, values, initialValues }: any) => {
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
// [Alaister] although this "technically" is breaking the rules of React hooks
// it won't error because the hooks are always rendered in the same order
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -83,7 +111,9 @@ const PostgrestConfig = () => {
db_schema: config.db_schema,
max_rows: config.max_rows,
db_extra_search_path: config.db_extra_search_path ?? '',
db_pool: config.db_pool || null,
}
resetForm({ values, initialValues: values })
}
}, [config])
@@ -112,51 +142,81 @@ const PostgrestConfig = () => {
>
{isError ? (
<div className="flex items-center justify-center py-8 space-x-2">
<IconAlertCircle size={16} strokeWidth={1.5} />
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">Failed to retrieve API settings</p>
</div>
) : (
<>
<FormSection header={<FormSectionLabel>Exposed schemas</FormSectionLabel>}>
<FormSectionContent loading={false}>
{schema.length >= 1 && (
<MultiSelect
disabled={!canUpdatePostgrestConfig}
options={schema}
descriptionText={
<>
{isLoadingSchemas ? (
<div className="col-span-12 flex flex-col gap-2 lg:col-span-7">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
) : (
<FormSectionContent loading={false}>
{schema.length >= 1 && (
<div className="grid gap-2">
<MultiSelectV2
options={schema}
disabled={!canUpdatePostgrestConfig}
value={
values?.db_schema
? values.db_schema.replace(/ /g, '').split(',')
: []
}
placeholder="Choose a schema to expose"
searchPlaceholder="Search for a schema"
onChange={(event) => {
let updatedValues: any = values
updatedValues.db_schema = event.join(', ')
resetForm({ values: updatedValues, initialValues: updatedValues })
updateConfig({ ...updatedValues })
}}
/>
<p className="text-foreground-lighter text-sm">
The schemas to expose in your API. Tables, views and stored procedures
in these schemas will get API endpoints.
<code className="text-xs">public</code> and{' '}
<code className="text-xs">storage</code> are protected by default.
</>
}
emptyMessage={
<>
<IconAlertCircle strokeWidth={2} />
<div className="flex flex-col mt-2 text-center">
<p className="text-sm align-center">
No schema available to choose
</p>
<p className="text-xs opacity-50">
New schemas you create will appear here
</p>
</div>
</>
}
// value must be passed as array of strings
value={(values?.db_schema ?? '').replace(/ /g, '').split(',')}
// onChange returns array of strings
onChange={(event) => {
let updatedValues: any = values
updatedValues.db_schema = event.join(', ')
resetForm({ values: updatedValues, initialValues: updatedValues })
updateConfig({ ...updatedValues })
}}
/>
)}
</FormSectionContent>
</p>
{!isPublicSchemaEnabled && (
<Alert_Shadcn_ variant="default">
<Info className="h-4 w-4" />
<AlertTitle_Shadcn_>
The public schema for this project is not exposed
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p>
You will not be able to query tables and views in the{' '}
<code>public</code> schema via supabase-js or HTTP clients.
</p>
{isGraphqlExtensionEnabled && (
<div className="grid gap-3 mt-2">
<div>
Tables in the <code>public</code> schema are still exposed
over our GraphQL endpoints.
</div>
<p>
<Button asChild type="default">
<Link
href={`/project/${projectRef}/database/extensions`}
className="!no-underline !hover:bg-surface-100 !text-foreground"
>
Disable the pg_graphql extension
</Link>
</Button>
</p>
</div>
)}
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
</div>
)}
</FormSectionContent>
)}
</FormSection>
<FormSection header={<FormSectionLabel>Extra search path</FormSectionLabel>}>
<FormSectionContent loading={false}>
<Input
@@ -177,6 +237,17 @@ const PostgrestConfig = () => {
/>
</FormSectionContent>
</FormSection>
<FormSection header={<FormSectionLabel>Pool size</FormSectionLabel>}>
<FormSectionContent loading={false}>
<InputNumber
id="db_pool"
size="small"
disabled={!canUpdatePostgrestConfig}
descriptionText="Number of maximum connections to keep open in the Data API server's database pool. Unset to let it be configured automatically based on compute size."
placeholder="Configured automatically based on compute size"
/>
</FormSectionContent>
</FormSection>
</>
)}
</FormPanel>
@@ -169,7 +169,7 @@ const ServiceList = () => {
<JWTSettings />
</section>
<section>
<section id="postgrest-config">
<PostgrestConfig />
</section>
</>
@@ -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<number | false>(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'
)
@@ -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<any>(replicaKeys.list(projectRef), (old: Database[]) => {
return old.filter((db: Database) => db.identifier === projectRef)
})
queryClient.setQueriesData<any>(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()
@@ -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 (
@@ -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<number>()
const numTransition = useRef<number>()
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 = () => {
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<IconChevronDown size={16} />}
icon={<ChevronDown size={16} />}
className="px-1 rounded-l-none border-l-0"
/>
</DropdownMenuTrigger>
@@ -261,7 +269,7 @@ const InstanceConfigurationUI = () => {
<DeployNewReplicaPanel
visible={showNewReplicaPanel}
selectedDefaultRegion={newReplicaRegion}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onClose={() => {
setNewReplicaRegion(undefined)
setShowNewReplicaPanel(false)
@@ -270,13 +278,13 @@ const InstanceConfigurationUI = () => {
<DropReplicaConfirmationModal
selectedReplica={selectedReplicaToDrop}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onCancel={() => setSelectedReplicaToDrop(undefined)}
/>
<DropAllReplicasConfirmationModal
visible={showDeleteAllModal}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onCancel={() => setShowDeleteAllModal(false)}
/>
@@ -1,6 +1,6 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import { Database, DatabaseBackup } from 'lucide-react'
import { Database, DatabaseBackup, HelpCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { Handle, NodeProps, Position } from 'reactflow'
import {
@@ -11,11 +11,11 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
IconHelpCircle,
IconMoreVertical,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
cn,
} from 'ui'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
@@ -173,6 +173,15 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
const { ref } = useParams()
const created = dayjs(inserted_at).format('DD MMM YYYY')
const isInTransition = (
[
REPLICA_STATUS.COMING_UP,
REPLICA_STATUS.GOING_DOWN,
REPLICA_STATUS.RESTORING,
REPLICA_STATUS.INIT_READ_REPLICA,
] as string[]
).includes(status)
return (
<>
<Handle
@@ -186,8 +195,19 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
style={{ width: NODE_WIDTH / 2 - 10 }}
>
<div className="flex gap-x-3">
<div className="w-8 h-8 bg-brand-400 border border-brand-500 rounded-md flex items-center justify-center">
<DatabaseBackup size={16} />
<div
className={cn(
'w-8 h-8 border rounded-md flex items-center justify-center',
status === REPLICA_STATUS.ACTIVE_HEALTHY
? 'bg-brand-400 border-brand-500'
: 'bg-surface-100 border-foreground/20'
)}
>
{isInTransition ? (
<Loader2 className="animate-spin" size={16} />
) : (
<DatabaseBackup size={16} />
)}
</div>
<div className="flex flex-col gap-y-0.5">
<div className="flex items-center gap-x-2">
@@ -203,15 +223,22 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
<Badge variant="destructive">Init failed</Badge>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_>
<IconHelpCircle />
<HelpCircle size={16} />
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="w-60 text-center">
Replica failed to initialize. Please drop this replica, and spin up a new one.
<TooltipContent_Shadcn_
side="bottom"
align="end"
alignOffset={-70}
className="w-60 text-center"
>
Replica failed to initialize. Please drop this replica and spin up a new one.
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</>
) : status === REPLICA_STATUS.COMING_UP ? (
<Badge>Coming up</Badge>
) : status === REPLICA_STATUS.GOING_DOWN ? (
<Badge>Going down</Badge>
) : status === REPLICA_STATUS.RESTORING ? (
<Badge>Restoring</Badge>
) : (
@@ -277,12 +277,14 @@ const _SQL_FILTER_COMMON = {
export const SQL_FILTER_TEMPLATES: any = {
postgres_logs: {
..._SQL_FILTER_COMMON,
database: (value: string) => `identifier = '${value}'`,
'severity.error': `parsed.error_severity in ('ERROR', 'FATAL', 'PANIC')`,
'severity.noError': `parsed.error_severity not in ('ERROR', 'FATAL', 'PANIC')`,
'severity.log': `parsed.error_severity = 'LOG'`,
},
edge_logs: {
..._SQL_FILTER_COMMON,
database: (value: string) => `identifier = '${value}'`,
'status_code.error': `response.status_code between 500 and 599`,
'status_code.success': `response.status_code between 200 and 299`,
'status_code.warning': `response.status_code between 400 and 499`,
@@ -344,6 +346,7 @@ export const SQL_FILTER_TEMPLATES: any = {
},
supavisor_logs: {
..._SQL_FILTER_COMMON,
database: (value: string) => `m.project like '${value}%'`,
},
}
@@ -712,3 +715,9 @@ export const TIER_QUERY_LIMITS: {
TEAM: { text: '28 days', value: 28, unit: 'day', promptUpgrade: true },
ENTERPRISE: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
}
export const LOG_ROUTES_WITH_REPLICA_SUPPORT = [
'/project/[ref]/logs/edge-logs',
'/project/[ref]/logs/pooler-logs',
'/project/[ref]/logs/postgres-logs',
]
@@ -81,7 +81,7 @@ const genWhereStatement = (table: LogsTableName, filters: Filters) => {
if (value !== undefined && typeof template === 'function') {
return template(value)
} else if (template === undefined) {
// resolve unknwon filters (possibly from filter overrides)
// resolve unknown filters (possibly from filter overrides)
// no template, set a default
if (typeof value === 'string') {
return `${dotKey} = '${value}'`
@@ -105,12 +105,6 @@ const genWhereStatement = (table: LogsTableName, filters: Filters) => {
(typeof filters[rootKey] === 'string' && (filters[rootKey] as string).length === 0)
) {
return null
} else if (rootKey === 'database') {
return table === 'edge_logs'
? `(request.host like '${filters[rootKey]}%')`
: table === 'supavisor_logs'
? `(m.project like '${filters[rootKey]}%')`
: null
} else if (typeof filters[rootKey] === 'object') {
// join all statements with an OR
const nestedStatements = getDotKeys(filters[rootKey] as Filters, rootKey)
@@ -145,7 +139,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
const orderBy = 'order by timestamp desc'
switch (table) {
case 'edge_logs':
return `select id, timestamp, event_message, request.method, request.path, response.status_code
return `select id, identifier, timestamp, event_message, request.method, request.path, response.status_code
from ${table}
${joins}
${where}
@@ -154,7 +148,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
`
case 'postgres_logs':
return `select postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table}
return `select identifier, postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table}
${joins}
${where}
${orderBy}
@@ -1,20 +1,21 @@
import { Button, Checkbox, Form, Popover } from 'ui'
import React, { useState } from 'react'
import { Filters, FilterSet } from '.'
import { Fragment, useState } from 'react'
interface Props {
import { Button, Checkbox, Form, Popover } from 'ui'
import { FilterSet, Filters } from '.'
interface LogsFilterPopoverProps {
options: FilterSet
filters: Filters
onFiltersChange: (filters: Filters) => void
buttonClassName: string
}
const LogsFilterPopover: React.FC<Props> = ({
const LogsFilterPopover = ({
options,
filters,
onFiltersChange,
buttonClassName,
}) => {
}: LogsFilterPopoverProps) => {
const [open, setOpen] = useState(false)
const handleReset = () => {
@@ -51,7 +52,7 @@ const LogsFilterPopover: React.FC<Props> = ({
<>
<div className="space-y-4 py-6">
{options.options.map((x, i: number) => (
<React.Fragment key={x.key}>
<Fragment key={x.key}>
<Checkbox
value="true"
id={`${options.key}.${x.key}`}
@@ -62,7 +63,7 @@ const LogsFilterPopover: React.FC<Props> = ({
defaultChecked={(filters?.[options.key] as Filters)?.[x.key] as boolean}
/>
{i !== options.options.length - 1 && <Popover.Separator />}
</React.Fragment>
</Fragment>
))}
</div>
<div className="flex items-center justify-end gap-2 border-t border-default bg-studio py-2 px-3">
@@ -70,7 +71,7 @@ const LogsFilterPopover: React.FC<Props> = ({
Clear
</Button>
<Button loading={isSubmitting} type="primary" htmlType="submit">
Save
Apply
</Button>
</div>
</>
@@ -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])
@@ -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}
</p>
</div>
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
<div className="h-4 w-4 animate-ping rounded-full bg-green-800 opacity-60"></div>
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
</div>
)}
<IconRefreshCw />
<RefreshCw />
</div>
}
loading={isLoading}
@@ -147,7 +136,7 @@ const PreviewFilterPanel = ({
}}
icon={
<div className="text-foreground-lighter">
<IconSearch size={14} />
<Search size={14} />
</div>
}
value={search}
@@ -205,7 +194,7 @@ const PreviewFilterPanel = ({
<Button
type="default"
onClick={() => onToggleEventChart()}
icon={isShowingEventChart ? <IconEye /> : <IconEyeOff />}
icon={isShowingEventChart ? <Eye /> : <EyeOff />}
>
Chart
</Button>
@@ -217,7 +206,7 @@ const PreviewFilterPanel = ({
<div className="flex items-center justify-center gap-x-2">
<Tooltip_Shadcn_ delayDuration={100}>
<TooltipTrigger_Shadcn_ asChild>
<Button asChild className="px-1" type="default" icon={<IconTerminal />}>
<Button asChild className="px-1" type="default" icon={<Terminal />}>
<Link href={queryUrl} />
</Button>
</TooltipTrigger_Shadcn_>
@@ -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()
},
@@ -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<any>(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
</Button>
<div className="flex items-center space-x-2">
<Button type="default" onClick={clearFeedback}>
Clear
</Button>
{screenshot !== undefined ? (
<div
style={{ backgroundImage: `url("${screenshot}")` }}
+34 -1
View File
@@ -17,12 +17,18 @@ import {
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
cn,
} from 'ui'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/replicas.utils'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { Check } from 'lucide-react'
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
import { Markdown } from 'components/interfaces/Markdown'
interface DatabaseSelectorProps {
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
@@ -124,6 +130,33 @@ const DatabaseSelector = ({
const region = formatDatabaseRegion(database.region)
const id = formatDatabaseID(database.identifier)
if (database.status !== 'ACTIVE_HEALTHY') {
const status = [
REPLICA_STATUS.INIT_READ_REPLICA,
REPLICA_STATUS.COMING_UP,
].includes(database.status)
? 'coming up'
: 'not healthy'
return (
<Tooltip_Shadcn_ key={database.identifier}>
<TooltipTrigger_Shadcn_ asChild>
<div className="px-2 py-1.5 w-full flex items-center justify-between">
<p className="text-xs text-foreground-lighter">
Read replica ({region} - {id})
</p>
</div>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="right" className="w-80">
<Markdown
className="text-xs text-foreground"
content={`Replica unable to accept requests as its ${status}. [View infrastructure settings](/project/${projectRef}/settings/infrastructure) for more information.`}
/>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
)
}
return (
<CommandItem_Shadcn_
key={database.identifier}
@@ -146,7 +179,7 @@ const DatabaseSelector = ({
? 'Primary database'
: `Read replica (${region} - ${id})`}
</p>
{database.identifier === selectedDatabaseId && <IconCheck />}
{database.identifier === selectedDatabaseId && <Check size={16} />}
</div>
</CommandItem_Shadcn_>
)
@@ -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
}
@@ -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
@@ -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<ReturnType<typeof getDatabaseExtensions>>
@@ -248,6 +248,7 @@ export type IntegrationConnectionsCreateVariables = {
connection: {
foreign_project_id: string
supabase_project_ref: string
integration_id: string
metadata: any
}
orgSlug: string | undefined
+84 -46
View File
@@ -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.
@@ -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<any>(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)
@@ -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: (
<Markdown content={initialMessage} className="text-foreground [&>p]:m-0" />
) as any,
description: <Markdown content={resolutionCTA} className="[&>p]:m-0" />,
action: (
<div className="w-full flex gap-x-2 !mx-0 mt-3">
{/* [Joshen] Ideally we also are able to add this CTA but we can't guarantee this info without an on-demand fetch */}
{/* <Button asChild key="cta-1" type="default">
<Link href={`/project/${projectRef}/editor`}>
View "{referencingTable}" table
</Link>
</Button> */}
<Button asChild type="outline" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/database/postgres/cascade-deletes"
>
Documentation
</a>
</Button>
</div>
),
})
} else {
toast.error(`Failed to delete table row: ${data.message}`)
}
} else {
onError(data, variables, context)
}
+2 -3
View File
@@ -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
+13
View File
@@ -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:
@@ -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:
'<h2>You have been invited</h2>\n\n<p>You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:</p>\n<p><a href="{{ .ConfirmationURL }}">Accept the invite</a></p>',
@@ -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:
'<h2>Confirm reauthentication</h2> <p>Enter the code: {{ .Token }}</p>',
})
}
@@ -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)
@@ -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 = (
<img src={`${BASE_PATH}/img/icons/vercel-icon.svg`} alt="Vercel Icon" className="w-4" />
@@ -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: {
+17 -2
View File
@@ -24,8 +24,14 @@ const JoinOrganizationPage = () => {
const [error, setError] = useState<any>()
const [tokenValidationInfo, setTokenValidationInfo] = useState<TokenInfo>(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 ? (
<p>There was an error requesting details for this invitation. ({error.message})</p>
) : sso_mismatch ? (
<>
<p>SSO providers do not match.</p>
<p className="text-foreground-lighter">
This invitation comes from a different identity provider than the one you are currently
logged in with. Please use the same SSO provider instead.
</p>
</>
) : token_does_not_exist ? (
<>
<p>The invite token is invalid.</p>
@@ -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:
+1 -1
View File
@@ -5,7 +5,7 @@ author: samrose
image: nix-postgres.png
thumb: nix-postgres.png
categories:
- product
- engineering
tags:
- supabase-engineering
date: '2024-04-25'
@@ -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 = '<value from previous query>';
```
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)
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+2 -1
View File
@@ -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",
+2 -2
View File
@@ -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
@@ -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('*')
@@ -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)
+2
View File
@@ -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' },
+10
View File
@@ -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: {
@@ -42,7 +42,7 @@ const PortalToast = () => (
? 'max-w-[380px]'
: isConsentToast
? 'max-w-none sm:max-w-[800px]'
: 'max-w-[260px]'
: 'max-w-[280px]'
}`}
>
{message}