mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
fix conflicts
This commit is contained in:
commit
d484a117ab
431 files changed
+11387
-4280
No files matched your search
@@ -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" />
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 || '')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
+21
-13
@@ -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 && (
|
||||
<>
|
||||
|
||||
+16
-3
@@ -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>
|
||||
|
||||
+52
-16
@@ -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
|
||||
|
||||
|
||||
+18
-7
@@ -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
Reference in new issue
Block a user