fix conflicts

This commit is contained in:
Jordi Enric committed 2024-10-29 12:07:24 +01:00
commit d484a117ab
431 files changed
+11387 -4280

No files matched your search

+59 -4
View File
@@ -1798,13 +1798,68 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"multi-select-form": {
name: "multi-select-form",
"multi-select-disabled": {
name: "multi-select-disabled",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-form")),
component: React.lazy(() => import("@/registry/default/example/multi-select-disabled")),
source: "",
files: ["registry/default/example/multi-select-form.tsx"],
files: ["registry/default/example/multi-select-disabled.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-badge-limit-wrap": {
name: "multi-select-badge-limit-wrap",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit-wrap")),
source: "",
files: ["registry/default/example/multi-select-badge-limit-wrap.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-badge-limit": {
name: "multi-select-badge-limit",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit")),
source: "",
files: ["registry/default/example/multi-select-badge-limit.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-combobox": {
name: "multi-select-combobox",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-combobox")),
source: "",
files: ["registry/default/example/multi-select-combobox.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-deletable-badge": {
name: "multi-select-deletable-badge",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-deletable-badge")),
source: "",
files: ["registry/default/example/multi-select-deletable-badge.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-inline-search-input": {
name: "multi-select-inline-search-input",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-inline-search-input")),
source: "",
files: ["registry/default/example/multi-select-inline-search-input.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
@@ -4,6 +4,14 @@ description: Latest updates and announcements.
toc: false
---
## 18th Oct 2024 - Update multi-select
Improved Multi Select with combobox, badge limit, and more examples.
- [Multi Select](/design-system/components/multi-select)
[PR](https://github.com/supabase/supabase/pull/29695)
## 12th June 2024 - Button styles updated
- [`<Button />`](/design-system/docs/components/button) styles updated
@@ -12,31 +12,62 @@ fragment: true
```tsx
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
MultiSelectorContent,
MultiSelectorItem,
} from 'ui-patterns/multi-select'
```
```tsx
<MultiSelector values={value} onValuesChange={setValue}>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select items" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
</MultiSelectorList>
<MultiSelector
open={open}
onOpenChange={setOpen}
selected={selected}
onSelectedChange={onSelectedChange}
>
<MultiSelectorTrigger className="w-72" label="Fruits" />
<MultiSelectorContent sameWidthAsTrigger>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorContent>
</MultiSelector>
```
## Examples
### Form
### Disabled
<ComponentPreview name="multi-select-form" />
<ComponentPreview name="multi-select-disabled" />
### Badge Combo Box
Use `MultiSelectorInput`to add a search input.
<ComponentPreview name="multi-select-combobox" />
### Badge Limit
badgeLimit: `number` | `"wrap"`.
`badgeLimit` prop on the `MultiSelectorTrigger` component can be used to limit the number of badges displayed.
<ComponentPreview name="multi-select-badge-limit" />
### Badge Limit="wrap"
`badgeLimit` prop can also be "wrap" to wrap the badges to the next line.
<ComponentPreview name="multi-select-badge-limit-wrap" />
### Deletable Badge
Add the `deletableBadge` prop on the `MultiSelectorTrigger` component to enable badge removal directly from the badge.
<ComponentPreview name="multi-select-deletable-badge" />
### Inline search input
Use `mode="inline-combobox"` on the `MultiSelectorTrigger` component to add an inline search input.
<ComponentPreview name="multi-select-inline-search-input" />
+1
View File
@@ -10,6 +10,7 @@
"start": "next start",
"lint": "next lint",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
@@ -0,0 +1,45 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([
'Apple',
'Banana',
'Cherry',
'Date',
'Elderberrie',
])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
persistLabel
badgeLimit="wrap"
showIcon={false}
deletableBadge={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -0,0 +1,48 @@
import { Minus, Plus } from 'lucide-react'
import { useState } from 'react'
import { Button } from 'ui'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple', 'Banana', 'Cherry'])
const [limit, setLimit] = useState(2)
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2">
<Button size="tiny" type="default" onClick={() => setLimit(limit - 1)} disabled={limit < 1}>
<Minus size={12} />
</Button>
<span className="text-sm text-foreground/90 peer-checked:line-through font-semibold hover:cursor-pointer">
Limit: {limit}
</span>
<Button size="tiny" type="default" onClick={() => setLimit(limit + 1)}>
<Plus size={12} />
</Button>
</div>
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit={limit} />
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
</div>
)
}
@@ -0,0 +1,34 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit="wrap" />
<MultiSelectorContent>
<MultiSelectorInput placeholder="Search fruits" showResetIcon />
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -0,0 +1,37 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple', 'Banana'])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
deletableBadge
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
@@ -2,25 +2,42 @@ import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [value, setValue] = useState<string[]>([])
const [selectedValues, setSelectedValues] = useState<string[]>([])
const fruits = [
{ value: 'Apple', isDisabled: false },
{ value: 'Banana', isDisabled: false },
{ value: 'Cherry', isDisabled: false },
{ value: 'Date', isDisabled: false },
{ value: 'Elderberrie', isDisabled: false },
{ value: 'Fig', isDisabled: false },
{ value: 'Grape', isDisabled: false },
{ value: 'Kiwi', isDisabled: true },
{ value: 'Mango', isDisabled: false },
{ value: 'Strawberry', isDisabled: false },
]
return (
<MultiSelector values={value} onValuesChange={setValue} size="small">
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select items" />
</MultiSelectorTrigger>
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
className="w-72"
label="Select fruits"
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="1">Item 1</MultiSelectorItem>
<MultiSelectorItem value="2">Item 2</MultiSelectorItem>
<MultiSelectorItem value="3">Item 3</MultiSelectorItem>
{fruits.map(({ value, isDisabled }) => (
<MultiSelectorItem key={value} value={value} disabled={isDisabled}>
{value}
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
@@ -0,0 +1,18 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues} disabled={true}>
<MultiSelectorTrigger className="w-72" label="Select fruits" />
</MultiSelector>
)
}
@@ -1,105 +0,0 @@
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import Image from 'next/image'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
import {
Button,
Form_Shadcn_,
FormDescription_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
FormMessage_Shadcn_,
} from 'ui'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
const form = z.object({
value: z.array(z.string()).nonempty('Please select at least one person'),
})
type Form = z.infer<typeof form>
const users = [
{
name: 'ThePrimeagen',
picture: 'https://pbs.twimg.com/profile_images/1759330620160049152/2i_wkOoK_400x400.jpg',
},
{
name: 'Shadcn',
picture: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg',
},
{
name: 'Theo',
picture: 'https://pbs.twimg.com/profile_images/1605762947686375425/lsoGWWty_400x400.jpg',
},
]
const MultiSelectZod = () => {
const multiForm = useForm<Form>({
resolver: zodResolver(form),
defaultValues: form.parse({ value: [users[0].name] }),
})
const onSubmit = (data: Form) => {
toast('Form submitted ', { description: JSON.stringify(data, null, 2) })
}
return (
<Form_Shadcn_ {...multiForm}>
<form onSubmit={multiForm.handleSubmit(onSubmit)} className="space-y-3 grid gap-3 w-full">
<FormField_Shadcn_
control={multiForm.control}
name="value"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormLabel_Shadcn_>Invite people</FormLabel_Shadcn_>
<MultiSelector onValuesChange={field.onChange} values={field.value} size={'small'}>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select people to invite" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{users.map((user) => (
<MultiSelectorItem key={user.name} value={user.name}>
<div className="flex items-center space-x-2">
<Image
src={user.picture}
alt={user.name}
width={32}
height={32}
className="w-8 h-8 rounded-full"
/>
<span>{user.name}</span>
</div>
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
<FormDescription_Shadcn_>
Select people to invite to this event
</FormDescription_Shadcn_>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
<Button type="secondary" size="medium" htmlType="submit">
Submit
</Button>
</form>
</Form_Shadcn_>
)
}
export default MultiSelectZod
@@ -0,0 +1,39 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>(['Apple'])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger
mode="inline-combobox"
className="w-72"
label="Select fruits"
deletableBadge
badgeLimit="wrap"
showIcon={false}
/>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
+27 -2
View File
@@ -1036,9 +1036,34 @@ export const examples: Registry = [
files: ['example/multi-select-demo.tsx'],
},
{
name: 'multi-select-form',
name: 'multi-select-disabled',
type: 'components:example',
files: ['example/multi-select-form.tsx'],
files: ['example/multi-select-disabled.tsx'],
},
{
name: 'multi-select-badge-limit-wrap',
type: 'components:example',
files: ['example/multi-select-badge-limit-wrap.tsx'],
},
{
name: 'multi-select-badge-limit',
type: 'components:example',
files: ['example/multi-select-badge-limit.tsx'],
},
{
name: 'multi-select-combobox',
type: 'components:example',
files: ['example/multi-select-combobox.tsx'],
},
{
name: 'multi-select-deletable-badge',
type: 'components:example',
files: ['example/multi-select-deletable-badge.tsx'],
},
{
name: 'multi-select-inline-search-input',
type: 'components:example',
files: ['example/multi-select-inline-search-input.tsx'],
},
{
name: 'tree-view-demo',
+1 -1
View File
@@ -14,7 +14,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE
## Local setup
[supabase.com/docs](https://supabase.com/docs) is a Next.JS site. You can get setup by following the same steps for all of our other Next.JS projects:
[supabase.com/docs](https://supabase.com/docs) is a Next.js site. You can get setup by following the same steps for all of our other Next.js projects:
1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md)
2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false`
+1 -1
View File
@@ -31,7 +31,7 @@ We use documentation specifications which can be used to generate human-readable
- CLISpec (custom to Supabase): for CLI commands and usage.
The benefit of using custom specifications is that we can generate many other types from a strict schema (eg, HTML and manpages).
It also means that we can switch to any documentation system we want. On this site we use Next.JS, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool.
It also means that we can switch to any documentation system we want. On this site we use Next.js, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool.
## Contributing
@@ -21,7 +21,7 @@ const AnalyticsConfigPage = async () => {
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/analytics/config.tsx'
'supabase/supabase/blob/master/apps/docs/app/guides/(with-sidebar)/self-hosting/analytics/config/page.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
@@ -21,7 +21,7 @@ const AuthConfigPage = async () => {
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/auth/config.tsx'
'supabase/supabase/blob/master/apps/docs/app/guides/(with-sidebar)/self-hosting/auth/config/page.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
@@ -21,7 +21,7 @@ const RealtimeConfigPage = async () => {
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/realtime/config.tsx'
'supabase/supabase/blob/master/apps/docs/app/guides/(with-sidebar)/self-hosting/realtime/config/page.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
@@ -21,7 +21,7 @@ const StorageConfigPage = async () => {
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/storage/config.tsx'
'supabase/supabase/blob/master/apps/docs/app/guides/(with-sidebar)/self-hosting/storage/config/page.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
+5 -58
View File
@@ -4,6 +4,7 @@ import { IconBackground, TextLink } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import { MIGRATION_PAGES } from '~/components/Navigation/NavigationMenu/NavigationMenu.constants'
import { GlassPanelWithIconPicker } from '~/features/ui/GlassPanelWithIconPicker'
import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker'
import HomeLayout from '~/layouts/HomeLayout'
@@ -70,60 +71,6 @@ const products = [
},
]
const migrationGuides = [
{
title: 'Auth0',
icon: '/docs/img/icons/auth0-icon',
href: '/guides/resources/migrating-to-supabase/auth0',
hasLightIcon: true,
},
{
title: 'Firebase Auth',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-auth',
},
{
title: 'Firestore Data',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firestore-data',
},
{
title: 'Firebase Storage',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-storage',
},
{
title: 'Heroku',
icon: '/docs/img/icons/heroku-icon',
href: '/guides/resources/migrating-to-supabase/heroku',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
},
{
title: 'Amazon RDS',
icon: '/docs/img/icons/aws-rds-icon',
href: '/guides/resources/migrating-to-supabase/amazon-rds',
},
{
title: 'Postgres',
icon: '/docs/img/icons/postgres-icon',
href: '/guides/resources/migrating-to-supabase/postgres',
},
{
title: 'MySQL',
icon: '/docs/img/icons/mysql-icon',
href: '/guides/resources/migrating-to-supabase/mysql',
},
{
title: 'MSSQL',
icon: '/docs/img/icons/mssql-icon',
href: '/guides/resources/migrating-to-supabase/mssql',
},
]
const selfHostingOptions = [
{
title: 'Auth',
@@ -268,11 +215,11 @@ const HomePage = () => (
</div>
<ul className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{migrationGuides.map((guide) => {
{MIGRATION_PAGES.map((guide) => {
return (
<li key={guide.title} className="col-span-6 md:col-span-4">
<Link href={guide.href} passHref>
<IconPanel {...guide} background={true} showLink={false} />
<li key={guide.name} className="col-span-6 md:col-span-4">
<Link href={guide.url} passHref>
<IconPanel {...guide} title={guide.name} background={true} showLink={false} />
</Link>
</li>
)
+1 -1
View File
@@ -68,7 +68,7 @@ export async function GET(request: Request) {
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = createClient()
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
@@ -1,3 +1,7 @@
import type { ComponentProps } from 'react'
import type { IconPanel } from 'ui-patterns/IconPanel'
import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types'
export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
@@ -959,10 +963,6 @@ export const database: NavMenuConstant = {
name: 'pg_net: Async Networking',
url: '/guides/database/extensions/pg_net',
},
{
name: 'pg_partman: Partition Maintenance',
url: '/guides/database/extensions/pg_partman',
},
{
name: 'pg_plan_filter: Restrict Total Cost',
url: '/guides/database/extensions/pg_plan_filter',
@@ -983,10 +983,6 @@ export const database: NavMenuConstant = {
name: 'PostGIS: Geo queries',
url: '/guides/database/extensions/postgis',
},
{
name: 'pgmq: Queues',
url: '/guides/database/extensions/pgmq',
},
{
name: 'pgsodium (pending deprecation): Encryption Features',
url: '/guides/database/extensions/pgsodium',
@@ -1809,23 +1805,70 @@ export const local_development: NavMenuConstant = {
],
}
export const MIGRATION_PAGES: Partial<NavMenuSection>[] = [
{ name: 'Amazon RDS', url: '/guides/platform/migrating-to-supabase/amazon-rds' },
{ name: 'Auth0', url: '/guides/platform/migrating-to-supabase/auth0' },
{ name: 'Firebase Auth', url: '/guides/platform/migrating-to-supabase/firebase-auth' },
export const MIGRATION_PAGES: Partial<NavMenuSection & ComponentProps<typeof IconPanel>>[] = [
{
name: 'Firebase Firestore',
name: 'Auth0',
icon: '/docs/img/icons/auth0-icon',
url: '/guides/platform/migrating-to-supabase/auth0',
hasLightIcon: true,
},
{
name: 'Firebase Auth',
icon: '/docs/img/icons/firebase-icon',
url: '/guides/platform/migrating-to-supabase/firebase-auth',
},
{
name: 'Firestore Data',
icon: '/docs/img/icons/firebase-icon',
url: '/guides/platform/migrating-to-supabase/firestore-data',
},
{
name: 'Firebase Storage',
icon: '/docs/img/icons/firebase-icon',
url: '/guides/platform/migrating-to-supabase/firebase-storage',
},
{ name: 'Heroku Postgres', url: '/guides/platform/migrating-to-supabase/heroku' },
{ name: 'MySQL', url: '/guides/platform/migrating-to-supabase/mysql' },
{ name: 'MSSQL', url: '/guides/platform/migrating-to-supabase/mssql' },
{ name: 'Postgres', url: '/guides/platform/migrating-to-supabase/postgres' },
{ name: 'Render', url: '/guides/platform/migrating-to-supabase/render' },
{
name: 'Heroku',
icon: '/docs/img/icons/heroku-icon',
url: '/guides/platform/migrating-to-supabase/heroku',
},
{
name: 'Render',
icon: '/docs/img/icons/render-icon',
url: '/guides/platform/migrating-to-supabase/render',
},
{
name: 'Amazon RDS',
icon: '/docs/img/icons/aws-rds-icon',
url: '/guides/platform/migrating-to-supabase/amazon-rds',
},
{
name: 'Postgres',
icon: '/docs/img/icons/postgres-icon',
url: '/guides/platform/migrating-to-supabase/postgres',
},
{
name: 'Vercel Postgres',
icon: '/docs/img/icons/vercel-icon',
url: '/guides/platform/migrating-to-supabase/vercel-postgres',
hasLightIcon: true,
},
{
name: 'Neon',
icon: '/docs/img/icons/neon-icon',
url: '/guides/platform/migrating-to-supabase/neon',
hasLightIcon: true,
},
{
name: 'MySQL',
icon: '/docs/img/icons/mysql-icon',
url: '/guides/platform/migrating-to-supabase/mysql',
},
{
name: 'MSSQL',
icon: '/docs/img/icons/mssql-icon',
url: '/guides/platform/migrating-to-supabase/mssql',
},
]
export const platform: NavMenuConstant = {
@@ -20,7 +20,7 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
size="medium"
className="text-foreground-light border-b mt-8 pb-2"
>
<AccordionItem
header="See legacy docs"
id="legacy-docs"
@@ -218,7 +218,7 @@ export async function GET(request: NextRequest) {
const code = requestUrl.searchParams.get('code')
if (code) {
const cookieStore = cookies()
const cookieStore = await cookies()
const supabase = createRouteHandlerClient<Database>({ cookies: () => cookieStore })
await supabase.auth.exchangeCodeForSession(code)
}
@@ -6,6 +6,17 @@ subtitle: 'Use a custom email provider to send authentication messages'
The Send Email Hook runs before an email is sent and allows for flexibility around email sending. You can use this hook to configure a back-up email provider or add internationalization to your emails.
## Email Sending Behavior
Email sending depends on two settings: Email Provider and Auth Hook status.
| Email Provider | Auth Hook | Result |
| -------------- | --------- | -------------------------------------------------------------------- |
| Enabled | Enabled | Auth Hook handles email sending (SMTP not used) |
| Enabled | Disabled | SMTP handles email sending (custom if configured, default otherwise) |
| Disabled | Enabled | Email Signups Disabled |
| Disabled | Disabled | Email Signups Disabled |
**Inputs**
| Field | Type | Description |
@@ -630,6 +641,11 @@ const templates = {
},
}
function generateConfirmationURL(email_data) {
// TODO: replace the ref with your project ref
return `https://<ref>.supabase.co/auth/v1/verify?token=${email_data.token}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}`
}
Deno.serve(async (req) => {
const payload = await req.text()
const serverToken = Deno.env.get('POSTMARK_SERVER_TOKEN')
@@ -642,8 +658,9 @@ Deno.serve(async (req) => {
const subject = subjects[language][email_data.email_action_type] || 'Notification'
let template = templates[language][email_data.email_action_type]
const confirmation_url = generateConfirmationURL(email_data)
let htmlBody = template
.replace('{{confirmation_url}}', email_data.confirmation_url)
.replace('{{confirmation_url}}', confirmation_url)
.replace('{{token}}', email_data.token || '')
.replace('{{new_token}}', email_data.new_token || '')
.replace('{{site_url}}', email_data.site_url || '')
@@ -736,10 +753,16 @@ const templates = {
reauthentication: `<h2>Confirm reauthentication</h2><p>Enter the code: {{token}}</p>`
};
function generateConfirmationURL(email_data) {
// TODO: replace the ref with your project ref
return `https://<ref>.supabase.co/auth/v1/verify?token=${email_data.token}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}`
}
async function sendEmailWithPostmark(user: any, email_data: any, serverToken: string): Promise<Response> {
const subject = subjects[email_data.email_action_type] || 'Notification';
const confirmation_url = generateConfirmationURL(email_data)
let template = templates[email_data.email_action_type];
let htmlBody = template.replace('{{confirmation_url}}', email_data.confirmation_url)
let htmlBody = template.replace('{{confirmation_url}}', confirmation_url)
.replace('{{token}}', email_data.token || '')
.replace('{{new_token}}', email_data.new_token || '')
.replace('{{site_url}}', email_data.site_url || '')
@@ -767,7 +790,8 @@ async function sendEmailWithPostmark(user: any, email_data: any, serverToken: st
async function sendEmailWithSendGrid(user: any, email_data: any, apiKey: string): Promise<Response> {
const subject = subjects[email_data.email_action_type] || 'Notification';
let template = templates[email_data.email_action_type];
let htmlBody = template.replace('{{confirmation_url}}', email_data.confirmation_url)
cont confirmation_url = generateConfirmationURL(email_data)
let htmlBody = template.replace('{{confirmation_url}}', confirmation_url)
.replace('{{token}}', email_data.token || '')
.replace('{{new_token}}', email_data.new_token || '')
.replace('{{site_url}}', email_data.site_url || '')
+2 -2
View File
@@ -184,7 +184,7 @@ export async function GET(request: NextRequest) {
redirectTo.pathname = next
if (token_hash && type) {
const supabase = createClient()
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
@@ -648,7 +648,7 @@ export async function GET(request: NextRequest) {
redirectTo.pathname = next
if (token_hash && type) {
const supabase = createClient()
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
@@ -72,9 +72,7 @@ hideToc: true
Start the development server, go to http://localhost:3000 in a browser, and you should see the contents of `app/page.tsx`.
To Sign Up a new user, navigate to http://localhost:3000/login, and click `Sign Up Now`.
Check out the [Supabase Auth docs](https://supabase.com/docs/guides/auth#authentication) for more authentication methods.
To sign up a new user, navigate to http://localhost:3000/login, and click `Sign Up Now`.
</StepHikeCompact.Details>
@@ -88,3 +86,8 @@ hideToc: true
</StepHikeCompact.Step>
</StepHikeCompact>
## Learn more
- [Setting up Server-Side Auth for Next.js](https://supabase.com/docs/guides/auth/server-side/nextjs) for a Next.js deep dive
- [Supabase Auth docs](https://supabase.com/docs/guides/auth#authentication) for more Supabase authentication methods
@@ -171,8 +171,8 @@ export function createClient() {
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -125,8 +125,8 @@ export function createClient() {
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -337,7 +337,7 @@ import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export async function login(formData: FormData) {
const supabase = createClient()
const supabase = await createClient()
// type-casting here for convenience
// in practice, you should validate your inputs
@@ -357,7 +357,7 @@ export async function login(formData: FormData) {
}
export async function signup(formData: FormData) {
const supabase = createClient()
const supabase = await createClient()
// type-casting here for convenience
// in practice, you should validate your inputs
@@ -429,7 +429,7 @@ export async function GET(request: NextRequest) {
const next = searchParams.get('next') ?? '/'
if (token_hash && type) {
const supabase = createClient()
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
@@ -482,7 +482,7 @@ import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export default async function PrivatePage() {
const supabase = createClient()
const supabase = await createClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
@@ -77,6 +77,20 @@ Future<void> signInWithGithub() async {
}
```
</TabPanel>
<TabPanel id="swift" label="Swift">
When your user signs in, call [signInWithWithOAuth](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`:
```swift
func signInWithGithub() async throws {
try await supabase.auth.signInWithOAuth(
provider: .github,
redirectTo: URL(string: "my-custom-scheme://my-app-host")
)
}
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
@@ -341,9 +341,9 @@ Most web apps and websites can utilize Google's [personalized sign-in buttons](h
// Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method
```
### One-tap with NextJS
### One-tap with Next.js
If you're integrating Google One-Tap with your NextJS application, you can refer to the example below to get started:
If you're integrating Google One-Tap with your Next.js application, you can refer to the example below to get started:
```tsx
'use client'
@@ -128,7 +128,7 @@ If you'd like us to add a new quickstart guide, share your [feedback](https://su
- [Prisma](/docs/guides/database/prisma)
- [Drizzle](/docs/guides/database/drizzle)
- [Postgres.js](/docs/guides/database/drizzle)
- [Postgres.js](/docs/guides/database/postgres-js)
### External tools
@@ -160,6 +160,6 @@ alter role "<role_name>" set "<setting_name>" to default;
### Considerations
1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds.
1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you’re making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won’t see those changes take effect until a restart is manually triggered.
1. Custom Postgres Config will always override the default optimizations generated by Supabase. When changing compute add-ons, you should also review and update your custom Postgres Config to ensure they remain compatible and effective with the updated compute.
1. Some parameters (e.g. `wal_keep_size`) can increase disk utilization, triggering disk expansion, which in turn can lead to [increases in your bill](/docs/guides/platform/compute-add-ons#disk-io).
@@ -309,9 +309,9 @@ order by "created" desc;
## Configuration
<Admonition type="note" label="Only Self-Hosted Deployments can Modify Configuration Variables">
<Admonition type="note" label="Must be on pg_net v0.10.0 or above to reconfigure ">
See the [self-hosting docs](/docs/guides/hosting/overview#configuration) for self-hosting instructions.
Supabase supports reconfiguring pg*net starting from v0.10.0+. For the latest release, initiate a postgres upgrade in the [Infrastructure Settings](https://supabase.com/dashboard/project/*/settings/infrastructure).
</Admonition>
@@ -329,18 +329,11 @@ where "name" like 'pg_net%';
### Alter settings
Note, that doing `ALTER SYSTEM` requires SUPERUSER but on PostgreSQL >= 15, you can do:
Change variables:
```sql
grant alter system on parameter pg_net.ttl to <role>;
grant alter system on parameter pg_net.batch_size to <role>;
```
Then variables can be changed:
```sql
alter system set pg_net.ttl to '1 hour'
alter system set pg_net.batch_size to 500;
alter role "postgres" set pg_net.ttl to '24 hour';
alter role "postgres" set pg_net.batch_size to 500;
```
Then reload the settings and restart the `pg_net` background worker with:
@@ -1,100 +0,0 @@
---
id: 'pg_partman'
title: 'pg_partman: partition management'
description: 'Automated partition management'
---
[`pg_partman`](https://github.com/pgpartman/pg_partman) is a PostgreSQL extension that enables automated partition management. It removes the need to create and detach partitions for tables partitioned by time or an integer series.
## Enable the extension
To enable `pg_partman`, first enable the extension in your database inside a dedicated schema.
{/* prettier-ignore */}
```sql
create schema partman;
create extension pg_partman schema partman;
```
## Setting up Partitioning
pg_partman supports partitioning based on time (e.g., by day, month, or year) or by a sequential integer. Setup is performed using the `create_parent` function.
For time-based partitioning:
{/* prettier-ignore */}
```sql
select partman.create_parent('schema_name.table_name', 'partition_column', 'time', 'daily');
```
or for integer-based partitioning:
{/* prettier-ignore */}
```sql
select partman.create_parent('schema_name.table_name', 'partition_column', 'integer', '1000');
```
## API
- partman.create_parent: creates a parent table for partitioning
- partman.run_maintenance: ensures partitions are created and dropped as per the defined settings. This should be run periodically, such as via a cron job scheduled by pg_cron
## Example
Let’s set up a simple partitioning example using pg_partman with time-based partitions.
{/* prettier-ignore */}
```sql
-- Create the table
create table public.foo (
id serial primary key,
data text,
created_at timestamp not null
);
-- Create the parent table
select
partman.create_parent(
'public.foo',
'created_at',
'time',
'daily'
);
-- Configure the partitioned table
update
partman.part_config
set
infinite_time_partitions = true,
retention = '3 months',
retention_keep_table=true
where
parent_table = 'public.foo';
```
For more information about available configuration, see the [pg_partman docs](https://github.com/pgpartman/pg_partman/blob/de93f4a104b8566087f4b9b2a0edf25893672da8/doc/pg_partman.md#configuration-tables).
Following these statements, partitions for `public.foo` will be automatically managed as necessary when the `partman.run_maintenance()` function is called.
## Running Maintenance
It is important to call the `partman.run_maintenance()` procedure frequently to ensure partitions are created and detached according to the partitioned table's configuration.
{/* prettier-ignore */}
```sql
call partman.run_maintenance_proc();
```
To automate this task, consider using `pg_cron`.
{/* prettier-ignore */}
```sql
create extension pg_cron;
select
cron.schedule('@hourly', $$call partman.run_maintenance_proc()$$);
```
## Resources
- Official [pg_partman documentation](https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman_howto.md)
@@ -1,707 +0,0 @@
---
id: 'pgmq'
title: 'pgmq: Queues'
description: 'pgmq: Managed queues in Postgres'
---
pgmq is a lightweight message queue built on Postgres.
## Features
- Lightweight - No background worker or external dependencies, just Postgres functions packaged in an extension
- "exactly once" delivery of messages to a consumer within a visibility timeout
- API parity with AWS SQS and RSMQ
- Messages stay in the queue until explicitly removed
- Messages can be archived, instead of deleted, for long-term retention and replayability
## Enable the extension
```sql
create extension pgmq;
```
## Usage [#get-usage]
### Sending Messages
#### send
Send a single message to a queue.
{/* prettier-ignore */}
```sql
pgmq.send(
queue_name text,
msg jsonb,
delay integer default 0
)
returns setof bigint
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------ | :----------------------------------------------------------------- |
| queue_name | text | The name of the queue |
| msg | jsonb | The message to send to the queue |
| delay | integer | Time in seconds before the message becomes visible. Defaults to 0. |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.send('my_queue', '{"hello": "world"}');
send
------
4
```
---
#### send_batch
Send 1 or more messages to a queue.
{/* prettier-ignore */}
```sql
pgmq.send_batch(
queue_name text,
msgs jsonb[],
delay integer default 0
)
returns setof bigint
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------ | :------------------------------------------------------------------ |
| queue_name | text | The name of the queue |
| msgs | jsonb[] | Array of messages to send to the queue |
| delay | integer | Time in seconds before the messages becomes visible. Defaults to 0. |
{/* prettier-ignore */}
```sql
select * from pgmq.send_batch(
'my_queue',
array[
'{"hello": "world_0"}'::jsonb,
'{"hello": "world_1"}'::jsonb
]
);
send_batch
------------
1
2
```
---
### Reading Messages
#### read
Read 1 or more messages from a queue. The VT specifies the delay in seconds between reading and the message becoming invisible to other consumers.
{/* prettier-ignore */}
```sql
pgmq.read(
queue_name text,
vt integer,
qty integer
)
returns setof pgmq.message_record
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------ | :-------------------------------------------------------------- |
| queue_name | text | The name of the queue |
| vt | integer | Time in seconds that the message become invisible after reading |
| qty | integer | The number of messages to read from the queue. Defaults to 1 |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.read('my_queue', 10, 2);
msg_id | read_ct | enqueued_at | vt | message
--------+---------+-------------------------------+-------------------------------+----------------------
1 | 1 | 2023-10-28 19:14:47.356595-05 | 2023-10-28 19:17:08.608922-05 | {"hello": "world_0"}
2 | 1 | 2023-10-28 19:14:47.356595-05 | 2023-10-28 19:17:08.608974-05 | {"hello": "world_1"}
(2 rows)
```
---
#### read_with_poll
Same as read(). Also provides convenient long-poll functionality.
When there are no messages in the queue, the function call will wait for `max_poll_seconds` in duration before returning.
If messages reach the queue during that duration, they will be read and returned immediately.
{/* prettier-ignore */}
```sql
pgmq.read_with_poll(
queue_name text,
vt integer,
qty integer,
max_poll_seconds integer default 5,
poll_interval_ms integer default 100
)
returns setof pgmq.message_record
```
**Parameters:**
| Parameter | Type | Description |
| :--------------- | :------ | :-------------------------------------------------------------------------- |
| queue_name | text | The name of the queue |
| vt | integer | Time in seconds that the message become invisible after reading. |
| qty | integer | The number of messages to read from the queue. Defaults to 1. |
| max_poll_seconds | integer | Time in seconds to wait for new messages to reach the queue. Defaults to 5. |
| poll_interval_ms | integer | Milliseconds between the internal poll operations. Defaults to 100. |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.read_with_poll('my_queue', 1, 1, 5, 100);
msg_id | read_ct | enqueued_at | vt | message
--------+---------+-------------------------------+-------------------------------+--------------------
1 | 1 | 2023-10-28 19:09:09.177756-05 | 2023-10-28 19:27:00.337929-05 | {"hello": "world"}
```
---
#### pop
Reads a single message from a queue and deletes it upon read.
Note: utilization of pop() results in at-most-once delivery semantics if the consuming application does not guarantee processing of the message.
{/* prettier-ignore */}
```sql
pgmq.pop(queue_name text)
returns setof pgmq.message_record
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
{/* prettier-ignore */}
```sql
pgmq=# select * from pgmq.pop('my_queue');
msg_id | read_ct | enqueued_at | vt | message
--------+---------+-------------------------------+-------------------------------+--------------------
1 | 2 | 2023-10-28 19:09:09.177756-05 | 2023-10-28 19:27:00.337929-05 | {"hello": "world"}
```
---
### Deleting/Archiving Messages
#### delete (single)
Deletes a single message from a queue.
{/* prettier-ignore */}
```sql
pgmq.delete (queue_name text, msg_id: bigint)
returns boolean
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :----- | :---------------------------------- |
| queue_name | text | The name of the queue |
| msg_id | bigint | Message ID of the message to delete |
Example:
{/* prettier-ignore */}
```sql
select pgmq.delete('my_queue', 5);
delete
--------
t
```
---
#### delete (batch)
Delete one or many messages from a queue.
{/* prettier-ignore */}
```sql
pgmq.delete (queue_name text, msg_ids: bigint[])
returns setof bigint
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------- | :----------------------------- |
| queue_name | text | The name of the queue |
| msg_ids | bigint[] | Array of message IDs to delete |
Examples:
Delete two messages that exist.
{/* prettier-ignore */}
```sql
select * from pgmq.delete('my_queue', array[2, 3]);
delete
--------
2
3
```
Delete two messages, one that exists and one that does not. Message `999` does not exist.
```sql
select * from pgmq.delete('my_queue', array[6, 999]);
delete
--------
6
```
---
#### purge_queue
Permanently deletes all messages in a queue. Returns the number of messages that were deleted.
```text
purge_queue(queue_name text)
returns bigint
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
Purge the queue when it contains 8 messages;
{/* prettier-ignore */}
```sql
select * from pgmq.purge_queue('my_queue');
purge_queue
-------------
8
```
---
#### archive (single)
Removes a single requested message from the specified queue and inserts it into the queue's archive.
{/* prettier-ignore */}
```sql
pgmq.archive(queue_name text, msg_id bigint)
returns boolean
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :----- | :----------------------------------- |
| queue_name | text | The name of the queue |
| msg_id | bigint | Message ID of the message to archive |
Returns
Boolean value indicating success or failure of the operation.
Example; remove message with ID 1 from queue `my_queue` and archive it:
{/* prettier-ignore */}
```sql
select * from pgmq.archive('my_queue', 1);
archive
---------
t
```
---
#### archive (batch)
Deletes a batch of requested messages from the specified queue and inserts them into the queue's archive.
Returns an array of message ids that were successfully archived.
```text
pgmq.archive(queue_name text, msg_ids bigint[])
RETURNS SETOF bigint
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------- | :------------------------------ |
| queue_name | text | The name of the queue |
| msg_ids | bigint[] | Array of message IDs to archive |
Examples:
Delete messages with ID 1 and 2 from queue `my_queue` and move to the archive.
{/* prettier-ignore */}
```sql
select * from pgmq.archive('my_queue', array[1, 2]);
archive
---------
1
2
```
Delete messages 4, which exists and 999, which does not exist.
{/* prettier-ignore */}
```sql
select * from pgmq.archive('my_queue', array[4, 999]);
archive
---------
4
```
---
### Queue Management
#### create
Create a new queue.
{/* prettier-ignore */}
```sql
pgmq.create(queue_name text)
returns void
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
{/* prettier-ignore */}
```sql
select from pgmq.create('my_queue');
create
--------
```
---
#### create_partitioned
Create a partitioned queue.
{/* prettier-ignore */}
```sql
pgmq.create_partitioned (
queue-ue_name text,
partition_interval text default '10000'::text,
retention_interval text default '100000'::text
)
returns void
```
**Parameters:**
| Parameter | Type | Description |
| :----------------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
| partition_interval | text | The name of the queue |
| retention_interval | text | The name of the queue |
Example:
Create a queue with 100,000 messages per partition, and will retain 10,000,000 messages on old partitions. Partitions greater than this will be deleted.
{/* prettier-ignore */}
```sql
select * from pgmq.create_partitioned(
'my_partitioned_queue',
'100000',
'10000000'
);
create_partitioned
--------------------
```
---
#### create_unlogged
Creates an unlogged table. This is useful when write throughput is more important that durability.
See Postgres documentation for [unlogged tables](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED) for more information.
{/* prettier-ignore */}
```sql
pgmq.create_unlogged(queue_name text)
returns void
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
{/* prettier-ignore */}
```sql
select pgmq.create_unlogged('my_unlogged');
create_unlogged
-----------------
```
---
#### detach_archive
Drop the queue's archive table as a member of the PGMQ extension. Useful for preventing the queue's archive table from being drop when `drop extension pgmq` is executed.
This does not prevent the further archives() from appending to the archive table.
{/* prettier-ignore */}
```sql
pgmq.detach_archive(queue_name text)
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.detach_archive('my_queue');
detach_archive
----------------
```
---
#### drop_queue
Deletes a queue and its archive table.
{/* prettier-ignore */}
```sql
pgmq.drop_queue(queue_name text)
returns boolean
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.drop_queue('my_unlogged');
drop_queue
------------
t
```
### Utilities
#### set_vt
Sets the visibility timeout of a message to a specified time duration in the future. Returns the record of the message that was updated.
{/* prettier-ignore */}
```sql
pgmq.set_vt(
queue_name text,
msg_id bigint,
vt_offset integer
)
returns pgmq.message_record
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :------ | :-------------------------------------------------------------------- |
| queue_name | text | The name of the queue |
| msg_id | bigint | ID of the message to set visibility time |
| vt_offset | integer | Duration from now, in seconds, that the message's VT should be set to |
Example:
Set the visibility timeout of message 1 to 30 seconds from now.
```sql
select * from pgmq.set_vt('my_queue', 11, 30);
msg_id | read_ct | enqueued_at | vt | message
--------+---------+-------------------------------+-------------------------------+----------------------
1 | 0 | 2023-10-28 19:42:21.778741-05 | 2023-10-28 19:59:34.286462-05 | {"hello": "world_0"}
```
---
#### list_queues
List all the queues that currently exist.
{/* prettier-ignore */}
```sql
list_queues()
RETURNS TABLE(
queue_name text,
created_at timestamp with time zone,
is_partitioned boolean,
is_unlogged boolean
)
```
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.list_queues();
queue_name | created_at | is_partitioned | is_unlogged
----------------------+-------------------------------+----------------+-------------
my_queue | 2023-10-28 14:13:17.092576-05 | f | f
my_partitioned_queue | 2023-10-28 19:47:37.098692-05 | t | f
my_unlogged | 2023-10-28 20:02:30.976109-05 | f | t
```
---
#### metrics
Get metrics for a specific queue.
{/* prettier-ignore */}
```sql
pgmq.metrics(queue_name: text)
returns table(
queue_name text,
queue_length bigint,
newest_msg_age_sec integer,
oldest_msg_age_sec integer,
total_messages bigint,
scrape_time timestamp with time zone
)
```
**Parameters:**
| Parameter | Type | Description |
| :--------- | :--- | :-------------------- |
| queue_name | text | The name of the queue |
**Returns:**
| Attribute | Type | Description |
| :----------------- | :----------------------- | :------------------------------------------------------------------------ |
| queue_name | text | The name of the queue |
| queue_length | bigint | Number of messages currently in the queue |
| newest_msg_age_sec | integer \| null | Age of the newest message in the queue, in seconds |
| oldest_msg_age_sec | integer \| null | Age of the oldest message in the queue, in seconds |
| total_messages | bigint | Total number of messages that have passed through the queue over all time |
| scrape_time | timestamp with time zone | The current timestamp |
Example:
{/* prettier-ignore */}
```sql
select * from pgmq.metrics('my_queue');
queue_name | queue_length | newest_msg_age_sec | oldest_msg_age_sec | total_messages | scrape_time
------------+--------------+--------------------+--------------------+----------------+-------------------------------
my_queue | 16 | 2445 | 2447 | 35 | 2023-10-28 20:23:08.406259-05
```
---
#### metrics_all
Get metrics for all existing queues.
```text
pgmq.metrics_all()
RETURNS TABLE(
queue_name text,
queue_length bigint,
newest_msg_age_sec integer,
oldest_msg_age_sec integer,
total_messages bigint,
scrape_time timestamp with time zone
)
```
**Returns:**
| Attribute | Type | Description |
| :----------------- | :----------------------- | :------------------------------------------------------------------------ |
| queue_name | text | The name of the queue |
| queue_length | bigint | Number of messages currently in the queue |
| newest_msg_age_sec | integer \| null | Age of the newest message in the queue, in seconds |
| oldest_msg_age_sec | integer \| null | Age of the oldest message in the queue, in seconds |
| total_messages | bigint | Total number of messages that have passed through the queue over all time |
| scrape_time | timestamp with time zone | The current timestamp |
{/* prettier-ignore */}
```sql
select * from pgmq.metrics_all();
queue_name | queue_length | newest_msg_age_sec | oldest_msg_age_sec | total_messages | scrape_time
----------------------+--------------+--------------------+--------------------+----------------+-------------------------------
my_queue | 16 | 2563 | 2565 | 35 | 2023-10-28 20:25:07.016413-05
my_partitioned_queue | 1 | 11 | 11 | 1 | 2023-10-28 20:25:07.016413-05
my_unlogged | 1 | 3 | 3 | 1 | 2023-10-28 20:25:07.016413-05
```
### Types
#### message_record
The complete representation of a message in a queue.
| Attribute Name | Type | Description |
| :------------- | :----------------------- | :--------------------------------------------------------------------- |
| msg_id | bigint | Unique ID of the message |
| read_ct | bigint | Number of times the message has been read. Increments on read(). |
| enqueued_at | timestamp with time zone | time that the message was inserted into the queue |
| vt | timestamp with time zone | Timestamp when the message will become available for consumers to read |
| message | jsonb | The message payload |
Example:
{/* prettier-ignore */}
```sql
msg_id | read_ct | enqueued_at | vt | message
--------+---------+-------------------------------+-------------------------------+--------------------
1 | 1 | 2023-10-28 19:06:19.941509-05 | 2023-10-28 19:06:27.419392-05 | {"hello": "world"}
```
## Resources
- Official Docs: [pgmq/api](https://tembo.io/pgmq/#creating-a-queue)
@@ -14,8 +14,6 @@ subtitle: "Limits applied Edge Functions in Supabase's hosted platform."
- Maximum Function Size (after bundling via CLI): 10MB
- Maximum log message length: 10,000 characters
- Log event threshold: 100 events per 10 seconds
- Max Request: 5GB
- Max Response: No limit
## Other limits & restrictions
@@ -8,7 +8,7 @@ hideToc: true
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create a Supabase project">
Go to [database.new](https://database.new) and create a new Supabase project.
@@ -118,7 +118,7 @@ hideToc: true
import { createClient } from '@/utils/supabase/server';
export default async function Notes() {
const supabase = createClient();
const supabase = await createClient();
const { data: notes } = await supabase.from("notes").select();
return <pre>{JSON.stringify(notes, null, 2)}</pre>
@@ -129,8 +129,8 @@ hideToc: true
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -385,7 +385,7 @@ import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export async function login(formData) {
const supabase = createClient()
const supabase = await createClient()
// type-casting here for convenience
// in practice, you should validate your inputs
@@ -405,7 +405,7 @@ export async function login(formData) {
}
export async function signup(formData) {
const supabase = createClient()
const supabase = await createClient()
const data = {
email: formData.get('email'),
@@ -512,7 +512,7 @@ export async function GET(request) {
redirectTo.searchParams.delete('type')
if (token_hash && type) {
const supabase = createClient()
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
@@ -720,7 +720,7 @@ import AccountForm from './account-form'
import { createClient } from '@/utils/supabase/server'
export default async function Account() {
const supabase = createClient()
const supabase = await createClient()
const {
data: { user },
@@ -7,7 +7,7 @@ description: 'Filter logs with regular expressions'
## Understanding field references
The log tables are queried with subset of BigQuery SQL syntax. They all have three columns: `event_message`, `timestamp`, and `metadata`.
The log tables are queried with a subset of BigQuery SQL syntax. They all have three columns: `event_message`, `timestamp`, and `metadata`.
| column | description |
| ------------- | --------------------------- |
@@ -154,6 +154,10 @@ Any changes made to your billing details will only be reflected in your upcoming
We accept credit card payments only. If you cannot pay via credit card, we do offer alternatives for larger upfront payments. Please create a [support ticket](https://supabase.help) in case you’re interested.
#### What credit card brands are supported?
Visa, Mastercard, American Express, Japan Credit Bureau (JCB), China UnionPay (CUP), Cartes Bancaires
#### What currency can I pay in?
All our invoices are issued in USD, but you can pay in any currency as long as your credit card provider allows charging in USD after conversion.
@@ -1,19 +1,18 @@
---
id: 'database-size'
title: 'Database size'
id: 'disk-metrics'
title: 'Understanding Database and Disk Size'
description: 'Understanding how database size applies to your subscription.'
---
Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your organization's [billing usage](https://supabase.com/dashboard/org/_/usage) and is updated daily. As you read this document we will refer to "database size" and "disk size":
Disk metrics refer to the storage usage reported by Postgres. These metrics are updated daily. As you read through this document, we will refer to "database size" and "disk size":
- "Database size" is the total size of used storage from your database.
- "Disk Storage size" describes the size of the underlying available storage.
- _Database size_: Displays the actual size of the data within your Postgres database. This can be found on the [Database Reports page](https://supabase.com/dashboard/project/_/reports/database).
## Database space management
- _Disk size_: Shows the overall disk space usage, which includes both the database size and additional files required for Postgres to function like the Write Ahead Log (WAL) and other system log files. You can view this on the [Database Settings page](https://supabase.com/dashboard/project/_/settings/database).
### Database size
## Database size
This SQL query will show the current size of your Postgres database:
This SQL query will show the size of all databases in your Postgres cluster:
```sql
select
@@ -23,7 +22,7 @@ from pg_database;
This value is reported in the [database report page](https://supabase.com/dashboard/project/_/reports/database).
Database Space is consumed primarily by your data, indexes, and materialized views. You can reduce your disk size by removing any of these and running a Vacuum operation.
Database size is consumed primarily by your data, indexes, and materialized views. You can reduce your database size by removing any of these and running a Vacuum operation.
<Admonition type="note">
@@ -69,39 +68,37 @@ Running a manual vacuum after deleting large amounts of data from your DB could
New Supabase projects have a database size of ~40-60mb. This space includes pre-installed extensions, schemas, and default Postgres data. Additional database size is used when installing extensions, even if those extensions are inactive.
## Disk management
## Disk size
Supabase uses network-attached storage to balance performance with scalability. The behavior of your disk depends on your billing plan.
Supabase uses network-attached storage to balance performance with scalability. The disk scaling behavior depends on your billing plan.
### Paid plan behavior
Projects on the Pro Plan and above have auto-scaling Disk Storage.
Projects on the Pro Plan and higher have auto-scaling disks.
Disk Storage expands automatically when the database reaches 90% of the disk size. The disk is expanded to be 50% larger (e.g., 8GB -> 12GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
Disk size expands automatically when the database reaches 90% of the allocated disk size. The disk is expanded to be 50% larger (for example, 8 GB -> 12 GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
<Admonition type="note">
The automatic resize operation will add an additional 50% capped to a maximum of 200GB. If 50% of your current usage is more than 200GB then only 200GB will be added to your disk (e.g. a size of 1500GB will resize to 1700GB).
The automatic resize operation will add an additional 50% capped to a maximum of 200 GB. If 50% of your current usage is more than 200 GB then only 200 GB will be added to your disk (for example a size of 1500 GB will resize to 1700 GB).
</Admonition>
The Disk Storage Size can also be manually expanded on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). The maximum default you can expand the Disk Storage to is 200GB. If you wish to manually expand it to any more than 200GB, please [contact us](https://supabase.com/dashboard/support/new) to discuss expanding the 200GB limit.
Disk size can also be manually expanded on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). The maximum disk size for the Pro/Team Plan is 60 TB. If you need more than this, [contact us](https://forms.supabase.com/enterprise) to learn more about the Enterprise Plan.
<Admonition type="note">
You may want to import a lot of data into your database which requires multiple disk expansions; for example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). If so, it is highly recommended you increase the Disk Storage Size manually on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database).
You may want to import a lot of data into your database which requires multiple disk expansions. for example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). If so, it is highly recommended you increase the disk size manually on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database).
Due to provider restrictions, disk expansions can occur only once every six hours. During the six-hour window, the disk cannot be resized again.
Due to restrictions on the underlying cloud provider, disk expansions can occur only once every six hours. During the six hour cool down window, the disk cannot be resized again.
</Admonition>
The maximum Disk Storage Size for the Pro/Team Plan is 60TB. If you need more than this, [contact us](https://forms.supabase.com/enterprise) to learn more about the Enterprise Plan.
### Free Plan behavior
Free Plan projects enter [read-only](#read-only-mode) mode when you exceed the 500MB limit. Once in read-only mode, you have several options:
Free Plan projects enter [read-only](#read-only-mode) mode when you exceed the 500 MB limit. Once in read-only mode, you have these options:
- [Upgrade to the Pro Plan](https://supabase.com/dashboard/org/_/billing) to increase the limit to 8GB. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale and expand beyond the 8GB database size limit.
- [Upgrade to the Pro Plan](https://supabase.com/dashboard/org/_/billing) to increase the limit to 8 GB. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale beyond the 8 GB disk size limit.
- [Disable read-only mode](#disabling-read-only-mode) and reduce your database size.
### Read-only mode
@@ -134,6 +131,6 @@ set default_transaction_read_only = 'off';
### Reducing disk size
Disks don't automatically downsize during normal operation. Once you have [reduced your database space usage](/docs/guides/platform/database-size#database-space-management), they _will_ automatically "right-size" when a Supabase (or Postgres) update is applied. Their final size after the update is 1.2x the size of the database (min 8 GB). For example, if your database is 100GB in size, and you have a 200GB disk, the size after a Supabase update will be 120GB.
Disks don't automatically downsize during normal operation. Once you have [reduced your database size](/docs/guides/platform/database-size#database-size), they _will_ automatically "right-size" during a [project upgrade](/docs/guides/platform/upgrading). The final disk size after the upgrade is 1.2x the size of the database with a minimum of 8 GB. For example, if your database size is 100GB, and you have a 200GB disk, the size after a project upgrade will be 120 GB.
Learn more about [upgrading your project](/docs/guides/platform/migrating-and-upgrading-projects#upgrade-your-project).
In the event that your project is already on the latest version of Postgres and cannot be upgraded, a new version of Postgres will be released approximately every week which you can then upgrade to once it becomes available.
@@ -11,7 +11,13 @@ Learn how to migrate to Supabase from another database service.
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-6 mb-6 not-prose">
{migrationPages.map((page) => (
<Link href={`${page.url}`} key={page.url} passHref>
<GlassPanel title={page.name} background={false} className="[&>div]:p-4" />
<GlassPanel
icon={page.icon}
title={page.name}
hasLightIcon={page.hasLightIcon}
background={false}
className="[&>div]:p-4"
/>
</Link>
))}
</div>
@@ -0,0 +1,90 @@
---
title: 'Migrate from Neon to Supabase'
description: 'Migrate your existing Neon database to Supabase.'
subtitle: 'Migrate your existing Neon database to Supabase.'
---
This guide demonstrates how to migrate your Neon database to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
## Retrieve your Neon database credentials [#retrieve-credentials]
1. Log in to your Neon Console [https://console.neon.tech/login](https://console.neon.tech/login).
1. Select **Projects** on the left.
1. Click on your project in the list.
1. From your Project Dashboard find your **Connection string** and click **Copy snippet** to copy it to the clipboard (do not check "pooled connection").
Example:
```bash
postgresql://neondb_owner:xxxxxxxxxxxxxxx-random-word-yyyyyyyy.us-west-2.aws.neon.tech/neondb?sslmode=require
```
## Set your OLD_DB_URL environment variable
Set the **OLD_DB_URL** environment variable at the command line using your Neon database credentials from the clipboard.
Example:
```bash
export OLD_DB_URL="postgresql://neondb_owner:xxxxxxxxxxxxxxx-random-word-yyyyyyyy.us-west-2.aws.neon.tech/neondb?sslmode=require"
```
## Retrieve your Supabase connection string [#retrieve-supabase-connection-string]
1. If you're new to Supabase, [create a project](https://supabase.com/dashboard).
Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database).
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection string**, select **URI**, make sure **Display connection pooler** is checked, and **Mode: Session** is set.
1. Click the **Copy** button to the right of your connection string to copy it to the clipboard.
## Set your NEW_DB_URL environment variable
Set the **NEW_DB_URL** environment variable at the command line using your Supabase connection string. You will need to replace `[YOUR-PASSWORD]` with your actual database password.
Example:
```bash
export NEW_DB_URL="postgresql://postgres.xxxxxxxxxxxxxxxxxxxx:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres"
```
## Migrate the database
You will need the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full [PostgreSQL installation](https://www.postgresql.org/download).
1. Export your database to a file in console
Use `pg_dump` with your Postgres credentials to export your database to a file (e.g., `dump.sql`).
```bash
pg_dump "$OLD_DB_URL" \
--clean \
--if-exists \
--quote-all-identifiers \
--no-owner \
--no-privileges \
> dump.sql
```
2. Import the database to your Supabase project
Use `psql` to import the Postgres database file to your Supabase project.
```bash
psql -d "$NEW_DB_URL" -f dump.sql
```
Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
- To exclude a schema: `--exclude-schema=PATTERN`.
- To only migrate a single table: `--table=PATTERN`.
- To exclude a table: `--exclude-table=PATTERN`.
Run `pg_dump --help` for a full list of options.
<MigrationWarnings />
## Enterprise
[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project.
@@ -0,0 +1,97 @@
---
title: 'Migrate from Vercel Postgres to Supabase'
description: 'Migrate your existing Vercel Postgres database to Supabase.'
subtitle: 'Migrate your existing Vercel Postgres database to Supabase.'
---
This guide demonstrates how to migrate your Vercel Postgres database to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
## Retrieve your Vercel Postgres database credentials [#retrieve-credentials]
1. Log in to your Vercel Dashboard [https://vercel.com/login](https://vercel.com/login).
1. Click on the **Storage** tab.
1. Click on your Postgres Database.
1. Under the **Quickstart** section, select **psql** then click **Show Secret** to reveal your database password.
1. Copy the string after `psql ` to the clipboard.
Example:
```bash
psql "postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require"
```
Copy this part to your clipboard:
```bash
"postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require"
```
## Set your OLD_DB_URL environment variable
Set the **OLD_DB_URL** environment variable at the command line using your Vercel Postgres Database credentials.
Example:
```bash
export OLD_DB_URL="postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-west-2.aws.neon.tech:5432/verceldb?sslmode=require"
```
## Retrieve your Supabase connection string [#retrieve-supabase-connection-string]
1. If you're new to Supabase, [create a project](https://supabase.com/dashboard).
Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database).
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection string**, select **URI**, make sure **Display connection pooler** is checked, and **Mode: Session** is set.
1. Click the **Copy** button to the right of your connection string to copy it to the clipboard.
## Set your NEW_DB_URL environment variable
Set the **NEW_DB_URL** environment variable at the command line using your Supabase connection string. You will need to replace `[YOUR-PASSWORD]` with your actual database password.
Example:
```bash
export NEW_DB_URL="postgresql://postgres.xxxxxxxxxxxxxxxxxxxx:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres"
```
## Migrate the database
You will need the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full [PostgreSQL installation](https://www.postgresql.org/download).
1. Export your database to a file in console
Use `pg_dump` with your Postgres credentials to export your database to a file (e.g., `dump.sql`).
```bash
pg_dump "$OLD_DB_URL" \
--clean \
--if-exists \
--quote-all-identifiers \
--no-owner \
--no-privileges \
> dump.sql
```
2. Import the database to your Supabase project
Use `psql` to import the Postgres database file to your Supabase project.
```bash
psql -d "$NEW_DB_URL" -f dump.sql
```
Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
- To exclude a schema: `--exclude-schema=PATTERN`.
- To only migrate a single table: `--table=PATTERN`.
- To exclude a table: `--exclude-table=PATTERN`.
Run `pg_dump --help` for a full list of options.
<MigrationWarnings />
## Enterprise
[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project.
@@ -99,7 +99,7 @@ If you upgrade to a paid plan while your project is paused, any expired one-clic
If the 90 day project restore window has expired but you need to access data contained within your project using SQL, you can attempt to restore the project into a local Postgres instance. Supabase publishes tooling that can be used for that purpose. Be aware that this workflow does not produce a complete Supabase environment with REST/Auth/Storage. Instead, it creates a standalone Postgres instance that is maximally compatible with your project's backup file to assist with recovering your data.
Downloaded backups are plain text SQL files that can be executed in Postgres to restore your project's schema and data. If you need you access data within a backup we recommend restoring into a Postgres instance built using the Supabase open source build tooling to ensure that the local instance is as similar to the original project as possible. Here are the steps to start Supabase Postgres locally and restore the backup:
Downloaded backups are plain text SQL files that can be executed in Postgres to restore your project's schema and data. If you need to access data within a backup we recommend restoring into a Postgres instance built using the Supabase open source build tooling to ensure that the local instance is as similar to the original project as possible. Here are the steps to start Supabase Postgres locally and restore the backup:
Install [nix](https://nixos.org/):
@@ -265,7 +265,7 @@ Each system has a number of configuration options which can be found in the rele
- [Postgres](https://hub.docker.com/_/postgres/)
- [PostgREST](https://postgrest.org/en/stable/configuration.html)
- [Realtime](https://github.com/supabase/realtime#server)
- [GoTrue](https://github.com/supabase/gotrue)
- [Auth](https://github.com/supabase/auth)
- [Storage](https://github.com/supabase/storage-api)
- [Kong](https://docs.konghq.com/gateway/latest/install/docker/)
- [Supavisor](https://supabase.github.io/supavisor/development/docs/)
@@ -15,6 +15,14 @@ import {
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { MultiSelect } from '~/components/MultiSelect.client'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import { type ITroubleshootingMetadata } from './Troubleshooting.utils'
import {
formatError,
@@ -30,7 +38,7 @@ function useTroubleshootingSearchState() {
const setSelectedProducts = useCallback(
(products: string[]) => {
_setState({
products: products.length === 0 ? null : products,
products: products.length === 0 ? [] : products,
})
},
[_setState]
@@ -38,7 +46,7 @@ function useTroubleshootingSearchState() {
const setSelectedErrorCodes = useCallback(
(errorCodes: string[]) => {
_setState({
errorCodes: errorCodes.length === 0 ? null : errorCodes,
errorCodes: errorCodes.length === 0 ? [] : errorCodes,
})
},
[_setState]
@@ -46,7 +54,7 @@ function useTroubleshootingSearchState() {
const setSelectedTags = useCallback(
(tags: string[]) => {
_setState({
tags: tags.length === 0 ? null : tags,
tags: tags.length === 0 ? [] : tags,
})
},
[_setState]
@@ -177,9 +185,6 @@ function TroubleshootingFilterInternal({
reset,
} = useTroubleshootingSearchState()
const [productsOpen, _setProductsOpen] = useState(false)
const [errorsOpen, _setErrorsOpen] = useState(false)
const [tagsOpen, _setTagsOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement | null>(null)
const allEntries = useRef<HTMLElement[]>([])
@@ -195,112 +200,46 @@ function TroubleshootingFilterInternal({
allEntries.current = entries
}, [])
const setProductsOpen = useCallback(
(open: boolean) => {
_setProductsOpen(open)
if (open) {
allEntries.current?.forEach((entry) => (entry.inert = true))
} else {
allEntries.current?.forEach((entry) => (entry.inert = false))
}
},
[_setProductsOpen]
)
const setErrorsOpen = useCallback(
(open: boolean) => {
_setErrorsOpen(open)
if (open) {
allEntries.current?.forEach((entry) => (entry.inert = true))
} else {
allEntries.current?.forEach((entry) => (entry.inert = false))
}
},
[_setErrorsOpen]
)
const setTagsOpen = useCallback(
(open: boolean) => {
_setTagsOpen(open)
if (open) {
allEntries.current?.forEach((entry) => (entry.inert = true))
} else {
allEntries.current?.forEach((entry) => (entry.inert = false))
}
},
[_setTagsOpen]
)
const convertToItem = useCallback(
(arr: string[], { capitalize = false }: { capitalize?: boolean } = {}) =>
arr.map((item) => ({
value: item,
label: capitalize ? item[0].toUpperCase() + item.slice(1) : item,
})),
[]
)
return (
<>
<h2 className="sr-only">Search and filter</h2>
<div className={cn('flex flex-wrap gap-2 items-center', className)}>
<MultiSelect
open={productsOpen}
onOpenChange={setProductsOpen}
selected={convertToItem(selectedProducts, { capitalize: true })}
onSelectedChange={(newItemsOrCreateNewItems) => {
const newItems =
typeof newItemsOrCreateNewItems === 'function'
? newItemsOrCreateNewItems(convertToItem(selectedProducts, { capitalize: true }))
: newItemsOrCreateNewItems
setSelectedProducts(newItems.map((item) => item.value))
}}
>
<MultiSelect.Trigger className="w-48" label="Products" />
<MultiSelect.Content sameWidthAsTrigger>
{products.map((product) => (
<MultiSelect.Item
item={{ value: product, label: product[0].toUpperCase() + product.slice(1) }}
/>
))}
</MultiSelect.Content>
</MultiSelect>
<MultiSelect
open={errorsOpen}
onOpenChange={setErrorsOpen}
selected={convertToItem(selectedErrorCodes, { capitalize: true })}
onSelectedChange={(newItemsOrCreateNewItems) => {
const newItems =
typeof newItemsOrCreateNewItems === 'function'
? newItemsOrCreateNewItems(convertToItem(selectedErrorCodes, { capitalize: true }))
: newItemsOrCreateNewItems
setSelectedErrorCodes(newItems.map((item) => item.value))
}}
>
<MultiSelect.Trigger className="w-48" label="Error codes" />
<MultiSelect.Content sameWidthAsTrigger>
{errors.map((error) => (
<MultiSelect.Item item={{ value: formatError(error), label: formatError(error) }} />
))}
</MultiSelect.Content>
</MultiSelect>
<MultiSelect
open={tagsOpen}
onOpenChange={setTagsOpen}
selected={convertToItem(selectedTags, { capitalize: true })}
onSelectedChange={(newItemsOrCreateNewItems) => {
const newItems =
typeof newItemsOrCreateNewItems === 'function'
? newItemsOrCreateNewItems(convertToItem(selectedTags, { capitalize: true }))
: newItemsOrCreateNewItems
setSelectedTags(newItems.map((item) => item.value))
}}
>
<MultiSelect.Trigger className="w-48" label="Tags" />
<MultiSelect.Content sameWidthAsTrigger>
{keywords.map((keyword) => (
<MultiSelect.Item item={{ value: keyword, label: keyword }} />
))}
</MultiSelect.Content>
</MultiSelect>
<MultiSelector values={selectedProducts} onValuesChange={setSelectedProducts}>
<MultiSelector.Trigger badgeLimit={1} className="w-48" label="Products" />
<MultiSelector.Content>
<MultiSelector.List>
{products?.map((product) => (
<MultiSelector.Item key={`product-${product}`} value={product}>
{product}
</MultiSelector.Item>
))}
</MultiSelector.List>
</MultiSelector.Content>
</MultiSelector>
<MultiSelector values={selectedErrorCodes} onValuesChange={setSelectedErrorCodes}>
<MultiSelector.Trigger badgeLimit={1} className="w-48" label="Error codes" />
<MultiSelector.Content>
<MultiSelector.List>
{errors?.map((error) => (
<MultiSelector.Item key={`error-${error.code}`} value={error.code}>
{error.code}
</MultiSelector.Item>
))}
</MultiSelector.List>
</MultiSelector.Content>
</MultiSelector>
<MultiSelector values={selectedTags} onValuesChange={setSelectedTags}>
<MultiSelector.Trigger badgeLimit={1} className="w-48" label="Tags" />
<MultiSelector.Content>
<MultiSelector.List>
{keywords?.map((keyword) => (
<MultiSelector.Item key={`keyword-${keyword}`} value={keyword}>
{keyword}
</MultiSelector.Item>
))}
</MultiSelector.List>
</MultiSelector.Content>
</MultiSelector>
<div className="relative">
<Input_Shadcn_
id="troubleshooting-search"
+1 -1
View File
@@ -22,7 +22,7 @@
"last-changed:reset": "npm run last-changed -- --reset",
"codegen:references": "tsx features/docs/Reference.generated.script.ts",
"codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"",
"clean": "rm -rf ./docs/features/docs/generated"
"clean": "rimraf node_modules ./docs/features/docs/generated"
},
"dependencies": {
"@code-hike/mdx": "^0.9.0",
+5 -3
View File
@@ -6,7 +6,9 @@ At Supabase, we consider the security of our systems a top priority. But no matt
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.
Out of scope vulnerabilities:
The full scope of our VDP is available at https://hackerone.com/supabase.
Here is a brief list of some common out of scope vulnerabilities:
- Clickjacking on pages with no sensitive actions.
- Unauthenticated/logout/login CSRF.
@@ -25,7 +27,7 @@ Testing guidelines:
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
Reporting guidelines:
- E-mail your findings to security@supabase.io.
- File a report through our VDP at https://hackerone.com/supabase
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
Disclosure guidelines:
@@ -36,7 +38,7 @@ Disclosure guidelines:
- information about Supabase employees, contractors or partners
What we promise:
- We will respond to your report within 7 business days with our evaluation of the report and an expected resolution date.
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
- We will keep you informed of the progress towards resolving the problem.
@@ -0,0 +1,4 @@
<svg width="58" height="57" viewBox="0 0 58 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 9.82759C0 4.39996 4.47705 0 9.99976 0H47.9989C53.5216 0 57.9986 4.39996 57.9986 9.82759V41.5893C57.9986 47.2045 50.7684 49.6414 47.2618 45.2082L36.2991 31.3488V48.1552C36.2991 53.04 32.2698 57 27.2993 57H9.99976C4.47705 57 0 52.6 0 47.1724V9.82759ZM9.99976 7.86207C8.89522 7.86207 7.99981 8.74206 7.99981 9.82759V47.1724C7.99981 48.2579 8.89522 49.1379 9.99976 49.1379H27.5993C28.1516 49.1379 28.2993 48.6979 28.2993 48.1552V25.6178C28.2993 20.0027 35.5295 17.5656 39.0361 21.9989L49.9988 35.8583V9.82759C49.9988 8.74206 50.1034 7.86207 48.9988 7.86207H9.99976Z" fill="#0C0D0D"/>
<path d="M48.0003 0C53.523 0 58 4.39996 58 9.82759V41.5893C58 47.2045 50.7699 49.6414 47.2633 45.2082L36.3006 31.3488V48.1552C36.3006 53.04 32.2712 57 27.3008 57C27.8531 57 28.3008 56.56 28.3008 56.0172V25.6178C28.3008 20.0027 35.5309 17.5656 39.0375 21.9989L50.0002 35.8583V1.96552C50.0002 0.879992 49.1048 0 48.0003 0Z" fill="#0C0D0D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="58" height="57" viewBox="0 0 58 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 9.82759C0 4.39996 4.47705 0 9.99976 0H47.9989C53.5216 0 57.9986 4.39996 57.9986 9.82759V41.5893C57.9986 47.2045 50.7684 49.6414 47.2618 45.2082L36.2991 31.3488V48.1552C36.2991 53.04 32.2698 57 27.2993 57H9.99976C4.47705 57 0 52.6 0 47.1724V9.82759ZM9.99976 7.86207C8.89522 7.86207 7.99981 8.74206 7.99981 9.82759V47.1724C7.99981 48.2579 8.89522 49.1379 9.99976 49.1379H27.5993C28.1516 49.1379 28.2993 48.6979 28.2993 48.1552V25.6178C28.2993 20.0027 35.5295 17.5656 39.0361 21.9989L49.9988 35.8583V9.82759C49.9988 8.74206 50.1034 7.86207 48.9988 7.86207H9.99976Z" fill="white"/>
<path d="M48.0003 0C53.523 0 58 4.39996 58 9.82759V41.5893C58 47.2045 50.7699 49.6414 47.2633 45.2082L36.3006 31.3488V48.1552C36.3006 53.04 32.2712 57 27.3008 57C27.8531 57 28.3008 56.56 28.3008 56.0172V25.6178C28.3008 20.0027 35.5309 17.5656 39.0375 21.9989L50.0002 35.8583V1.96552C50.0002 0.879992 49.1048 0 48.0003 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="1155" height="1000" viewBox="0 0 1155 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M577.344 0L1154.69 1000H0L577.344 0Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

@@ -0,0 +1,3 @@
<svg width="1155" height="1000" viewBox="0 0 1155 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M577.344 0L1154.69 1000H0L577.344 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

+3 -5
View File
@@ -1570,8 +1570,7 @@
},
"500": { "description": "Failed to retrieve project's Postgres config" }
},
"tags": ["Database"],
"security": [{ "bearer": [] }]
"tags": ["Database"]
},
"put": {
"operationId": "v1-update-postgres-config",
@@ -1605,8 +1604,7 @@
"403": { "description": "" },
"500": { "description": "Failed to update project's Postgres config" }
},
"tags": ["Database"],
"security": [{ "bearer": [] }]
"tags": ["Database"]
}
},
"/v1/projects/{ref}/config/database/pgbouncer": {
@@ -1936,7 +1934,7 @@
},
"/v1/projects/{ref}/functions": {
"post": {
"operationId": "createFunction",
"operationId": "v1-create-a-function",
"summary": "Create a function",
"description": "Creates a function and adds it to the specified project.",
"parameters": [
+7 -3
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 1.200.3
version: 1.207.8
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -1128,6 +1128,10 @@ commands:
name: --config <strings>
description: Config overrides specified as a 'key=value' pair
default_value: '[]'
- id: no-restart
name: --no-restart
description: Do not restart the database after updating config.
default_value: 'false'
- id: replace-existing-overrides
name: --replace-existing-overrides
description: |
@@ -2878,7 +2882,7 @@ commands:
Requires the local development stack to be started by running `supabase start`.
Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql` by default, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded.
Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql`, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded.
Note that since Postgres roles are cluster level entities, those changes will persist between resets. In order to reset custom roles, you need to restart the local development stack.
examples:
@@ -2976,7 +2980,7 @@ commands:
default_value: 'false'
- id: include-seed
name: --include-seed
description: Include seed data from supabase/seed.sql.
description: Include seed data from your config.
default_value: 'false'
- id: linked
name: --linked
+6
View File
@@ -225,6 +225,12 @@
"type": "category",
"title": "Edge Functions",
"items": [
{
"id": "v1-create-a-function",
"title": "Create a function",
"slug": "v1-create-a-function",
"type": "operation"
},
{
"id": "v1-delete-a-function",
"title": "Delete a function",
+36 -24
View File
@@ -11,9 +11,11 @@ info:
title: 'GoTrue' # {string} A readable name.
source: 'https://github.com/supabase/gotrue' # {string} Where developers can find the source code.
bugs: 'https://github.com/supabase/gotrue/issues' # {string} Where developers can file bugs.
spec: 'https://github.com/supabase/supabase/blob/master/spec/gotrue_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs).
spec: 'https://github.com/supabase/supabase/blob/master/docs/spec/gotrue_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs).
description: |
A `config.toml` file is generated after running `supabase init`. This file is located in the `supabase` folder under `supabase/config.toml`.
In a self-hosted environment, you do not have access to the Auth configuration such as third party OAuth provider settings through the Supabase dashboard. Instead, you configure them through the `docker-compose.yml` file. You can read more about the configuration in the [Self-hosting guide](/docs/guides/self-hosting/docker#configuring-services).
You can find the complete list of available configuration parameters on the README of the [Auth repository](https://github.com/supabase/auth?tab=readme-ov-file#configuration).
tags:
- id: general
title: General
@@ -21,58 +23,68 @@ info:
# This section is an array of public functions which a user might need to execute.
parameters:
- id: 'project_id' # {string} A unique identifier for this param.
title: 'project_id' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
# default: '5432'
description: |
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
- id: 'GOTRUE_EXTERNAL_GITHUB' # {string} A unique identifier for this param.
title: 'auth.external.github' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
# default: '5432'
description: |
Describes whether the Github provider is enabled or not.
- id: 'GOTRUE_SITE_URL' # {string} A unique identifier for this param.
title: 'auth.site_url' # {string} Any name.
title: 'GOTRUE_SITE_URL' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails.
- id: 'GOTRUE_EXTERNAL_GITHUB_ENABLED' # {string} A unique identifier for this param.
title: 'GOTRUE_EXTERNAL_GITHUB_ENABLED' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: false
description: |
Whether the external provider, GitHub in this case, is enabled or not.
- id: 'GOTRUE_EXTERNAL_GITHUB_CLIENT_ID' # {string} A unique identifier for this param.
title: 'GOTRUE_EXTERNAL_GITHUB_CLIENT_ID' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: false
description: |
The OAuth2 Client ID registered with the external provider.
- id: 'GOTRUE_EXTERNAL_GITHUB_SECRET' # {string} A unique identifier for this param.
title: 'GOTRUE_EXTERNAL_GITHUB_SECRET' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: false
description: |
The OAuth2 Client Secret provided by the external provider when you registered.
- id: 'GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI' # {string} A unique identifier for this param.
title: 'GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: false
description: |
The URI a OAuth2 provider will redirect to with the `code` and `state` values.
- id: 'GOTRUE_URI_ALLOW_LIST' # {string} A unique identifier for this param.
title: 'auth.additional_redirect_urls' # {string} Any name.
title: 'GOTRUE_URI_ALLOW_LIST' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
A comma separated list of URIs (e.g. `"https://foo.example.com,https://*.foo.example.com,https://bar.example.com"`) which are permitted as valid `redirect_to` destinations.
- id: 'GOTRUE_JWT_EXP' # {string} A unique identifier for this param.
title: 'auth.jwt_expiry' # {string} Any name.
title: 'GOTRUE_JWT_EXP' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).
- id: 'GOTRUE_DISABLE_SIGNUP' # {string} A unique identifier for this param.
title: 'auth.enable_signup' # {string} Any name.
title: 'GOTRUE_DISABLE_SIGNUP' # {string} Any name.
tags: ['general'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
Allow/disallow new user signups to your project.
- id: 'GOTRUE_EXTERNAL_EMAIL_ENABLED' # {string} A unique identifier for this param.
title: 'auth.email.enable_signup' # {string} Any name.
title: 'GOTRUE_EXTERNAL_EMAIL_ENABLED' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
Allow/disallow new user signups via email to your project.
- id: 'GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED' # {string} A unique identifier for this param.
title: 'auth.email.double_confirm_changes' # {string} Any name.
title: 'GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
If enabled, a user will be required to confirm any email change on both the old, and new email addresses. If disabled, only the new email is required to confirm.
- id: 'GOTRUE_MAILER_AUTOCONFIRM' # {string} A unique identifier for this param.
title: 'auth.email.enable_confirmations' # {string} Any name.
title: 'GOTRUE_MAILER_AUTOCONFIRM' # {string} Any name.
tags: ['general', 'email'] # {string[]} These tags are useful for grouping parameters
required: true
description: |
@@ -4604,12 +4604,7 @@
"description": "Failed to retrieve project's Postgres config"
}
},
"tags": ["Database"],
"security": [
{
"bearer": []
}
]
"tags": ["Database"]
},
"put": {
"operationId": "v1-update-postgres-config",
@@ -4814,12 +4809,7 @@
"description": "Failed to update project's Postgres config"
}
},
"tags": ["Database"],
"security": [
{
"bearer": []
}
]
"tags": ["Database"]
}
},
"/v1/projects/{ref}/config/database/pgbouncer": {
@@ -7832,7 +7822,7 @@
},
"/v1/projects/{ref}/functions": {
"post": {
"operationId": "createFunction",
"operationId": "v1-create-a-function",
"summary": "Create a function",
"description": "Creates a function and adds it to the specified project.",
"parameters": [
+11 -3
View File
@@ -28,8 +28,12 @@ import { InitialStateType } from './store/reducers'
import type { SupabaseGridProps } from './types'
import { getGridColumns } from './utils/gridColumns'
function onLoadStorage(storageRef: string, tableName: string, schema?: string | null) {
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, storageRef)
export function loadTableEditorSortsAndFiltersFromLocalStorage(
projectRef: string,
tableName: string,
schema?: string | null
) {
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
const jsonStr = localStorage.getItem(storageKey)
if (!jsonStr) return
const json = JSON.parse(jsonStr)
@@ -45,7 +49,11 @@ async function initTable(
filter?: string[] // Comes directly from URL param
): Promise<{ savedState: { sorts?: string[]; filters?: string[] } }> {
const savedState = props.projectRef
? onLoadStorage(props.projectRef, props.table.name, props.table.schema)
? loadTableEditorSortsAndFiltersFromLocalStorage(
props.projectRef,
props.table.name,
props.table.schema
)
: undefined
// Check for saved state on initial load and also, load sort and filters via URL param only if given
@@ -1,12 +1,20 @@
import type { PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import type { Filter } from 'components/grid/types'
import { compact } from 'lodash'
import type { Dictionary } from 'types'
import type { Filter } from 'components/grid/types'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import {
ForeignKeyConstraint,
ForeignKeyConstraintsData,
} from 'data/database/foreign-key-constraints-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { TableLike } from 'hooks/misc/useTable'
import type { Dictionary, SchemaView } from 'types'
import { FilterOperatorOptions } from './components/header/filter/Filter.constants'
import { STORAGE_KEY_PREFIX } from './constants'
import { InitialStateType } from './store/reducers'
import type { Sort, SupabaseGridProps, SupaColumn, SupaTable } from './types'
/**
* Ensure that if editable is false, we should remove all editing actions
* to prevent rare-case bugs with the UI
@@ -124,6 +132,54 @@ export function parseSupaTable(
}
}
export function getSupaTable({
selectedTable,
entityType,
foreignKeyMeta,
encryptedColumns,
}: {
selectedTable: TableLike
entityType?: ENTITY_TYPE
foreignKeyMeta: ForeignKeyConstraintsData
encryptedColumns: string[]
}) {
// [Joshen] We can tweak below to eventually support composite keys as the data
// returned from foreignKeyMeta should be easy to deal with, rather than pg-meta
const formattedRelationships = (
('relationships' in selectedTable && selectedTable.relationships) ||
[]
).map((relationship: PostgresRelationship) => {
const relationshipMeta = foreignKeyMeta.find(
(fk: ForeignKeyConstraint) => fk.id === relationship.id
)
return {
...relationship,
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
}
})
const isViewSelected =
entityType === ENTITY_TYPE.VIEW || entityType === ENTITY_TYPE.MATERIALIZED_VIEW
const isForeignTableSelected = entityType === ENTITY_TYPE.FOREIGN_TABLE
return !isViewSelected && !isForeignTableSelected
? parseSupaTable(
{
table: selectedTable as PostgresTable,
columns: (selectedTable as PostgresTable).columns ?? [],
primaryKeys: (selectedTable as PostgresTable).primary_keys ?? [],
relationships: formattedRelationships,
},
encryptedColumns
)
: parseSupaTable({
table: selectedTable as SchemaView,
columns: (selectedTable as SchemaView).columns ?? [],
primaryKeys: [],
relationships: [],
})
}
export const saveStorageDebounced = AwesomeDebouncePromise(saveStorage, 500)
function saveStorage(
@@ -15,6 +15,7 @@ import {
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
cn,
} from 'ui'
import { useTrackedState } from '../../store/Store'
import { BlockKeys } from '../common/BlockKeys'
@@ -188,9 +189,10 @@ export const TextEditor = <TRow, TSummaryRow = unknown>({
}
>
<div
className={`${
!!value && value.trim().length == 0 ? 'sb-grid-fill-container' : ''
} sb-grid-text-editor__trigger`}
className={cn(
!!value && value.toString().trim().length === 0 && 'sb-grid-fill-container',
'sb-grid-text-editor__trigger'
)}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{value === null ? <NullValue /> : value === '' ? <EmptyValue /> : value}
@@ -1,9 +1,9 @@
import { ArrowRight } from 'lucide-react'
import Link from 'next/link'
import type { PropsWithChildren } from 'react'
import type { RenderCellProps } from 'react-data-grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useTableQuery } from 'data/tables/table-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
@@ -62,11 +62,20 @@ export const ForeignKeyFormatter = (props: Props) => {
className="translate-y-[2px]"
style={{ padding: '3px' }}
>
<Link
<EditorTablePageLink
href={`/project/${projectRef}/editor/${targetTable?.id}?schema=${selectedSchema}&filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
projectRef={projectRef}
id={targetTable && String(targetTable?.id)}
filters={[
{
column: relationship.target_column_name,
operator: '=',
value: String(value),
},
]}
>
<ArrowRight size={14} />
</Link>
</EditorTablePageLink>
</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">View referencing record</TooltipContent_Shadcn_>
@@ -2,22 +2,15 @@ import { Toggle } from 'ui'
import Panel from 'components/ui/Panel'
import { useSendResetMutation } from 'data/telemetry/send-reset-mutation'
import { useFlag } from 'hooks/ui/useFlag'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
const AnalyticsSettings = () => {
const snap = useAppStateSnapshot()
const enablePostHogTelemetry = useFlag('enablePosthogChanges')
const { mutate: sendReset } = useSendResetMutation()
const consentFlag = enablePostHogTelemetry
? LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT_PH
: LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT
const onToggleOptIn = () => {
const value = !snap.isOptedInTelemetry ? 'true' : 'false'
snap.setIsOptedInTelemetry(value === 'true', consentFlag)
snap.setIsOptedInTelemetry(value === 'true')
if (value === 'false') sendReset()
}
@@ -1,7 +1,7 @@
import { Key } from 'lucide-react'
import { useMemo } from 'react'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { copyToClipboard } from 'lib/helpers'
import { Badge } from 'ui'
@@ -23,14 +23,13 @@ export function useApiKeysCommands() {
const setPage = useSetPage()
const project = useSelectedProject()
const { data: settings } = useProjectApiQuery(
const ref = project?.ref || '_'
const { data: settings } = useProjectSettingsV2Query(
{ projectRef: project?.ref },
{ enabled: !!project }
)
const ref = project?.ref || '_'
const anonKey = settings?.autoApiService?.defaultApiKey ?? undefined
const serviceKey = settings?.autoApiService?.serviceApiKey ?? undefined
const { anonKey, serviceKey } = getAPIKeys(settings)
const commands = useMemo(
() =>
@@ -40,7 +39,7 @@ export function useApiKeysCommands() {
id: 'anon-key',
name: `Copy anonymous API key`,
action: () => {
copyToClipboard(anonKey ?? '')
copyToClipboard(anonKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
@@ -56,7 +55,7 @@ export function useApiKeysCommands() {
id: 'service-key',
name: `Copy service API key`,
action: () => {
copyToClipboard(serviceKey ?? '')
copyToClipboard(serviceKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
@@ -1,10 +1,10 @@
import { Link } from 'lucide-react'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { copyToClipboard } from 'lib/helpers'
import { Badge } from 'ui'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
import { orderCommandSectionsByPriority } from './ordering'
@@ -12,13 +12,13 @@ export function useApiUrlCommand() {
const setIsOpen = useSetCommandMenuOpen()
const project = useSelectedProject()
const { data: settings } = useProjectApiQuery(
const { data: settings } = useProjectSettingsV2Query(
{ projectRef: project?.ref },
{ enabled: !!project }
)
const apiUrl = settings?.autoApiService.endpoint
? `${settings.autoApiService.protocol ?? 'https'}://${settings.autoApiService.endpoint}`
const apiUrl = settings?.app_config?.endpoint
? `https://${settings.app_config.endpoint}`
: undefined
useRegisterCommands(
@@ -3,7 +3,7 @@ import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
const APISidePanelPreview = () => {
export const APISidePanelPreview = () => {
const { ref } = useParams()
return (
@@ -38,5 +38,3 @@ const APISidePanelPreview = () => {
</div>
)
}
export default APISidePanelPreview
@@ -5,7 +5,7 @@ import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, WarningIcon } from 'ui'
const CLSPreview = () => {
export const CLSPreview = () => {
const { ref } = useParams()
return (
@@ -53,5 +53,3 @@ const CLSPreview = () => {
</div>
)
}
export default CLSPreview
@@ -3,6 +3,33 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { APISidePanelPreview } from './APISidePanelPreview'
import { CLSPreview } from './CLSPreview'
import { FunctionsAssistantPreview } from './FunctionsAssistantPreview'
export const FEATURE_PREVIEWS = [
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT,
name: 'Database Functions Assistant',
content: <FunctionsAssistantPreview />,
discussionsUrl: undefined,
isNew: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
content: <APISidePanelPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038',
isNew: false,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS,
name: 'Column-level privileges',
content: <CLSPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295',
isNew: false,
},
]
type FeaturePreviewContextType = {
flags: { [key: string]: boolean }
@@ -17,22 +44,19 @@ const FeaturePreviewContext = createContext<FeaturePreviewContextType>({
export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => {
const [flags, setFlags] = useState({
[LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]: false,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: false,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: false,
})
const [flags, setFlags] = useState(() =>
FEATURE_PREVIEWS.reduce((a, b) => {
return { ...a, [b.key]: false }
}, {})
)
useEffect(() => {
if (typeof window !== 'undefined') {
setFlags({
[LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]:
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT) === 'true',
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]:
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL) === 'true',
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]:
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS) === 'true',
})
setFlags(
FEATURE_PREVIEWS.reduce((a, b) => {
return { ...a, [b.key]: localStorage.getItem(b.key) === 'true' }
}, {})
)
}
}, [])
@@ -61,3 +85,8 @@ export const useIsColumnLevelPrivilegesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
}
export const useIsDatabaseFunctionsAssistantEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT]
}
@@ -3,38 +3,23 @@ import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Button, Modal, ScrollArea, cn } from 'ui'
import APISidePanelPreview from './APISidePanelPreview'
import CLSPreview from './CLSPreview'
import { useFeaturePreviewContext } from './FeaturePreviewContext'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext'
import { useFlag } from 'hooks/ui/useFlag'
const FeaturePreviewModal = () => {
// [Ivan] We should probably move this to a separate file, together with LOCAL_STORAGE_KEYS. We should make adding new feature previews as simple as possible.
const FEATURE_PREVIEWS: { key: string; name: string; content: any; discussionsUrl?: string }[] = [
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
content: <APISidePanelPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038',
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS,
name: 'Column-level privileges',
content: <CLSPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295',
},
]
const snap = useAppStateSnapshot()
const featurePreviewContext = useFeaturePreviewContext()
const { mutate: sendEvent } = useSendEventMutation()
const enableFunctionsAssistant = useFlag('functionsAssistantV2')
const selectedFeaturePreview =
snap.selectedFeaturePreview === '' ? FEATURE_PREVIEWS[0].key : snap.selectedFeaturePreview
const [selectedFeatureKey, setSelectedFeatureKey] = useState<string>(selectedFeaturePreview)
const isNotReleased =
selectedFeatureKey === 'supabase-ui-functions-assistant' && !enableFunctionsAssistant
// this modal can be triggered on other pages
// Update local state when valtio state changes
@@ -103,8 +88,11 @@ const FeaturePreviewModal = () => {
</div>
<div className="flex-grow max-h-[550px] p-4 space-y-3 overflow-y-auto">
<div className="flex items-center justify-between">
<p>{selectedFeature?.name}</p>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-x-2">
<p>{selectedFeature?.name}</p>
{selectedFeature?.isNew && <Badge color="green">New</Badge>}
</div>
<div className="flex items-center gap-x-2">
{selectedFeature?.discussionsUrl !== undefined && (
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
<Link href={selectedFeature.discussionsUrl} target="_blank" rel="noreferrer">
@@ -112,9 +100,15 @@ const FeaturePreviewModal = () => {
</Link>
</Button>
)}
<Button type="default" onClick={() => toggleFeature()}>
{isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
</Button>
{isNotReleased ? (
<Button disabled type="default">
Coming soon
</Button>
) : (
<Button type="default" onClick={() => toggleFeature()}>
{isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
</Button>
)}
</div>
</div>
{selectedFeature?.content}
@@ -0,0 +1,55 @@
import Image from 'next/image'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import { detectOS } from 'lib/helpers'
export const FunctionsAssistantPreview = () => {
const os = detectOS()
const { ref } = useParams()
return (
<div className="flex flex-col gap-y-2">
<p className="text-foreground-light text-sm mb-4">
We're providing an additional alternative UX to creating database functions through the
dashboard with the integration of our AI Assistant that you might have seen in the Auth
Policies section.
</p>
<Image
src={`${BASE_PATH}/img/previews/functions-assistant.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm mb-4">
This preview also shares an improved Assistant interface where you may provide the Assistant
with contexts in hopes to generate more relevant and higher quality outputs. Contexts that
you may provide include specific schemas, and / or specific tables from your database.
</p>
<p className="text-foreground-light text-sm mb-4">
We'd hope to use this as a consistent pattern throughout the dashboard eventually if this
feature preview proves itself to benefit most of our users, so as usual please do feel free
to let us know what you think if the attached GitHub discussion above!
</p>
<div className="flex flex-col gap-y-2">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
<Markdown
className="text-foreground-light"
content={`Add a button beside the "Create new function" button on the [Database Functions page](/project/${ref}/database/functions) that will open up a code editor paired with a contextualized AI assistant in a side panel.`}
/>
</li>
<li>
<Markdown
className="text-foreground-light"
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that will open the Assistant in an uncontextualized mode, along with a quick SQL Editor that you can run queries with from anywhere in the dashboard`}
/>
</li>
</ul>
</div>
</div>
)
}
@@ -1,18 +1,18 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { Check, ChevronUp, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import type { components } from 'data/api'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import Link from 'next/link'
import {
Alert,
Alert_Shadcn_,
@@ -109,8 +109,8 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => {
}
}
const { data: settings } = useProjectApiQuery({ projectRef })
const apiUrl = `${settings?.autoApiService.protocol}://${settings?.autoApiService.endpoint}`
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const apiUrl = `https://${settings?.app_config?.endpoint}`
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
@@ -123,6 +123,11 @@ export const CreateHookSheet = ({
[title]
)
const supportedReturnTypes =
definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED'
? ['json', 'jsonb', 'void']
: ['json', 'jsonb']
const hook: Hook = useMemo(() => {
return {
...definition,
@@ -288,20 +293,21 @@ export const CreateHookSheet = ({
</div>
</SheetHeader>
<Separator />
<SheetSection className="overflow-auto flex-grow">
<SheetSection className="overflow-auto flex-grow px-0">
<Form_Shadcn_ {...form}>
<form
id={FORM_ID}
className="space-y-6 w-full py-8 flex-1"
className="space-y-6 w-full py-5 flex-1"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
key="enabled"
control={form.control}
name="enabled"
control={form.control}
render={({ field }) => (
<FormItemLayout
layout="flex"
className="px-5"
label={`Enable ${values.hookType}`}
description={
values.hookType === 'Send SMS hook'
@@ -325,7 +331,7 @@ export const CreateHookSheet = ({
control={form.control}
name="selectedType"
render={({ field }) => (
<FormItemLayout label="Hook type">
<FormItemLayout label="Hook type" className="px-5">
<FormControl_Shadcn_>
<RadioGroupStacked
value={field.value}
@@ -353,7 +359,7 @@ export const CreateHookSheet = ({
)}
{values.selectedType === 'postgres' ? (
<>
<div className="grid grid-cols-2 gap-8">
<div className="grid grid-cols-2 gap-8 px-5">
<FormField_Shadcn_
key="postgresValues.schema"
control={form.control}
@@ -361,7 +367,7 @@ export const CreateHookSheet = ({
render={({ field }) => (
<FormItemLayout
label="Postgres Schema"
description="Postgres schema where the function is defined."
description="Postgres schema where the function is defined"
>
<FormControl_Shadcn_>
<SchemaSelector
@@ -381,8 +387,8 @@ export const CreateHookSheet = ({
name="postgresValues.functionName"
render={({ field }) => (
<FormItemLayout
label="Function name"
description="Postgres function which will be called by Supabase Auth each time the hook is triggered."
label="Postgres function"
description="This function will be called by Supabase Auth each time the hook is triggered"
>
<FormControl_Shadcn_>
<FunctionSelector
@@ -392,7 +398,7 @@ export const CreateHookSheet = ({
onChange={field.onChange}
disabled={field.disabled}
filterFunction={(func) => {
if (func.return_type === 'json' || func.return_type === 'jsonb') {
if (supportedReturnTypes.includes(func.return_type)) {
const { value } = convertArgumentTypes(func.argument_types)
if (value.length !== 1) return false
return value[0].type === 'json' || value[0].type === 'jsonb'
@@ -403,7 +409,11 @@ export const CreateHookSheet = ({
<span>
No function with a single JSON/B argument
<br />
and JSON/B return type found in this schema.
and JSON/B
{definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED'
? ' or void'
: ''}{' '}
return type found in this schema.
</span>
}
/>
@@ -413,8 +423,8 @@ export const CreateHookSheet = ({
/>
</div>
<div className="h-72 w-full gap-3 flex flex-col">
<p className="text-sm text-foreground-light">
The following statements will be executed on the function:
<p className="text-sm text-foreground-light px-5">
The following statements will be executed on the selected function:
</p>
<CodeEditor
id="postgres-hook-editor"
@@ -100,7 +100,9 @@ export const HookCard = ({ hook, onSelect }: HookCardProps) => {
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to configure auth hooks',
text: !canUpdateAuthHook
? 'You need additional permissions to configure auth hooks'
: undefined,
},
}}
>
@@ -68,9 +68,6 @@ const RLSCodeEditor = ({
lineNumbersMinChars: 4,
folding: undefined,
scrollBeyondLastLine: false,
padding: {
top: editView === 'conversation' ? 20 : 0,
},
}
const onMount: OnMount = async (editor, monaco) => {
@@ -804,7 +804,9 @@ export const AIPolicyEditorPanel = memo(function ({
tooltip={{
content: {
side: 'top',
text: 'You need additional permissions to update policies',
text: !canUpdatePolicies
? 'You need additional permissions to update policies'
: undefined,
},
}}
>
@@ -3,7 +3,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import Panel from 'components/ui/Panel'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -20,7 +20,6 @@ import {
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
interface PolicyRowProps {
policy: PostgresPolicy
@@ -1,12 +1,13 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { noop } from 'lodash'
import { Lock, Unlock } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useQueryState } from 'nuqs'
import type { PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
AiIconAnimation,
Badge,
@@ -29,8 +30,7 @@ const PolicyTableRowHeader = ({
onSelectToggleRLS = noop,
onSelectCreatePolicy,
}: PolicyTableRowHeaderProps) => {
const router = useRouter()
const { ref } = router.query
const { ref } = useParams()
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
@@ -42,7 +42,11 @@ const PolicyTableRowHeader = ({
return (
<div id={table.id.toString()} className="flex w-full items-center justify-between">
<div className="flex gap-x-4 text-left">
<Link href={`/project/${ref}/editor/${table.id}`} className="flex items-center gap-x-2">
<EditorTablePageLink
projectRef={ref}
id={String(table.id)}
className="flex items-center gap-x-2"
>
{table.rls_enabled ? (
<div className="flex items-center gap-x-1 text-xs">
<Lock size={14} strokeWidth={2} className="text-brand" />
@@ -53,7 +57,7 @@ const PolicyTableRowHeader = ({
</div>
)}
<h4 className="m-0">{table.name}</h4>
</Link>
</EditorTablePageLink>
<div className="flex items-center gap-x-2">
{isTableLocked && (
<Badge>
@@ -75,7 +79,9 @@ const PolicyTableRowHeader = ({
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to toggle RLS',
text: !canToggleRLS
? 'You need additional permissions to toggle RLS'
: undefined,
},
}}
>
@@ -90,7 +96,9 @@ const PolicyTableRowHeader = ({
content: {
side: 'bottom',
text: !canToggleRLS
? 'You need additional permissions to create RLS policies'
? !canToggleRLS
? 'You need additional permissions to create RLS policies'
: undefined
: undefined,
},
}}
@@ -111,7 +119,7 @@ const PolicyTableRowHeader = ({
<AiIconAnimation className="scale-75 [&>div>div]:border-black dark:[&>div>div]:border-white" />
</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="top">
<TooltipContent_Shadcn_ side="bottom">
{!canToggleRLS
? 'You need additional permissions to create RLS policies'
: 'Create with Supabase Assistant'}
@@ -53,7 +53,7 @@ const RateLimits = () => {
})
const canUpdateEmailLimit = authConfig?.EXTERNAL_EMAIL_ENABLED && isSmtpEnabled(authConfig)
const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED && !authConfig?.SMS_AUTOCONFIRM
const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED
const canUpdateAnonymousUsersRateLimit = authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED
const FormSchema = z.object({
@@ -408,8 +408,8 @@ const RateLimits = () => {
<FormSectionLabel
description={
<p className="text-foreground-light text-sm">
Number of sign up and sign-in requests that can be made per hour per IP
address (excludes anonymous users).
Number of sign up and sign-in requests that can be made in a 5 minute
interval per IP address (excludes anonymous users).
</p>
}
>
@@ -72,7 +72,9 @@ export const RedirectUrlList = ({
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to remove redirect URLs',
text: !canUpdateConfig
? 'You need additional permissions to remove redirect URLs'
: undefined,
},
}}
icon={<Trash />}
@@ -88,7 +90,9 @@ export const RedirectUrlList = ({
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to update redirect URLs',
text: !canUpdateConfig
? 'You need additional permissions to update redirect URLs'
: undefined,
},
}}
>
@@ -1,10 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod'
import dayjs from 'dayjs'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { toast } from 'sonner'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { useUserUpdateMutation } from 'data/auth/user-update-mutation'
import { User } from 'data/auth/users-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import {
Button,
cn,
@@ -20,10 +24,6 @@ import {
Separator,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useUserUpdateMutation } from 'data/auth/user-update-mutation'
import { User } from 'data/auth/users-query'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useParams } from 'common'
interface BanUserModalProps {
visible: boolean
@@ -34,7 +34,7 @@ interface BanUserModalProps {
export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => {
const { ref: projectRef } = useParams()
const { data: apiData } = useProjectApiQuery({ projectRef })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { mutate: updateUser, isLoading: isBanningUser } = useUserUpdateMutation({
onSuccess: (_, vars) => {
const bannedUntil = dayjs()
@@ -62,19 +62,24 @@ export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => {
const bannedUntil = dayjs().add(Number(value), unit).format('DD MMM YYYY HH:mm (ZZ)')
const onSubmit = (data: FormType) => {
if (!apiData) {
if (!settings) {
return toast.error(`Failed to ban user: Error loading project config`)
} else if (user.id === undefined) {
return toast.error(`Failed to ban user: User ID not found`)
}
const durationHours = data.unit === 'hours' ? Number(data.value) : Number(data.value) * 24
const { protocol, endpoint, serviceApiKey } = apiData.autoApiService
const endpoint = settings.app_config?.endpoint
const { serviceKey } = getAPIKeys(settings)
if (!endpoint) return toast.error(`Failed to ban user: Unable to retrieve API endpoint`)
if (!serviceKey?.api_key) return toast.error(`Failed to ban user: Unable to retrieve API key`)
updateUser({
projectRef,
protocol,
protocol: 'https',
endpoint,
serviceApiKey,
serviceApiKey: serviceKey.api_key,
userId: user.id,
banDuration: durationHours,
})
@@ -1,13 +1,13 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { Lock, Mail } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { useUserCreateMutation } from 'data/auth/user-create-mutation'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
@@ -40,7 +40,11 @@ const CreateUserFormSchema = z.object({
const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => {
const { ref: projectRef } = useParams()
const { data, isLoading, isSuccess } = useProjectApiQuery({ projectRef }, { enabled: visible })
const {
data: settings,
isLoading,
isSuccess,
} = useProjectSettingsV2Query({ projectRef }, { enabled: visible })
const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user')
@@ -56,8 +60,20 @@ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => {
if (!isSuccess) {
return toast.error(`Failed to create user: Error loading project config`)
}
const { protocol, endpoint, serviceApiKey } = data.autoApiService
createUser({ projectRef, endpoint, protocol, serviceApiKey, user: values })
const endpoint = settings?.app_config?.endpoint
const { serviceKey } = getAPIKeys(settings)
if (!endpoint) return toast.error(`Failed to create user: Unable to retrieve API endpoint`)
if (!serviceKey?.api_key)
return toast.error(`Failed to create user: Unable to retrieve API key`)
createUser({
projectRef,
endpoint,
protocol: 'https',
serviceApiKey: serviceKey.api_key,
user: values,
})
}
const form = useForm<z.infer<typeof CreateUserFormSchema>>({
@@ -9,7 +9,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { User } from 'data/auth/users-query'
import useLogsPreview from 'hooks/analytics/useLogsPreview'
import { Button, cn, CriticalIcon, Separator } from 'ui'
import { Admonition } from 'ui-patterns'
import { Admonition, TimestampInfo } from 'ui-patterns'
import { UserHeader } from './UserHeader'
import { PANEL_PADDING } from './UserPanel'
@@ -103,7 +103,6 @@ export const UserLogs = ({ user }: UserLogsProps) => {
<div className="border border-b-0 rounded-t divide-y">
{authLogs.map((log) => {
const status = ((log.status ?? '-') as any).toString()
const formattedTime = dayjs(log.timestamp).format('DD MMM HH:mm:ss')
const is400 = status.startsWith('4')
const is500 = status.startsWith('5')
@@ -112,8 +111,8 @@ export const UserLogs = ({ user }: UserLogsProps) => {
key={log.id}
className="flex items-center transition font-mono px-2 py-1.5 bg-surface-100 divide-x"
>
<p className="text-xs text-foreground-light min-w-[120px] w-[120px] px-1">
{formattedTime}
<p className="text-xs text-foreground-light min-w-[125px] w-[125px] px-1">
<TimestampInfo value={log.timestamp / 1000} />
</p>
<div className="flex items-center text-xs text-foreground-light h-[22px] min-w-[70px] w-[70px] px-2">
<div
@@ -16,7 +16,7 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut
import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation'
import { useUserUpdateMutation } from 'data/auth/user-update-mutation'
import { User } from 'data/auth/users-query'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import { timeout } from 'lib/helpers'
@@ -76,7 +76,8 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
const [isDeleteFactorsModalOpen, setIsDeleteFactorsModalOpen] = useState(false)
const { data } = useAuthConfigQuery({ projectRef })
const { data: apiData } = useProjectApiQuery({ projectRef })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const mailerOtpExpiry = data?.MAILER_OTP_EXP ?? 0
const minutes = Math.floor(mailerOtpExpiry / 60)
const seconds = Math.floor(mailerOtpExpiry % 60)
@@ -142,18 +143,23 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
}
const handleUnban = () => {
if (!apiData) {
if (!settings) {
return toast.error(`Failed to ban user: Error loading project config`)
} else if (user.id === undefined) {
return toast.error(`Failed to ban user: User ID not found`)
}
const { protocol, endpoint, serviceApiKey } = apiData.autoApiService
const endpoint = settings.app_config?.endpoint
const { serviceKey } = getAPIKeys(settings)
if (!endpoint) return toast.error(`Failed to unban user: Unable to retrieve API endpoint`)
if (!serviceKey?.api_key) return toast.error(`Failed to unban user: Unable to retrieve API key`)
updateUser({
projectRef,
protocol,
protocol: 'https',
endpoint,
serviceApiKey,
serviceApiKey: serviceKey.api_key,
userId: user.id,
banDuration: 'none',
})
@@ -515,6 +521,8 @@ const RowAction = ({
}
className?: string
}) => {
const disabled = button?.disabled ?? false
return (
<div className={cn(CONTAINER_CLASS, className)}>
<div>
@@ -529,11 +537,13 @@ const RowAction = ({
icon={success ? <Check className="text-brand" /> : button.icon}
loading={button.isLoading ?? false}
onClick={button.onClick}
disabled={button?.disabled ?? false}
disabled={disabled}
tooltip={{
content: {
side: 'bottom',
text: `You need additional permissions to ${button.text.toLowerCase()}`,
text: disabled
? `You need additional permissions to ${button.text.toLowerCase()}`
: undefined,
},
}}
>
@@ -115,6 +115,20 @@ const phoneProviders = providers.phone.map((x) => {
return key
})
function toPrettyJsonString(value: unknown): string | undefined {
if (!value) return undefined
if (typeof value === 'string') return value
if (Array.isArray(value)) return value.map((item) => toPrettyJsonString(item)).join(' ')
try {
return JSON.stringify(value)
} catch (error) {
// ignore the error
}
return undefined
}
export function getDisplayName(user: User, fallback = '-'): string {
const {
custom_claims,
@@ -149,39 +163,44 @@ export function getDisplayName(user: User, fallback = '-'): string {
first_name: cc_first_name,
} = custom_claims ?? {}
const last =
const last = toPrettyJsonString(
familyName ||
family_name ||
surname ||
lastName ||
last_name ||
ccFamilyName ||
cc_family_name ||
ccSurname ||
ccLastName ||
cc_last_name
family_name ||
surname ||
lastName ||
last_name ||
ccFamilyName ||
cc_family_name ||
ccSurname ||
ccLastName ||
cc_last_name
)
const first =
const first = toPrettyJsonString(
givenName ||
given_name ||
firstName ||
first_name ||
ccGivenName ||
cc_given_name ||
ccFirstName ||
cc_first_name
given_name ||
firstName ||
first_name ||
ccGivenName ||
cc_given_name ||
ccFirstName ||
cc_first_name
)
return (
displayName ||
display_name ||
ccDisplayName ||
cc_display_name ||
fullName ||
full_name ||
ccFullName ||
cc_full_name ||
(first && last && `${first} ${last}`) ||
fallback
toPrettyJsonString(
displayName ||
display_name ||
ccDisplayName ||
cc_display_name ||
fullName ||
full_name ||
ccFullName ||
cc_full_name ||
(first && last && `${first} ${last}`) ||
last ||
first
) || fallback
)
}
@@ -210,9 +229,12 @@ export function getAvatarUrl(user: User): string | undefined {
profile_url ||
profileImageUrl ||
profileImageURL ||
profile_image_url) as string | undefined
profile_image_url ||
'') as unknown
return SUPPORTED_CSP_AVATAR_URLS.some((x) => (url ?? '').startsWith(x)) ? url : undefined
if (typeof url !== 'string') return undefined
const isSupported = SUPPORTED_CSP_AVATAR_URLS.some((x) => url.startsWith(x))
return isSupported ? url : undefined
}
export const formatUserColumns = ({
@@ -216,7 +216,9 @@ const BranchManagement = () => {
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create branches',
text: !canCreateBranches
? 'You need additional permissions to create branches'
: undefined,
},
}}
>
@@ -271,7 +273,9 @@ const BranchManagement = () => {
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to disable branching',
text: !canDisableBranching
? 'You need additional permissions to disable branching'
: undefined,
},
}}
>
@@ -29,7 +29,9 @@ export const BranchingEmptyState = () => {
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to enable branching',
text: !canEnableBranching
? 'You need additional permissions to enable branching'
: undefined,
},
}}
>
@@ -40,7 +40,9 @@ const CreateWarehouseAccessToken = ({ onSubmit, loading, open, setOpen }: Create
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create access tokens',
text: !canCreateAccessTokens
? 'You need additional permissions to create access tokens'
: undefined,
},
}}
>
@@ -19,6 +19,8 @@ import DatePickers from '../Settings/Logs/Logs.DatePickers'
import { DatetimeHelper } from '../Settings/Logs/Logs.types'
import dayjs from 'dayjs'
import { useKeyboardShortcuts } from 'components/grid/components/common/Hooks'
import { useWarehouseLogDetailQuery } from 'data/analytics/warehouse-collection-log-detail-query'
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
const INTERVALS: DatetimeHelper[] = [
{
@@ -154,6 +156,19 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset}
</span>
)
}
// Selected Log Detail
const [selectedLogId, setSelectedLogId] = useSelectedLog()
const {
data: selectedLogDetail,
isLoading: selectedLogDetailLoading,
error: selectedLogDetailError,
} = useWarehouseLogDetailQuery({
ref: projectRef,
collectionName: collection?.name,
logId: selectedLogId ?? undefined,
})
return (
<div className="relative flex flex-col flex-grow h-full">
<ShimmerLine active={isLoading} />
@@ -248,9 +263,11 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset}
hasEditorValue={false}
projectRef={projectRef}
isLoading={isLoading}
onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)}
selectedLog={selectedLogDetail?.result[0] ?? undefined}
selectedLogError={selectedLogDetailError ?? undefined}
isSelectedLogLoading={!!selectedLogId && selectedLogDetailLoading}
data={results}
params={{ sql }}
maxHeight="calc(100vh - 139px)"
showHeader={false}
EmptyState={
<ProductEmptyState title="No events found" size="large">
@@ -277,20 +294,18 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset}
</LoadingOpacity>
{!isError && (
<div className="border-t flex flex-row justify-between p-2">
<div className="flex items-center gap-2">
<div className="border-t flex flex-row justify-between">
<div className="flex items-center gap-2 p-2">
{results.length > 0 && (
<>
<Button
onClick={loadMore}
icon={<Rewind />}
type="default"
loading={isLoading}
disabled={isLoading}
>
Load more
</Button>
</>
<Button
onClick={loadMore}
icon={<Rewind />}
type="default"
loading={isLoading}
disabled={isLoading}
>
Load more
</Button>
)}
{filters.offset !== 0 && (
<>
@@ -1,5 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { includes, noop, sortBy } from 'lodash'
import { Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react'
import { useRouter } from 'next/router'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -7,12 +8,13 @@ import Table from 'components/to-be-cleaned/Table'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Edit3, FileText, MoreVertical, Trash } from 'lucide-react'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
@@ -33,6 +35,7 @@ const FunctionList = ({
}: FunctionListProps) => {
const router = useRouter()
const { project: selectedProject } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const { data: functions } = useDatabaseFunctionsQuery({
projectRef: selectedProject?.ref,
@@ -116,11 +119,21 @@ const FunctionList = ({
</DropdownMenuItem>
)}
<DropdownMenuItem className="space-x-2" onClick={() => editFunction(x)}>
<Edit3 size={14} />
<Edit2 size={14} />
<p>Edit function</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => {
setAiAssistantPanel({ open: true, editor: 'functions', entity: x })
}}
>
<Edit size={14} />
<p>Edit function with Assistant</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="space-x-2" onClick={() => deleteFunction(x)}>
<Trash stroke="red" size={14} />
<Trash size={14} className="text-destructive" />
<p>Delete function</p>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -5,6 +5,7 @@ import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
@@ -17,7 +18,8 @@ import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { Input } from 'ui'
import { useAppStateSnapshot } from 'state/app-state'
import { AiIconAnimation, Input } from 'ui'
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
import FunctionList from './FunctionList'
@@ -32,10 +34,13 @@ const FunctionsList = ({
editFunction = noop,
deleteFunction = noop,
}: FunctionsListProps) => {
const { project } = useProjectContext()
const router = useRouter()
const { search } = useParams()
const { project } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const enableFunctionsAssistant = useIsDatabaseFunctionsAssistantEnabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const filterString = search ?? ''
const setFilterString = (str: string) => {
@@ -122,20 +127,51 @@ const FunctionsList = ({
/>
</div>
{!isLocked && (
<ButtonTooltip
disabled={!canCreateFunctions}
onClick={() => createFunction()}
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create functions',
},
}}
>
Create a new function
</ButtonTooltip>
)}
<div className="flex items-center gap-x-2">
{!isLocked && (
<>
<ButtonTooltip
disabled={!canCreateFunctions}
onClick={() => createFunction()}
tooltip={{
content: {
side: 'bottom',
text: !canCreateFunctions
? 'You need additional permissions to create functions'
: undefined,
},
}}
>
Create a new function
</ButtonTooltip>
{enableFunctionsAssistant && (
<ButtonTooltip
type="default"
disabled={!canCreateFunctions}
className="px-1 pointer-events-auto"
icon={
<AiIconAnimation className="scale-75 [&>div>div]:border-black dark:[&>div>div]:border-white" />
}
onClick={() =>
setAiAssistantPanel({
open: true,
editor: 'functions',
entity: undefined,
})
}
tooltip={{
content: {
side: 'bottom',
text: !canCreateFunctions
? 'You need additional permissions to create functions'
: 'Create with Supabase Assistant',
},
}}
/>
)}
</>
)}
</div>
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
@@ -1,10 +1,11 @@
import clsx from 'clsx'
import { ChevronDown, Plus, Trash } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common/hooks'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { uuidv4 } from 'lib/helpers'
import {
@@ -19,7 +20,6 @@ import {
SidePanel,
} from 'ui'
import { HTTPArgument } from './EditHookPanel'
import { Trash, Plus, ChevronDown } from 'lucide-react'
interface HTTPRequestFieldsProps {
type: 'http_request' | 'supabase_function'
@@ -48,12 +48,12 @@ const HTTPRequestFields = ({
}: HTTPRequestFieldsProps) => {
const { project: selectedProject } = useProjectContext()
const { ref } = useParams()
const { data: settings } = useProjectApiQuery({ projectRef: ref })
const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
const edgeFunctions = functions ?? []
const apiService = settings?.autoApiService
const apiKey = apiService?.serviceApiKey ?? '[YOUR API KEY]'
const { serviceKey } = getAPIKeys(settings)
const apiKey = serviceKey?.api_key ?? '[YOUR API KEY]'
return (
<>
@@ -1,4 +1,4 @@
import { Check, ChevronsUpDown } from 'lucide-react'
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
@@ -50,7 +50,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
const [selectedIndexType, setSelectedIndexType] = useState<string>(INDEX_TYPES[0].value)
const [schemaDropdownOpen, setSchemaDropdownOpen] = useState(false)
const [tableDropdownOpen, setTableDropdownOpen] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const { refetch: refetchIndexes } = useIndexesQuery({
schema: selectedSchema,
projectRef: project?.ref,
@@ -60,10 +60,10 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: entities, isLoading } = useEntityTypesQuery({
schema: selectedSchema,
const { data: entities, isLoading: isLoadingEntities } = useEntityTypesQuery({
schemas: [selectedSchema],
sort: 'alphabetical',
search: undefined,
search: searchTerm,
projectRef: project?.ref,
connectionString: project?.connectionString,
})
@@ -94,6 +94,10 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
[entities?.pages]
)
function handleSearchChange(value: string) {
setSearchTerm(value)
}
const columns = tableColumns?.result[0]?.columns ?? []
const columnOptions: MultiSelectOption[] = columns
.filter((column): column is NonNullable<typeof column> => column !== null)
@@ -181,7 +185,11 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find schema..." />
<CommandInput_Shadcn_
placeholder="Find table..."
value={searchTerm}
onValueChange={handleSearchChange}
/>
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No schemas found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
@@ -218,7 +226,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
name="select-table"
description={
isSelectEntityDisabled &&
!isLoading &&
!isLoadingEntities &&
'Create a table in this schema via the Table or SQL editor first'
}
isReactForm={false}
@@ -228,28 +236,27 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
open={tableDropdownOpen}
onOpenChange={setTableDropdownOpen}
>
<PopoverTrigger_Shadcn_ asChild disabled={isSelectEntityDisabled || isLoading}>
{isLoading ? (
<ShimmeringLoader className="h-[38px]" />
) : (
<Button
type="default"
size="medium"
className={cn(
'w-full [&>span]:w-full text-left',
selectedEntity === '' && 'text-foreground-lighter'
)}
iconRight={
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
}
>
{selectedEntity !== undefined && selectedEntity !== ''
? selectedEntity
: isSelectEntityDisabled
? 'No tables available in schema'
: 'Choose a table'}
</Button>
)}
<PopoverTrigger_Shadcn_
asChild
disabled={isSelectEntityDisabled || isLoadingEntities}
>
<Button
type="default"
size="medium"
className={cn(
'w-full [&>span]:w-full text-left',
selectedEntity === '' && 'text-foreground-lighter'
)}
iconRight={
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
}
>
{selectedEntity !== undefined && selectedEntity !== ''
? selectedEntity
: isSelectEntityDisabled
? 'No tables available in schema'
: 'Choose a table'}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
@@ -257,10 +264,25 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
align="start"
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find table..." />
{/* [Terry] shouldFilter context:
https://github.com/pacocoursey/cmdk/issues/267#issuecomment-2252717107 */}
<Command_Shadcn_ shouldFilter={false}>
<CommandInput_Shadcn_
placeholder="Find table..."
value={searchTerm}
onValueChange={handleSearchChange}
/>
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No tables found</CommandEmpty_Shadcn_>
<CommandEmpty_Shadcn_>
{isLoadingEntities ? (
<div className="flex items-center gap-2 text-center justify-center">
<Loader2 size={12} className="animate-spin" />
Loading...
</div>
) : (
'No tables found'
)}
</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={(entityTypes || []).length > 7 ? 'h-[210px]' : ''}>
{(entityTypes ?? []).map((entity) => (
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import SchemaSelector from 'components/ui/SchemaSelector'
@@ -13,10 +14,9 @@ import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Button, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
import { TableNode } from 'ui-patterns/SchemaTableNode'
import { TableNode } from './SchemaTableNode'
import { SchemaGraphLegend } from './SchemaGraphLegend'
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
import { useParams } from 'common'
// [Joshen] Persisting logic: Only save positions to local storage WHEN a node is moved OR when explicitly clicked to reset layout
@@ -1,13 +1,17 @@
import { DiamondIcon, Fingerprint, Hash, Key, Table2 } from 'lucide-react'
import { DiamondIcon, ExternalLink, Fingerprint, Hash, Key, Link2, Table2 } from 'lucide-react'
import { Handle, NodeProps } from 'reactflow'
import { cn } from 'ui'
import Link from 'next/link'
import { Button, cn, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
// ReactFlow is scaling everything by the factor of 2
const TABLE_NODE_WIDTH = 320
const TABLE_NODE_ROW_HEIGHT = 40
export type TableNodeData = {
id?: number
name: string
ref: string
isForeign: boolean
columns: {
id: string
@@ -48,12 +52,21 @@ const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps<TableNode
>
<header
className={cn(
'text-[0.55rem] px-2 bg-alternative text-default flex gap-1 items-center',
'text-[0.55rem] pl-2 pr-1 bg-alternative text-default flex items-center justify-between',
itemHeight
)}
>
<Table2 strokeWidth={1} size={12} className="text-light" />
{data.name}
<div className="flex gap-x-1 items-center">
<Table2 strokeWidth={1} size={12} className="text-light" />
{data.name}
</div>
{data.id && (
<Button asChild type="text" className="px-0 w-[16px] h-[16px] rounded">
<Link href={`/project/${data.ref}/editor/${data.id}`}>
<ExternalLink size={10} className="text-foreground-light" />
</Link>
</Button>
)}
</header>
{data.columns.map((column) => (
@@ -63,8 +76,6 @@ const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps<TableNode
'bg-surface-100',
'border-t',
'border-t-[0.5px]',
// 'odd:bg-scale-300',
// 'even:bg-scale-400',
'hover:bg-scale-500 transition cursor-default',
itemHeight
)}
@@ -6,22 +6,7 @@ import 'reactflow/dist/style.css'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH } from 'ui-patterns/SchemaTableNode'
type TableNodeData = {
name: string
isForeign: boolean
columns: {
id: string
isPrimary: boolean
isNullable: boolean
isUnique: boolean
isUpdateable: boolean
isIdentity: boolean
name: string
format: string
}[]
}
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH, TableNodeData } from './SchemaTableNode'
const NODE_SEP = 25
const RANK_SEP = 50
@@ -56,10 +41,12 @@ export async function getGraphDataFromTables(
id: `${table.id}`,
type: 'table',
data: {
ref,
id: table.id,
name: table.name,
isForeign: false,
columns,
},
} as TableNodeData,
position: { x: 0, y: 0 },
}
})
@@ -83,10 +70,11 @@ export async function getGraphDataFromTables(
id: rel.constraint_name,
type: 'table',
data: {
ref,
name: `${rel.target_table_schema}.${rel.target_table_name}.${rel.target_column_name}`,
isForeign: true,
columns: [],
},
} as TableNodeData,
position: { x: 0, y: 0 },
})
@@ -72,7 +72,9 @@ const ColumnList = ({
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create columns',
text: !canUpdateColumns
? 'You need additional permissions to create columns'
: undefined,
},
}}
>
@@ -30,12 +30,12 @@ import { useDatabasePublicationsQuery } from 'data/database-publications/databas
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useForeignTablesQuery } from 'data/foreign-tables/foreign-tables-query'
import { useMaterializedViewsQuery } from 'data/materialized-views/materialized-views-query'
import { usePrefetchEditorTablePage } from 'data/prefetchers/project.$ref.editor.$id'
import { useTablesQuery } from 'data/tables/tables-query'
import { useViewsQuery } from 'data/views/views-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
Button,
Checkbox_Shadcn_,
@@ -74,6 +74,8 @@ const TableList = ({
const { ref } = useParams()
const { project } = useProjectContext()
const prefetchEditorTablePage = usePrefetchEditorTablePage()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [filterString, setFilterString] = useState<string>('')
@@ -273,7 +275,9 @@ const TableList = ({
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to create tables',
text: !canUpdateTables
? 'You need additional permissions to create tables'
: undefined,
},
}}
>
@@ -476,6 +480,9 @@ const TableList = ({
onClick={() =>
router.push(`/project/${project?.ref}/editor/${x.id}`)
}
onMouseEnter={() =>
prefetchEditorTablePage({ id: x.id ? String(x.id) : undefined })
}
>
<Eye size={12} />
<p>View in Table Editor</p>
@@ -125,9 +125,7 @@ const TriggerList = ({
{canUpdateTriggers ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1">
<MoreVertical />
</Button>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-36">
<DropdownMenuItem className="space-x-2" onClick={() => editTrigger(x)}>
@@ -52,7 +52,7 @@ const CreateWrapper = () => {
const hasNewSchema = newTables.some((table) => table.is_new_schema)
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
router.push(`/project/${ref}/integrations/wrappers`)
},
})
@@ -89,7 +89,7 @@ const CreateWrapper = () => {
</p>
</div>
<Button asChild type="default">
<Link href={`/project/${ref}/database/wrappers`}>Head back</Link>
<Link href={`/project/${ref}/integrations/wrappers`}>Head back</Link>
</Button>
</div>
)
@@ -142,7 +142,7 @@ const CreateWrapper = () => {
'absolute -left-20 top-1 opacity-75 hover:opacity-100',
].join(' ')}
>
<Link href={`/project/${ref}/database/wrappers`}>
<Link href={`/project/${ref}/integrations/wrappers`}>
<div className="flex items-center space-x-2">
<ArrowLeft strokeWidth={1.5} size={14} />
<p className="text-sm">Back</p>
@@ -62,7 +62,7 @@ const EditWrapper = () => {
const hasNewSchema = wrapperTables.some((table) => table.is_new_schema)
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
router.push(`/project/${ref}/integrations/wrappers`)
},
})
@@ -117,7 +117,7 @@ const EditWrapper = () => {
</p>
</div>
<Button asChild type="default">
<Link href={`/project/${ref}/database/wrappers`}>Head back</Link>
<Link href={`/project/${ref}/integrations/wrappers`}>Head back</Link>
</Button>
</div>
)
@@ -169,7 +169,7 @@ const EditWrapper = () => {
'absolute -left-20 top-1 opacity-75 hover:opacity-100',
].join(' ')}
>
<Link href={`/project/${ref}/database/wrappers`}>
<Link href={`/project/${ref}/integrations/wrappers`}>
<div className="flex items-center space-x-2">
<ArrowLeft strokeWidth={1.5} size={14} />
<p className="text-sm">Back</p>
Loaded 100 of 431 files, more files were not shown because too many files have changed in this diff. Show more