Merge pull request #11292 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-01-03 13:05:47 +07:00
commit 902aeb2884
149 files changed
+3420 -4666

No files matched your search

+1
View File
@@ -2,3 +2,4 @@
/apps/www/ @supabase/frontend
/apps/docs/ @supabase/docs
/spec/ @supabase/docs
/docker/ @supabase/cli
+13 -4
View File
@@ -13,8 +13,13 @@ jobs:
uses: actions/checkout@v2
# disable this check while we migrate to monorepo
- name: Run prettier
run: |-
npx prettier -c 'apps/**/*.{js,jsx,ts,tsx,css,md,json}'
uses: EPMatt/reviewdog-action-prettier@v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-review
level: warning
command: npx prettier -c 'apps/**/*.{js,jsx,ts,tsx,css,md,json}'
# run: |-
# function run_check() {
# if $(grep -q '"prettier"' "$1") ; then
@@ -35,5 +40,9 @@ jobs:
- name: Check out repo
uses: actions/checkout@v2
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,json}'
uses: EPMatt/reviewdog-action-prettier@v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-review
level: warning
command: npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,json}'
@@ -0,0 +1,6 @@
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click on [`Providers`](https://app.supabase.com/project/_/auth/providers) under the Configuration section
- Click on **{props.provider}** from the accordion list to expand and turn **{props.provider} Enabled** to ON
- Enter your **{props.provider} Client ID** and **{props.provider} Client Secret** saved in the previous step
- Click `Save`
@@ -0,0 +1,8 @@
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- Click on the `Authentication` icon in the left sidebar
- Click on [`Providers`](https://app.supabase.com/project/_/auth/providers) under the Configuration section
- Click on **{props.provider}** from the accordion list to expand and you'll find your **Redirect URL**, you can click `Copy` to copy it to the clipboard
@@ -14,7 +14,7 @@ export const REFERENCES: References = {
name: 'supabase-js',
library: 'supabase-js',
versions: ['v2', 'v1'],
icon: '/docs/img/libraries/javascript-icon.svg',
icon: '/img/libraries/javascript-icon',
},
dart: {
name: 'Flutter',
@@ -688,22 +688,22 @@ export const reference = {
url: '/reference/javascript/start',
level: 'reference_javascript',
items: [],
icon: 'docs/img/icons/javascript.svg',
icon: '/img/icons/menu/reference-javascript',
},
{
name: 'supabase-dart',
url: '/reference/dart/start',
level: 'reference_dart',
items: [],
icon: 'docs/img/icons/javascript.svg',
},
{
name: 'supbase-python',
url: '/reference/python/start',
level: 'reference_python',
items: [],
icon: 'docs/img/icons/javascript.svg',
icon: '/img/icons/menu/reference-dart',
},
// {
// name: 'supabase-python',
// url: '/reference/python/start',
// level: 'reference_python',
// items: [],
// icon: '/img/icons/menu/reference-javascript',
// },
],
},
{
@@ -713,13 +713,13 @@ export const reference = {
name: 'Supabase CLI',
url: '/reference/cli/start',
items: [],
icon: 'docs/img/icons/cli-icon.svg',
icon: '/img/icons/menu/reference-cli',
},
{
name: 'Management API',
url: '/reference/javascript',
items: [],
icon: 'docs/img/icons/javascript-icon.svg',
icon: '/img/icons/menu/reference-api',
},
],
},
@@ -62,7 +62,7 @@ export type RefKeyOptions =
| 'self-hosting-storage'
| 'self-hosting-realtime'
const SideNav = () => {
const NavigationMenu = () => {
const router = useRouter()
let version = ''
@@ -270,4 +270,4 @@ const SideNav = () => {
)
}
export default memo(SideNav)
export default memo(NavigationMenu)
@@ -0,0 +1,6 @@
// check if the link is allowed to be displayed
export function isFuncNotInLibraryOrVersion(id, type, allowedKeys) {
if (id && allowedKeys && !allowedKeys.includes(id) && type !== 'markdown') {
return true
}
}
@@ -137,7 +137,14 @@ const ContentLink = React.memo(function ContentLink(props: any) {
: 'hover:text-brand-900 text-scale-1000',
].join(' ')}
>
{props.icon && <img className="w-3" src={`${router.basePath}${props.icon}`} />}
{props.icon && (
<Image
alt={props.icon}
width={12}
height={12}
src={`${router.basePath}${props.icon}`}
/>
)}
{props.name}
</a>
</Link>
@@ -177,14 +184,15 @@ const Content = (props) => {
</a>
</Link>
{menu.items.map((x, index) => {
{menu.items.map((x) => {
return (
<div key={x.url}>
<div key={x.name}>
{x.items && x.items.length > 0 ? (
<div>
{x.items.map((subItem, subItemIndex) => {
return (
<ContentAccordionLink
key={subItem.name}
subItem={subItem}
subItemIndex={subItemIndex}
parent={x}
@@ -193,7 +201,7 @@ const Content = (props) => {
})}
</div>
) : (
<ContentLink url={x.url} icon={x.icon} name={x.name} />
<ContentLink url={x.url} icon={x.icon} name={x.name} key={x.name} />
)}
</div>
)
@@ -12,8 +12,9 @@ import RevVersionDropdown from '~/components/RefVersionDropdown'
import { useMenuActiveRefId } from '~/hooks/useMenuState'
import { RefIdOptions, RefKeyOptions } from './NavigationMenu'
import React from 'react'
import React, { Fragment } from 'react'
import { generateAllowedClientLibKeys } from '~/lib/refGenerator/helpers'
import { isFuncNotInLibraryOrVersion } from './NavigationMenu.utils'
const HeaderImage = React.memo(function HeaderImage(props: any) {
const router = useRouter()
@@ -131,6 +132,7 @@ const RenderLink = React.memo(function RenderLink(props: any) {
.map((item) => {
return (
<FunctionLink
key={item.title}
library={props.lib}
title={item.title}
id={item.id}
@@ -245,76 +247,56 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
<ul className="function-link-list">
{sections.map((fn: any, fnIndex) => {
//
// check if the link is allowed to be displayed
function isFuncNotInLibraryOrVersion(id, type) {
if (id && allowedKeys && !allowedKeys.includes(id) && type !== 'markdown') {
/*
* Remove this menu link from side bar, as it does not exist for either
* this language, or for this lib version
*
* */
return true
} else {
return false
}
}
// run allow check
if (isFuncNotInLibraryOrVersion(fn.id, fn.type)) {
return <></>
if (isFuncNotInLibraryOrVersion(fn.id, fn.type, allowedKeys)) {
return <Fragment key={fn.id}></Fragment>
}
// handle subtitles with subitems
if (!fn.id) {
return (
<>
<Divider />
<SideMenuTitle title={fn.title} />
{fn.items &&
fn.items
//.filter((item) => item.libs && item.libs.includes(lib))
.map((item) => {
// run allow check
if (isFuncNotInLibraryOrVersion(item.id, item.type)) return <></>
return (
<RenderLink
{...item}
library={menu.title}
index={fnIndex}
modifierIds={modifierIds}
filterIds={filterIds}
authServerIds={authServerIds}
lib={lib}
allowedKeys={allowedKeys}
/>
)
})}
</>
)
} else {
// handle normal links
return (
<>
<RenderLink {...fn} lib={lib} />
{fn.items &&
fn.items
//.filter((item) => item.libs.includes(lib))
.map((item) => (
<RenderLink
{...item}
library={menu.title}
index={fnIndex}
modifierIds={modifierIds}
filterIds={filterIds}
authServerIds={authServerIds}
lib={lib}
allowedKeys={allowedKeys}
/>
))}
</>
)
}
return fn.id ? (
<Fragment key={fn.id}>
<RenderLink {...fn} lib={lib} />
{fn.items &&
fn.items
//.filter((item) => item.libs.includes(lib))
.map((item) => (
<RenderLink
{...item}
library={menu.title}
index={fnIndex}
modifierIds={modifierIds}
filterIds={filterIds}
authServerIds={authServerIds}
lib={lib}
allowedKeys={allowedKeys}
/>
))}
</Fragment>
) : (
<Fragment key={fn.title}>
<Divider />
<SideMenuTitle title={fn.title} />
{fn.items &&
fn.items.map((item, i) => {
// run allow check
if (isFuncNotInLibraryOrVersion(item.id, item.type, allowedKeys))
return <Fragment key={item.id + i}></Fragment>
return (
<RenderLink
{...item}
key={item.id + i}
library={menu.title}
index={fnIndex}
modifierIds={modifierIds}
filterIds={filterIds}
authServerIds={authServerIds}
lib={lib}
allowedKeys={allowedKeys}
/>
)
})}
</Fragment>
)
})}
</ul>
</div>
@@ -42,6 +42,7 @@ const TopNavBarRef: FC = () => {
const key = localStorage.getItem('supabaseDarkMode')
document.documentElement.className = key === 'true' ? 'dark' : ''
document.documentElement.style.colorScheme = key === 'true' ? 'dark' : ''
}
const onSelectVersion = (version: string) => {
+4
View File
@@ -17,6 +17,8 @@ import Extensions from './Extensions'
import { Heading } from './CustomHTMLElements'
import QuickstartIntro from './MDX/quickstart_intro.mdx'
import ProjectSetup from './MDX/project_setup.mdx'
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
import InlineCodeTag from './CustomHTMLElements/InlineCode'
import React from 'react'
@@ -42,6 +44,8 @@ const components = {
JwtGenerator,
QuickstartIntro,
ProjectSetup,
SocialProviderSetup,
SocialProviderSettingsSupabase,
Mermaid,
Extensions,
Alert: (props: any) => (
@@ -31,7 +31,7 @@ const ApiOperationSection = (props) => {
>
{operation.operation}
</span>
{operation.fullPath}
{operation.path}
</code>
</div>
<div className="prose dark:prose-dark py-4">
@@ -9,6 +9,7 @@ import RefSubLayout from '~/layouts/ref/RefSubLayout'
import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers'
import RefDetailCollapse from '~/components/reference/RefDetailCollapse'
import { Fragment } from 'react'
interface ICommonFunc {
id: string
@@ -32,11 +33,6 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
// gracefully return nothing if function does not exist
if (!item) return <></>
if (item && !item['$ref']) {
//console.warn('🚩 issue with $ref in:', item.id)
}
// console.log(item)
const hasTsRef = item['$ref'] || null
const tsDefinition =
@@ -64,11 +60,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
<ReactMarkdown className="text-sm">{item.description}</ReactMarkdown>
</div>
)}
{/* {functionMarkdownContent && (
<div className="prose">
<MDXRemote {...functionMarkdownContent} components={components} />
</div>
)} */}
{item.notes && (
<div className="prose">
<ReactMarkdown className="text-sm">{item.notes}</ReactMarkdown>
@@ -89,25 +81,29 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
})
const paramItem = overide?.length > 0 ? overide[0] : param
return (
<Param {...paramItem}>
<Param {...paramItem} key={param.name}>
{paramItem.subContent && (
<div className="mt-3">
<Options>
{param.subContent.map((param) => {
return (
<>
<Fragment key={param.name + 'subcontent'}>
<Options.Option {...param}>
{param.subContent && (
<Options>
{param.subContent.map((param) => {
return <Options.Option {...param} />
return (
<Options.Option
{...param}
key={param.name + 'subcontent-option'}
/>
)
})}
</Options>
)}
</Options.Option>
</>
</Fragment>
)
})}
</Options>
@@ -134,6 +130,14 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
{item.examples &&
item.examples.map((example, exampleIndex) => {
const exampleString = ''
const codeBlockLang = example?.code?.startsWith('```js')
? 'js'
: example?.code?.startsWith('```ts')
? 'ts'
: example?.code?.startsWith('```dart')
? 'dart'
: 'js'
// `
// import { createClient } from '@supabase/supabase-js'
@@ -150,9 +154,18 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
return (
<Tabs.Panel
id={example.id}
key={example.id}
label={example.name}
className="flex flex-col gap-3"
>
{example.description && (
<div className="prose">
<ReactMarkdown className="text-sm">
{example.description}
</ReactMarkdown>
</div>
)}
{((tables && tables.length > 0) || sql) && (
<RefDetailCollapse
id={`${example.id}-${exampleIndex}-data`}
@@ -191,9 +204,10 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
</RefDetailCollapse>
)}
<CodeBlock
className="useless-code-block-class"
language="js"
language={codeBlockLang}
hideLineNumbers={true}
>
{exampleString +
@@ -201,8 +215,10 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
example.code
.replace(/```/g, '')
.replace('js', '')
.replace('ts', ''))}
.replace('ts', '')
.replace('dart', ''))}
</CodeBlock>
{response && (
<RefDetailCollapse
id={`${example.id}-${exampleIndex}-response`}
@@ -211,7 +227,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
>
<CodeBlock
className="useless-code-block-class"
language="js"
language={codeBlockLang}
hideLineNumbers={true}
>
{response.replace(/```/g, '').replace('json', '')}
@@ -36,16 +36,17 @@ const RefSectionHandler = (props) => {
return (
<RefSubLayout>
{props.sections.map((x) => {
{props.sections.map((x, i) => {
switch (x.type) {
case 'markdown':
const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id)
return <RefEducationSection item={x} markdownContent={markdownData} />
return <RefEducationSection key={x.id + i} item={x} markdownContent={markdownData} />
break
case 'function':
return (
<RefFunctionSection
key={x.id + i}
funcData={x}
commonFuncData={x}
spec={props.spec}
@@ -53,13 +54,21 @@ const RefSectionHandler = (props) => {
/>
)
case 'cli-command':
return <CliCommandSection funcData={x} commonFuncData={x} />
return <CliCommandSection key={x.id + i} funcData={x} commonFuncData={x} />
break
case 'operation':
return <ApiOperationSection funcData={x} commonFuncData={x} spec={props.spec} />
return (
<ApiOperationSection
key={x.id + i}
funcData={x}
commonFuncData={x}
spec={props.spec}
/>
)
default:
return (
<RefFunctionSection
key={x.id + i}
funcData={x}
commonFuncData={x}
spec={props.spec}
@@ -0,0 +1,704 @@
---
id: upgrade-guide
title: Upgrade to supabase-js v2
description: 'Learn how to upgrade to supabase-js v2.'
---
supabase-js v2 focuses on "quality-of-life" improvements for developers and addresses some of the largest pain points in v1. v2 includes type support, a rebuilt Auth library with async methods, improved errors, and more.
No new features will be added to supabase-js v1 , but we'll continuing merging security fixes to v1, with maintenance patches for the next 3 months.
## Upgrade the client library
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
Install the latest version
</RefSubLayout.Details>
<RefSubLayout.Examples>
```sh
npm install @supabase/supabase-js@2
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
_Optionally_ if you are using custom configuration with `createClient` then follow below:
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts title=src/supabaseClient.ts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
schema: 'custom',
persistSession: false,
})
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts title=src/supabaseClient.ts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
db: {
schema: 'custom',
},
auth: {
persistSession: true,
},
})
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
Read more about the [constructor options](/docs/reference/javascript/release-notes#explicit-constructor-options).
### Auth methods
The signIn() method has been deprecated in favor of more explicit method signatures to help with type hinting. Previously it was difficult for developers to know what they were missing (e.g., a lot of developers didn't realize they could use passwordless magic links).
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Sign in with email and password
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { user, error } = await supabase
.auth
.signIn({ email, password })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const {
data: { user },
error,
} = await supabase
.auth
.signInWithPassword({ email, password })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Sign in with magic link
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signIn({ email })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signInWithOtp({ email })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Sign in with a third-party provider
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signIn({ provider })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signInWithOAuth({ provider })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Sign in with phone
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signIn({ phone, password })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.signInWithPassword({ phone, password })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Sign in with phone using OTP
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { error } = await supabase
.auth
.api
.sendMobileOTP(phone)
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.auth
.signInWithOtp({ phone })
// After receiving a SMS with a OTP.
const { data, error } = await supabase
.auth
.verifyOtp({ phone, token })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Reset password for email
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.auth
.api
.resetPasswordForEmail(email)
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.auth
.resetPasswordForEmail(email)
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Get the user's current session
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts
const session = supabase.auth.session()
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts
const {
data: { session },
} = await supabase.auth.getSession()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Get the logged-in user
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts
const user = supabase.auth.user()
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts
const {
data: { session },
} = await supabase.auth.getSession()
const { user } = session
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Update user data for a logged-in user
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { user, error } = await supabase
.auth
.update({ attributes })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts
const {
data: { user },
error,
} = await supabase.auth.updateUser({ attributes })
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Use a custom `access_token` JWT with Supabase
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts
const { user, error } = supabase.auth.setAuth(access_token)
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const supabase = createClient(
SUPABASE_URL,
SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${access_token}`,
},
},
}
)
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Cookie methods
The cookie-related methods like `setAuthCookie` and `getUserByCookie` have been removed.
For Next.js you can use the [Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to help you manage cookies.
If you can't use the Auth Helpers, you can use [server-side rendering](https://supabase.com/docs/guides/auth/server-side-rendering).
Some the [PR](https://github.com/supabase/gotrue-js/pull/340) for additional background information.
### Data methods
`.insert()` / `.upsert()` / `.update()` / `.delete()` don't return rows by default: [PR](https://github.com/supabase/postgrest-js/pull/276).
Previously, these methods return inserted/updated/deleted rows by default (which caused [some confusion](https://github.com/supabase/supabase/discussions/1548)), and you can opt to not return it by specifying `returning: 'minimal'`. Now the default behavior is to not return rows. To return inserted/updated/deleted rows, add a `.select()` call at the end.
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Insert and return data
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.insert({ new_data })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.insert({ new_data })
.select()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Update and return data
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.update({ new_data })
.eq('id', id)
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.update({ new_data })
.eq('id', id)
.select()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Upsert and return data
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.upsert({ new_data })
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.upsert({ new_data })
.select()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Delete and return data
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.delete()
.eq('id', id)
```
</TabPanel>
<TabPanel id="2.0x" label="After">
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('my_table')
.delete()
.eq('id', id)
.select()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Realtime methods
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Subscribe
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts
const userListener = supabase
.from('users')
.on('*', (payload) => handleAllEventsPayload(payload.new))
.subscribe()
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts
const userListener = supabase
.channel('public:user')
.on('postgres_changes', { event: '*', schema: 'public', table: 'user' }, (payload) =>
handleAllEventsPayload()
)
.subscribe()
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
#### Unsubscribe
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="1.0x"
>
<TabPanel id="1.0x" label="Before">
```ts
userListener.unsubscribe()
```
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts
supabase.removeChannel(userListener)
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
+2
View File
@@ -294,8 +294,10 @@ const SiteLayout = ({ children }) => {
if (!key) {
// Default to dark mode if no preference config
document.documentElement.className = 'dark'
document.documentElement.style.colorScheme = 'dark'
} else {
document.documentElement.className = key === 'true' ? 'dark' : ''
document.documentElement.style.colorScheme = key === 'true' ? 'dark' : ''
}
}, [])
+7 -1
View File
@@ -60,6 +60,8 @@ const Layout: FC<Props> = (props) => {
const hasTableOfContents = tocList.length > 0
const ogPageType = asPath.split('/')[2]
return (
<>
<NextSeo
@@ -71,7 +73,11 @@ const Layout: FC<Props> = (props) => {
url: `https://supabase.com/docs${asPath}`,
images: [
{
url: `https://supabase.com/docs/img/supabase-og-image.png`,
url: `https://obuldanrptloktxcffvn.functions.supabase.co/og-images?site=docs${
ogPageType ? `&type=${ogPageType}` : ''
}&title=${encodeURIComponent(props.meta?.title)}&description=${encodeURIComponent(
props.meta?.description
)}`,
},
],
}}
+2 -2
View File
@@ -57,7 +57,7 @@ const RefSubLayout: FC<RefSubLayoutType> & RefSubLayoutSubComponents = (props) =
const Section: FC<ISectionContainer> = (props) => {
return (
<article
key={props.id}
key={props.id + 'section'}
className={[
props.singleColumn ? 'prose dark:prose-dark w-full' : 'w-full',
'py-16 lg:py-32 first:pt-8 last:pb-8',
@@ -150,7 +150,7 @@ const EducationRow: FC<IEducationRow> = (props) => {
const EducationSection: FC<IEducationSection> = ({ icon, hideTitle = false, ...props }) => {
return (
<article
key={props.id}
key={props.id + 'education'}
className={'prose dark:prose-dark max-w-none py-16 lg:py-32 first:pt-8 last:pb-8'}
>
{!hideTitle && <StickyHeader {...props} icon={icon} />}
+1 -1
View File
@@ -62,7 +62,7 @@ A scalable websocket engine for managing user Presence, broadcasting messages, a
- Official Docs: [Supabase Realtime docs](/docs/guides/realtime)
- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime)
- License: [Apache 2](https://github.com/supabase/realtime/blob/master/LICENSE)
- License: [Apache 2](https://github.com/supabase/realtime/blob/main/LICENSE)
- Language: Elixir
### Storage API (large file storage)
+1
View File
@@ -260,6 +260,7 @@ const { data, fetching, error } = result
curl --request POST '<SUPABASE_URL>/graphql/v1' \
-H 'apikey: <SUPABASE_ANON_KEY>' \
-H 'Authorization: Bearer <SUPABASE_ANON_KEY>' \
-H 'Content-Type: application/json' \
-d '{ "query":"{ todos(first: 3) { edges { node { id } } } }" }'
```
@@ -222,7 +222,7 @@ export default function SupabaseListener({ accessToken }) {
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== accessToken) {
router.refresh()
router.reload()
}
})
}, [accessToken])
@@ -322,7 +322,7 @@ Create a `/components/supabase-listener.tsx` file and add the following:
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import supabase from '../utils/supabase'
import supabase from '../utils/supabase-browser'
export default function SupabaseListener({ accessToken }: { accessToken?: string }) {
const router = useRouter()
@@ -440,8 +440,8 @@ And then use a `fetcher` to simulate submitting a form to our action, inside the
const { env, session } = useLoaderData()
const fetcher = useFetcher()
const [supabase] = useState(
() => createBrowserClient < Database > (env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
const [supabase] = useState(() =>
createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
)
const serverAccessToken = session?.access_token
@@ -451,6 +451,8 @@ useEffect(() => {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== serverAccessToken) {
// server and client are out of sync.
// Remix recalls active loaders after actions complete
fetcher.submit(null, {
method: 'post',
action: '/handle-supabase-auth',
@@ -18,7 +18,7 @@ For example, a `public.profiles` table might look like this:
```sql
create table public.profiles (
id uuid references auth.users not null on delete cascade,
id uuid not null references auth.users on delete cascade,
first_name text,
last_name text,
@@ -141,7 +141,7 @@ You should present a form to the user so they can input the 6 digit pin, then se
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '+13334445555',
token: '123456',
type: 'sms',
@@ -257,7 +257,7 @@ The second step is the same as the previous section, you need to collect the 6-d
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '+13334445555',
token: '123456',
type: 'sms',
@@ -57,19 +57,7 @@ This will serve as the `client_id` when you make API calls to authenticate the u
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Apple" />
## Configure your Services ID
@@ -167,13 +155,7 @@ puts token
## Add your OAuth credentials to Supabase
- Go to your [Supabase Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `Apple Enabled` to ON.
- Enter your `client_id` and `client_secret` saved in the previous steps.
- Click `Save`.
<SocialProviderSettingsSupabase provider="Apple" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up Bitbucket logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Bitbucket" />
## Create a Bitbucket OAuth app
@@ -56,13 +44,7 @@ The next step requires a callback URL, which looks like this:
## Add your Bitbucket credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Bitbucket Enabled` to ON
- Enter your `client_id` and `client_secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Bitbucket" />
## Add login code to your client app
@@ -29,19 +29,7 @@ Setting up Discord logins for your application consists of 3 parts:
## Find your callback URL
In the next step you require a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Discord" />
## Create a Discord Application
@@ -55,13 +43,7 @@ In the next step you require a callback URL, which looks like this:
## Add your Discord credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Discord Enabled` to ON
- Enter your `client_id` and `client_secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Discord" />
## Add login code to your client app
@@ -31,21 +31,7 @@ Setting up Facebook logins for your application consists of 3 parts:
- Fill in your app information, then click `Create App`.
- This should bring you to the screen: `Add Products to Your App`. (Alternatively you can click on `Add Product` in the left sidebar to get to this screen.)
## Find your callback URI
The next step requires a callback URI, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Facebook" />
## Set up FaceBook Login for your Facebook App
@@ -73,13 +59,7 @@ You can read more about access levels [here](https://developers.facebook.com/doc
## Enter your Facebook App ID and Secret into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Facebook Enabled` to ON
- Enter your `Facebook client ID` and `Facebook secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Facebook" />
## Add login code to your client app
@@ -34,19 +34,7 @@ Go to the [GitHub Developer Settings](https://github.com/settings/developers) pa
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="GitHub" />
## Register a new OAuth application
@@ -66,13 +54,7 @@ Copy your new OAuth credentials
## Enter your GitHub credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `GitHub Enabled` to ON
- Enter your `GitHub Client ID` and `GitHub Client Secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="GitHub" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up GitLab logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="GitLab" />
## Create your GitLab Application
@@ -53,13 +41,7 @@ The next step requires a callback URL, which looks like this:
## Add your GitLab credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `GitLab Enabled` to ON.
- Enter your `client_id` and `client_secret` saved in the previous step.
- Click `Save`.
<SocialProviderSettingsSupabase provider="GitLab" />
## Add login code to your client app
@@ -48,19 +48,7 @@ From your project's dashboard screen:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Google" />
## Create your Google credentials
@@ -81,13 +69,7 @@ Copy your new OAuth credentials
## Enter your Google credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Google Enabled` to ON
- Enter your `Google Client ID` and `Google Client Secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Google" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up LinkedIn logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="LinkedIn" />
## Create a LinkedIn OAuth app
@@ -52,13 +40,7 @@ The next step requires a callback URL, which looks like this:
## Enter your LinkedIn credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `LinkedIn Enabled` to ON.
- Enter your `client_id` and `client_secret` saved in the previous step.
- Click `Save`.
<SocialProviderSettingsSupabase provider="LinkedIn" />
## Add login code to your client app
@@ -36,19 +36,7 @@ Setting up Notion logins for your application consists of 3 parts:
![notion.so](/docs/img/guides/auth-notion/notion-redirect-uri.png)
You can retrieve the redirect uri with the following steps:
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
Your redirect uri should look like the following: `https://<project-ref>.supabase.co/auth/v1/callback`
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Notion" />
## Add your Notion credentials into your Supabase Project
@@ -56,12 +44,7 @@ Your redirect uri should look like the following: `https://<project-ref>.supabas
![notion.so](/docs/img/guides/auth-notion/notion-creds.png)
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Under `External OAuth Providers` turn `Notion Enabled` to ON
- Enter the "OAuth client ID" and "OAuth client secret" obtained in the `client id` and `client secret` fields.
- Click `Save`
<SocialProviderSettingsSupabase provider="Notion" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up Slack logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Slack" />
## Create a Slack OAuth app
@@ -65,13 +53,7 @@ Under `Redirect URLs`:
## Enter your Slack credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `Slack Enabled` to ON.
- Enter your `Client ID` (`client_id`) and `Client Secret` (`client_secret`) saved in the previous step.
- Click `Save`.
<SocialProviderSettingsSupabase provider="Slack" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up Spotify logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Spotify" />
## Create a Spotify OAuth app
@@ -60,13 +48,7 @@ Under `Redirect URIs`:
## Enter your Spotify credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `Spotify Enabled` to ON.
- Enter your `Client ID` (`client_id`) and `Client Secret` (`client_secret`) saved in the previous step.
- Click `Save`.
<SocialProviderSettingsSupabase provider="Spotify" />
## Add login code to your client app
@@ -30,19 +30,7 @@ Setting up Twitch logins for your application consists of 3 parts:
## Find your callback URL
In the next step you require a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Twitch" />
## Create a Twitch Application
@@ -71,13 +59,7 @@ In the next step you require a callback URL, which looks like this:
## Add your Twitch credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Twitch Enabled` to ON
- Enter your `client_id` and `client_secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Twitch" />
## Add login code to your client app
@@ -6,7 +6,7 @@ export const meta = {
description: 'Add Twitter OAuth to your Supabase project',
}
To enable Twitter Auth for your project, you need to set up a Twitter OAuth application and add the application credentials to your Supabase Dashboard.
To enable Twitter Auth for your project, you need to set up a Twitter OAuth application with [elevated access](https://developer.twitter.com/en/portal/products/elevated)and add the application credentials in the Supabase Dashboard.
## Overview
@@ -25,19 +25,7 @@ Setting up Twitter logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Twitter" />
## Create a Twitter OAuth app
@@ -61,13 +49,7 @@ The next step requires a callback URL, which looks like this:
## Enter your Twitter credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
- Under `External OAuth Providers` turn `Twitter Enabled` to ON.
- Enter your `API Key` (`client_id`) and `API Secret Key` (`client_secret`) saved in the previous step.
- Click `Save`.
<SocialProviderSettingsSupabase provider="Twitter" />
## Add login code to your client app
@@ -48,12 +48,7 @@ You can pick between any one of the many identity providers that WorkOS supports
## Add your WorkOS credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Under `External OAuth Providers` turn `WorkOS Enabled` to ON
- Enter the `Client ID`, `Secret`, and `WorkOS URL` saved in the previous steps. The ``WorkOS URL` setting should be set to https://api.workos.com/
- Click `Save`
<SocialProviderSettingsSupabase provider="WorkOS" />
## Add login code to your client app
@@ -25,19 +25,7 @@ Setting up Zoom logins for your application consists of 3 parts:
## Find your callback URL
The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
<SocialProviderSetup provider="Zoom" />
## Create a Zoom Oauth App
@@ -61,13 +49,7 @@ Under `Redirect URL for OAuth`
## Enter your Zoom credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Zoom Enabled` to ON
- Enter your `Zoom Client ID` and `Zoom Client Secret` saved in the previous step
- Click `Save`
<SocialProviderSettingsSupabase provider="Zoom" />
## Add login code to your client app
+1 -1
View File
@@ -29,7 +29,7 @@ Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree
<div className="grid md:grid-cols-12 gap-4 not-prose">
{examples.map((x) => (
<div className="col-span-4">
<div className="col-span-4" key={x.href}>
<Link href={x.href} passHref>
<a>
<GlassPanel icon={'/docs/img/icons/github-icon'} hasLightIcon={true} title={x.name}>
@@ -63,7 +63,7 @@ A scalable websocket engine for managing user Presence, broadcasting messages, a
- Official Docs: [Supabase Realtime docs](/docs/guides/realtime)
- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime)
- License: [Apache 2](https://github.com/supabase/realtime/blob/master/LICENSE)
- License: [Apache 2](https://github.com/supabase/realtime/blob/main/LICENSE)
- Language: Elixir
### Storage API (large file storage)
@@ -106,6 +106,7 @@ Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
import '../styles/globals.css'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { useState } from 'react'
function MyApp({ Component, pageProps }) {
const [supabase] = useState(() => createBrowserSupabaseClient())
@@ -125,6 +126,7 @@ export default MyApp
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
```jsx lines=2,8 title=pages/_app.tsx
import { useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
@@ -134,9 +134,9 @@ interface Props {
const Account: Component<Props> = ({ session }) => {
const [loading, setLoading] = createSignal(true)
const [username, setUsername] = (createSignal < string) | (null > null)
const [website, setWebsite] = (createSignal < string) | (null > null)
const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
const [username, setUsername] = createSignal<string | null>(null)
const [website, setWebsite] = createSignal<string | null>(null)
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
createEffect(() => {
getProfile()
@@ -46,8 +46,8 @@ PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
```
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed
on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
```js title=src/lib/supabaseClient.ts
import { createClient } from '@supabase/auth-helpers-sveltekit'
@@ -136,6 +136,7 @@ import '$lib/supabaseClient'
### Set up a Login component
Let's set up a Svelte component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
Create a new `Auth.svelte` component in the `src/routes` directory to handle this functionality.
```html title=src/routes/Auth.svelte
<script lang="ts">
@@ -178,7 +179,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
### Account component
After a user is signed in, they need to be able to edit their profile details and manage their account.
Create a new `Account.svelte` component to handle this functionality.
Create a new `Account.svelte` component in the `src/routes` directory to handle this functionality.
```html title=src/routes/Account.svelte
<script lang="ts">
@@ -328,7 +329,7 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
### Create an upload widget
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes` directory:
```html title=src/routes/Avatar.svelte
<script lang="ts">
+35 -33
View File
@@ -3,62 +3,64 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'estuary',
title: 'Estuary',
description: 'Create a real-time data pipeline connecting Firestore and Supabase to make migration simple.',
description:
'Create a real-time data pipeline connecting Firestore and Supabase to make migration simple.',
}
Estuary Flow is a platform for creating [real-time data pipelines at scale](https://www.estuary.dev/).
It combines the intuitive interface of an ELT service with an event-driven runtime and a variety of open-source connectors.
It combines the intuitive interface of an ELT service with an event-driven runtime and a variety of open-source connectors.
You can use Flow to migrate your data from Firestore to the Postgres database in your Supabase project.
You can use Flow to migrate your data from Firestore to the Postgres database in your Supabase project.
You do this by building a real-time pipeline that captures data from Firestore and materializes (loads) that data to Postgres.
Once created, the pipeline backfills all your historical data from Firestore and continues to process new data events in real time.
Once created, the pipeline backfills all your historical data from Firestore and continues to process new data events in real time.
## Prerequisites
Before you begin, you'll need:
* An [Estuary account](go.estuary.dev/sign-up).
- An [Estuary account](https://go.estuary.dev/sign-up).
* For your Firestore database:
* A Google service account with read access to your Firestore database, via [roles/datastore.viewer](https://cloud.google.com/datastore/docs/access/iam). You can assign this role when you [create the service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating), or [add it to an existing service account](https://cloud.google.com/iam/docs/granting-changing-revoking-access#single-role).
* A generated [JSON service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating) for the service account.
- For your Firestore database:
* A Supabase project
- A Google service account with read access to your Firestore database, via [roles/datastore.viewer](https://cloud.google.com/datastore/docs/access/iam). You can assign this role when you [create the service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating), or [add it to an existing service account](https://cloud.google.com/iam/docs/granting-changing-revoking-access#single-role).
- A generated [JSON service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating) for the service account.
- A Supabase project
## Step 1: Capture your Firestore data
You'll start by creating a **capture**, a task in Flow that connects to your data source system: in this case, Firestore. This process will create one or more data **collections**, backed by a real-time data lake.
You'll start by creating a **capture**, a task in Flow that connects to your data source system: in this case, Firestore. This process will create one or more data **collections**, backed by a real-time data lake.
1. Go to the Flow web application at [dashboard.estuary.dev](http://dashboard.estuary.dev) and sign in using the credentials provided by your Estuary account manager.
2. Click the **Captures** tab and choose **New Capture**.
3. Locate and select the **Google Firestore** card.
3. Locate and select the **Google Firestore** card.
A form appears with the properties required for a Firestore capture.
4. Set a name for your capture.
4. Set a name for your capture.
Click inside the **Name** field to generate a drop-down menu of available **prefixes** and select one (likely, this will be the name of your organization). Append a unique capture name after the `/` to create the full name, for example, `acmeCo/myFirestoreCapture`.
5. Fill out the required properties for Firestore.
* **Database**: Flow can autodetect the database name, but you may optionally specify it here. This is helpful if the service account used has access to multiple Firebase projects. Your database name usually follows the format `projects/$PROJECTID/databases/(default)`.
* **Credentials**: The JSON service account key created per the prerequisites.
- **Database**: Flow can autodetect the database name, but you may optionally specify it here. This is helpful if the service account used has access to multiple Firebase projects. Your database name usually follows the format `projects/$PROJECTID/databases/(default)`.
- **Credentials**: The JSON service account key created per the prerequisites.
6. Click **Discover Endpoint**.
Flow uses the provided configuration to initiate a connection with Firestore. It generates a capture specification and details of the collections that it will create, once published.
Flow uses the provided configuration to initiate a connection with Firestore. It generates a capture specification and details of the collections that it will create, once published.
7. Use the **Specification Editor** to view the [JSON schemas](https://docs.estuary.dev/concepts/schemas/) for each collection and make sure they are formatted correctly for your needs. If they're not, you can edit them.
7. Use the **Specification Editor** to view the [JSON schemas](https://docs.estuary.dev/concepts/schemas/) for each collection and make sure they are formatted correctly for your needs. If they're not, you can edit them.
8. Click **Save and publish**.
You'll see a notification when the capture publishes successfully.
You'll see a notification when the capture publishes successfully.
The data currently in your Firestore database has been captured, and future updates to it will be captured continuously.
The data currently in your Firestore database has been captured, and future updates to it will be captured continuously.
Click **Materialize Collections** to continue.
Click **Materialize Collections** to continue.
## Step 2: Materialize your collections to Postgres
@@ -66,35 +68,35 @@ Next, you'll add a Postgres materialization to connect the captured collections
1. On the **Create Materialization** page, search for and select the **PostgreSQL** tile.
A form appears with the properties required for a Postgres materialization.
A form appears with the properties required for a Postgres materialization.
2. Choose a unique name for your materialization like you did when naming your capture; for example, `acmeCo/mySupabaseMaterialization`.
3. Fill out the required properties for PostgreSQL. You can find most of these in Supabase by going to the **Settings** section and clicking **Database**.
* **Address**: Format at `<host>:<port>`.
* **User**: Usually, this is `postgres`.
* **Password**: The password you set when you created your Supabase project.
- **Address**: Format at `<host>:<port>`.
- **User**: Usually, this is `postgres`.
- **Password**: The password you set when you created your Supabase project.
4. Scroll down to view the **Collection Selector** and fill in the **Table** field for each collection.
The collections you just created have already been selected, but you must provide names for the tables to which they'll be materialized.
The collections you just created have already been selected, but you must provide names for the tables to which they'll be materialized.
5. Click **Discover Endpoint**.
Flow uses the provided configuration to initiate a connection to your Supabase Postgres database and generate a specification.
5. Click **Discover Endpoint**.
6. Click **Save and Publish**. You'll see a notification when the full materialization publishes successfully.
Flow uses the provided configuration to initiate a connection to your Supabase Postgres database and generate a specification.
Your Firestore collections are copied to tables in Supabase. As long as you leave the capture and materialation running, any changes to the Firestore data will be reflected in Supabase in milliseconds.
6. Click **Save and Publish**. You'll see a notification when the full materialization publishes successfully.
Your Firestore collections are copied to tables in Supabase. As long as you leave the capture and materialation running, any changes to the Firestore data will be reflected in Supabase in milliseconds.
## Resources
For more information, visit the [Flow docs](https://docs.estuary.dev/). In particular:
* [Guide to create a Data Flow](https://docs.estuary.dev/guides/create-dataflow/)
* [Firestore capture connector](https://docs.estuary.dev/reference/Connectors/capture-connectors/google-firestore/)
* [Postgres materializaiton connector](https://docs.estuary.dev/reference/Connectors/materialization-connectors/PostgreSQL/)
- [Guide to create a Data Flow](https://docs.estuary.dev/guides/create-dataflow/)
- [Firestore capture connector](https://docs.estuary.dev/reference/Connectors/capture-connectors/google-firestore/)
- [Postgres materializaiton connector](https://docs.estuary.dev/reference/Connectors/materialization-connectors/PostgreSQL/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -82,7 +82,7 @@ You'll use this client to query for data in your synced schema.
## Resources
- [Sequin](https://sequin.io) official website.
- [Sequin Console](https://app.sequin.io*).
- [Sequin Console](https://app.sequin.io).
- [Sequin](https://docs.sequin.io/welcome) documentation.
- [Sequin + Supabase + Stripe Tutorial](https://github.com/sequin-io/build-a-saas-with-next-js-supabase-stripe-and-sequin)
@@ -51,7 +51,7 @@ The password is the same password you used when creating the Supabase project.
![supabase-connection-db-info](/docs/img/guides/integrations/snaplet/supabase-connection-db-info.png)
You’ll have to confirm providing Snaplet access to your database. Snaplet will prompt you to only provide `read-only` access to your database. Snaplet has a guide in their documentation on how to do so [here](https://docs.snaplet.dev/postgresql/create-read-only-role).
You’ll have to confirm providing Snaplet access to your database. Snaplet will prompt you to only provide `read-only` access to your database. Snaplet has a guide in their documentation on how to do so [here](https://docs.snaplet.dev/guides/postgresql/#create-read-only-role).
> Note that whatever connection string you provide here will be that of your Data Source – essentially the production database in a real-life scenario
@@ -103,7 +103,7 @@ If we only ask for the data from the `location` field of the `vehicles` table, S
Assuming you were able to fetch data from Supabase in a WeWeb Collection, you'll be able to bind the data from that Collection on your WeWeb pages.
In the example below, we chose to display the car model and mileage in the [Data Grid element](https://docs.weweb.io/data-grid/) that comes out-of-the-box in WeWeb:
In the example below, we chose to display the car model and mileage in the [Data Grid element](https://docs.weweb.io/add-elements/elements/data-grid.html) that comes out-of-the-box in WeWeb:
![](https://weweb-changelog.ghost.io/content/images/2022/10/CleanShot-2022-10-21-at-12.31.05@2x.png)
@@ -111,7 +111,7 @@ We chose this element because it includes a built-in inline editing mode we'll w
#### 🔥 Pro Tip 🔥
> In WeWeb, you can [bind arrays of data to any Container](https://docs.weweb.io/display-collection-list/). Just bear in mind that the first child of the Container you bind the Collection to will be the repeated item. With that in mind, you might want the first child Element to be another Container with a number of items inside like a title, description, button or image.
> In WeWeb, you can [bind arrays of data to any Container](https://docs.weweb.io/binding-filtering/display-data.html). Just bear in mind that the first child of the Container you bind the Collection to will be the repeated item. With that in mind, you might want the first child Element to be another Container with a number of items inside like a title, description, button or image.
## Step 4: Update a record in Supabase
@@ -10,9 +10,9 @@ Every project on the Supabase Platform comes with its own dedicated Postgres ins
| Plan | Pricing | CPU | Memory | Disk IO Bandwidth | Connections: Direct | Connections: Pooler |
| --------------- | ------- | ----------------------- | ------ | ----------------- | ------------------- | -------------------- |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | Up to 2,085 Mbps | 10 (recommended) | 50 (recommended) |
| Small | $5 | 2-core ARM (shared) | 2 GB | Up to 2,085 Mbps | 30 (recommended) | 75 (recommended) |
| Medium | $50 | 2-core ARM (shared) | 4 GB | Up to 2,085 Mbps | 50 (recommended) | 150 (recommended) |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | Up to 2,606 Mbps | 10 (recommended) | 50 (recommended) |
| Small | $5 | 2-core ARM (shared) | 2 GB | Up to 2,606 Mbps | 30 (recommended) | 75 (recommended) |
| Medium | $50 | 2-core ARM (shared) | 4 GB | Up to 2,606 Mbps | 50 (recommended) | 150 (recommended) |
| Large | $100 | 2-core ARM (dedicated) | 8 GB | Up to 4,750 Mbps | 100 (recommended) | 300 (recommended) |
| XL | $200 | 4-core ARM (dedicated) | 16 GB | Up to 4,750 Mbps | 200 (recommended) | 600 (recommended) |
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | Up to 4,750 Mbps | 350 (recommended) | 1200 (recommended) |
@@ -20,7 +20,7 @@ To get started:
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
## Check restrictions
+1 -1
View File
@@ -15,7 +15,7 @@ Supabase provides a globally distributed cluster of [Realtime](https://github.co
A [channel](https://hexdocs.pm/phoenix/channels.html) is the basic building block of Realtime and narrows the scope of data flow to subscribed clients. You can think of a channel as a chatroom where participants are able to see who's online and send and receive messages; similar to a Discord or Slack channel.
All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extenstions, like Postgres Changes, must be enabled prior to use.
All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extensions, like Postgres Changes, must be enabled prior to use.
## Broadcast
@@ -54,7 +54,7 @@ git init
supabase init
```
Make sure Docker is running. The [start](/docs/reference/cli/usage#supabase-start) command uses Docker to start the Supabase [services](/docs/architecture).
Make sure Docker is running. The [start](/docs/reference/cli/usage#supabase-start) command uses Docker to start the Supabase [services](/docs/guides/getting-started/architecture).
This command may take a while to run if this is the first time using the CLI.
```bash
@@ -247,7 +247,6 @@ supabase functions deploy <function_name>
The local development environment is not as feature-complete as the Supabase Platform. Here are some of the differences:
- The Storage interface is coming soon.
- The Functions interface is coming soon.
- Logs are not supported through the interface (however you can access them through the Docker containers).
- You cannot update your project settings in the Dashboard—this must be done using the CLI.
+1 -1
View File
@@ -72,7 +72,7 @@ For working with JWT and Auth functions.
Supabase creates several default roles in your Postgres database. To restore defaults at any time you can run the commands inside the
[schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your
[role passwords](./docker#securing-your-setup) before deploying to production environments.
[role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments.
##### `postgres`
+3 -3
View File
@@ -11,9 +11,9 @@ export const meta = {
<h2 className="text-base max-w-xl m-0">
Supabase is an open source Firebase alternative providing all the backend features you need to
build a product. [Learn more](/docs/architecture) about Supabase, start with the
[tutorials](/docs/guides/getting-started#tutorials), or dive straight into the different products
and APIs.
build a product. [Learn more](/docs/guides/getting-started/architecture) about Supabase, start
with the [tutorials](/docs/guides/getting-started#tutorials), or dive straight into the different
products and APIs.
</h2>
{/* start container */}
@@ -49,7 +49,7 @@ So today we're shipping Light Mode. Access it in the settings of your [Dashboard
With the help of the community, we [started internationalizing](https://github.com/supabase/supabase/issues/1341) our main repository:
- [Arabic | العربية](https://github.com/supabase/supabase/blob/master/i18n/README.ar.md)
- [Chinese / 中文](https://github.com/supabase/supabase/blob/master/i18n/README.cn.md)
- [Chinese / 中文](https://github.com/supabase/supabase/blob/master/i18n/README.zh-cn.md)
- [Dutch / Nederlands](https://github.com/supabase/supabase/blob/master/i18n/README.nl.md)
- [English](https://github.com/supabase/supabase)
- [French / Français](https://github.com/supabase/supabase/blob/master/i18n/README.fr.md)
@@ -22,7 +22,7 @@ So let's get cracking, we've got a lot to cover. 🚀
## Open Source Spotlight: Kong API Gateway
Supabase provides a whole bunch of features that are made up of a [collection of open-source tools](/docs/architecture). To orchestrate these different services and functionalities we use the [Kong API Gateway](https://github.com/Kong/kong).
Supabase provides a whole bunch of features that are made up of a [collection of open-source tools](/docs/guides/getting-started/architecture). To orchestrate these different services and functionalities we use the [Kong API Gateway](https://github.com/Kong/kong).
As a user of our hosted offering you don't really need to know all this, but we love showcasing the amazing open-source tools that we work with, and so we've invited [Vik Gamov](https://twitter.com/gAmUssA) to give you a little intro to [Kong](https://github.com/Kong/kong):
@@ -76,4 +76,4 @@ Tech stack:
Sign up for the waitlist at [multiplayer.dev](https://multiplayer.dev).
You can follow development in our [open source repository on GitHub](https://github.com/supabase/realtime/tree/multiplayer).
You can follow development in our [open source repository on GitHub](https://github.com/supabase/realtime/tree/develop).
@@ -19,7 +19,7 @@ To keep track of all the great tools and people that “Work With Supabase”, w
![partner-gallery-screenshot.png](/images/blog/partner-gallery/partner-gallery-screenshot.png)
As with anything at Supabase, the Partner Gallery is built on top of the [Supabase stack](/docs/architecture) and is, of course, open-source. If you’re looking to build a partner gallery yourself, feel free to use our template as a start: [github.com/supabase-community/partner-gallery-example](https://github.com/supabase-community/partner-gallery-example). It’s packed with goodies like [Postgres Full Text Search](/docs/guides/database/full-text-search), `next-image` with Supabase Storage, and sending emails based on an insert trigger with Supabase Edge Functions.
As with anything at Supabase, the Partner Gallery is built on top of the [Supabase stack](/docs/guides/getting-started/architecture) and is, of course, open-source. If you’re looking to build a partner gallery yourself, feel free to use our template as a start: [github.com/supabase-community/partner-gallery-example](https://github.com/supabase-community/partner-gallery-example). It’s packed with goodies like [Postgres Full Text Search](/docs/guides/database/full-text-search), `next-image` with Supabase Storage, and sending emails based on an insert trigger with Supabase Edge Functions.
# Partner showcase - new integrations and expert services
@@ -13,7 +13,7 @@ date: '2022-08-09'
toc_depth: 3
---
Supabase is a platform-as-a-service built on top of PostgreSQL and [many other amazing open-source tools](https://supabase.com/docs/architecture). It’s a fantastic option to create data-intensive apps and tooling. Taking advantage of the community-made [Python SDK](https://github.com/supabase-community/supabase-py) and [Slack's Python SDK](https://slack.dev/python-slack-sdk/), you can automate tasks and build apps for several use cases.
Supabase is a platform-as-a-service built on top of PostgreSQL and [many other amazing open-source tools](https://supabase.com/docs/guides/getting-started/architecture). It’s a fantastic option to create data-intensive apps and tooling. Taking advantage of the community-made [Python SDK](https://github.com/supabase-community/supabase-py) and [Slack's Python SDK](https://slack.dev/python-slack-sdk/), you can automate tasks and build apps for several use cases.
## Prerequisites
@@ -161,9 +161,9 @@ Slack links have the following format:
https://<span className="text-scale-1200">ORGANIZATION</span>.slack.com/archives/
<span className="text-scale-1200">channel_id</span>/p<span className="text-scale-1200">message_id</span>
</span>
<br/>
<br />
<span style={{ color: 'maroon' }}>Organization: subdomain used in Slack</span>
<br/>
<br />
<span style={{ color: 'DarkGreen' }}>Channel ID</span>: It is the string that you'll need to enter
in slack channels as the channel id e.g C0000ABC02DE
@@ -418,7 +418,3 @@ If you have any questions please reach out via [Twitter](https://twitter.com/sup
- [Python data loading with Supabase](https://supabase.com/blog/loading-data-supabase-python)
- [Visualizing Supabase Data using Metabase](https://supabase.com/blog/visualizing-supabase-data-using-metabase)
- [Supabase-py (Database) on Replit](https://replit.com/@Supabase/Supabase-py-Database?v=1)
+3 -3
View File
@@ -14,7 +14,7 @@
{ "key": "pricing", "title": "Pricing", "value": "$0" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "1 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,085 Mbps" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{
"key": "directConnections",
"title": "Connections: Direct",
@@ -33,7 +33,7 @@
{ "key": "pricing", "title": "Pricing", "value": "$5" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "2 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,085 Mbps" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{
"key": "directConnections",
"title": "Connections: Direct",
@@ -52,7 +52,7 @@
{ "key": "pricing", "title": "Pricing", "value": "$50" },
{ "key": "cpu", "title": "CPU", "value": "2-core ARM (shared)" },
{ "key": "memory", "title": "Memory", "value": "4 GB" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,085 Mbps" },
{ "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 2,606 Mbps" },
{
"key": "directConnections",
"title": "Connections: Direct",
+1 -1
View File
@@ -78,7 +78,7 @@
"img_url": "/images/twitter-profiles/_ki30kYo_400x400.jpg"
},
{
"text": "Badass Supabase is amazing, literally saves our small team a while engineer’s worth of work constantly Founders and everyone I’ve chatted with at supabase are just awesome people as well :)",
"text": "Badass! Supabase is amazing. literally saves our small team a whole engineer’s worth of work constantly. The founders and everyone I’ve chatted with at supabase are just awesome people as well :)",
"url": "https://twitter.com/KennethCassel/status/1524359528619384834",
"handle": "KennethCassel",
"img_url": "/images/twitter-profiles/pmQj3TX-_400x400.jpg"
+10 -1
View File
@@ -535,12 +535,16 @@ module.exports = [
source: '/docs/reference/postgres/publications',
destination: '/docs/guides/database/replication',
},
{
permanent: true,
source: '/docs/guides/hosting/platform',
destination: '/docs/guides/platform',
},
{
permanent: true,
source: '/docs/guides/hosting/platform/access-control',
destination: '/docs/guides/platform/access-control',
},
{
permanent: true,
source: '/docs/guides/self-hosting/overview',
@@ -1707,6 +1711,11 @@ module.exports = [
source: '/docs/guides/auth/auth-workos',
destination: '/docs/guides/auth/social-login/auth-workos',
},
{
permanent: true,
source: '/docs/guides/auth/auth-zoom',
destination: '/docs/guides/auth/social-login/auth-zoom',
},
{
permanent: true,
source: '/docs/guides/database',
+24 -16
View File
@@ -102,7 +102,7 @@ export default function launchweek() {
<Badge
className={`!bg-transparent !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] !border-[#4d898c] dark:from-white dark:to-[#DFFFF1] dark:!border-[#DFFFF1] h-fit relative ${
shipped
? 'after:absolute after:rounded-full text-black after:bg-white after:w-full after:h-full after:top-0 after:right-0 after:bottom-0 after:left-0 after:bg-gradient-to-br after:from-[#dceef0] after:to-[#b6b6b6] dark:text-transparent dark:!border-[#163837] after:dark:from-[#14292c] after:dark:to-[#141516] after:border-[#c8d8d9] after:dark:border-[#1f3536] after:-z-10 border-[#DFFFF1]'
? 'after:absolute after:rounded-full text-black after:bg-white after:w-full after:h-full after:top-0 after:right-0 after:bottom-0 after:left-0 after:bg-gradient-to-br after:from-[#dceef0] after:to-[#FFFFFF] dark:text-transparent dark:!border-[#163837] after:dark:from-[#14292c] after:dark:to-[#141516] after:border-[#c8d8d9] after:dark:border-[#1f3536] after:-z-10 border-[#DFFFF1]'
: ''
}`}
>
@@ -330,7 +330,7 @@ export default function launchweek() {
>
<div className="flex items-center relative z-10 justify-between flex-col-reverse lg:flex-row lg:justify-start gap-2 text-black dark:text-white">
<span>{day1.description}</span>
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#DFFFF1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit lg:ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#DFFFF1] dark:border-[#DFFFF1]">
Redesigned
</Badge>
</div>
@@ -375,7 +375,7 @@ export default function launchweek() {
></div>
<div className="flex items-center justify-between flex-col-reverse lg:flex-row lg:justify-start gap-2 text-black dark:text-white">
<div>{day2.steps[0].title}</div>
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit lg:ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
New
</Badge>
</div>
@@ -454,7 +454,7 @@ export default function launchweek() {
<div className="flex items-center justify-between flex-col-reverse lg:flex-row lg:justify-start text-black dark:text-white">
<span>{day3.steps[0].title}</span>
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit lg:ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
Updated
</Badge>
</div>
@@ -516,7 +516,7 @@ export default function launchweek() {
</div>
<div className="flex items-center flex-col-reverse lg:flex-row text-black dark:text-white">
<span>{day4.steps[0].title}</span>
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit lg:ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
New
</Badge>
</div>
@@ -585,7 +585,7 @@ export default function launchweek() {
<span className="text-black dark:text-white">
{day5.steps[0].title}
</span>
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit lg:ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
New
</Badge>
</div>
@@ -778,7 +778,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
Updated
</Badge>
<span className="text-black dark:text-white text-center">
@@ -823,7 +823,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
New
</Badge>
<span className="text-black dark:text-white text-center">
@@ -868,7 +868,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
New
</Badge>
<span className="text-black dark:text-white text-center">
@@ -913,7 +913,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
Experimental
</Badge>
<span className="text-black dark:text-white text-center">
@@ -958,7 +958,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
Upgrade
</Badge>
<span className="text-black dark:text-white text-center">
@@ -1003,7 +1003,7 @@ export default function launchweek() {
/>
</div>
<div className="flex flex-col items-center gap-2 min-w-[300px]">
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
Upgrade
</Badge>
<span className="text-black dark:text-white text-center">
@@ -1029,8 +1029,10 @@ export default function launchweek() {
<SectionContainer className="!pt-0 sm:!pb-20 !pb-40"></SectionContainer>
<SectionContainer className="!pt-0 sm:!pb-20 !pb-40">
<div className="flex flex-col mb-16">
<Badge className="mb-4 w-fit bg-gradient-to-r from-[#0E3737C2] to-[#67947F] hover:to-[#39617D94] dark:hover:to-[#A6FFD899] text-whiteA-1200 dark:text-black font-normal !py-1 !px-4 dark:from-white dark:via-white dark:to-[#1a7a4c75] bg-slate-1200">
Happening all week
<Badge
className={`w-fit !bg-transparent !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] dark:from-white dark:to-[#DFFFF1] h-fit relative mb-4 after:absolute after:rounded-full text-black after:bg-white after:w-full after:h-full after:top-0 after:right-0 after:bottom-0 after:left-0 after:bg-gradient-to-br after:from-[#dceef0] after:to-[#FFFFFF] dark:text-transparent dark:!border-[#163837] after:dark:from-[#14292c] after:dark:to-[#141516] after:border-[#c8d8d9] after:dark:border-[#1f3536] after:-z-10 border-[#DFFFF1]`}
>
Submissions Closed
</Badge>
<h2 className="text-3xl text-black dark:text-white mb-2">Launch Week Hackathon</h2>
<p className="text-slate-900 w-[80%] lg:w-[50%]">
@@ -1212,8 +1214,14 @@ export default function launchweek() {
})}
</div>
<div className="flex-1">
<Badge className="mb-6 bg-gradient-to-r from-[#0E3737C2] to-[#67947F] hover:to-[#39617D94] dark:hover:to-[#A6FFD899] text-whiteA-1200 dark:text-black font-normal !py-1 !px-4 dark:from-white dark:via-white dark:to-[#1a7a4c75] bg-slate-1200">
Currently Happening
<Badge
className={`!bg-transparent !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] !border-[#4d898c] dark:from-white dark:to-[#DFFFF1] dark:!border-[#DFFFF1] h-fit relative mb-4 ${
true
? 'after:absolute after:rounded-full text-black after:bg-white after:w-full after:h-full after:top-0 after:right-0 after:bottom-0 after:left-0 after:bg-gradient-to-br after:from-[#dceef0] after:to-[#FFFFFF] dark:text-transparent dark:!border-[#163837] after:dark:from-[#14292c] after:dark:to-[#141516] after:border-[#c8d8d9] after:dark:border-[#1f3536] after:-z-10 border-[#DFFFF1]'
: ''
}`}
>
Shipped
</Badge>
<h2 className="text-3xl dark:text-white mb-2">The Supabase Content Storm</h2>
<p className="text-slate-900 md:max-w-[80%] mb-16">
+8 -1
View File
@@ -197,9 +197,16 @@ services:
- log_min_messages=fatal # prevents Realtime polling queries from appearing in logs
restart: unless-stopped
ports:
- ${POSTGRES_PORT}:5432
# Pass down internal port because it's set dynamically by other services
- ${POSTGRES_PORT}:${POSTGRES_PORT}
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: ${POSTGRES_PORT}
POSTGRES_PORT: ${POSTGRES_PORT}
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATABASE: ${POSTGRES_DB}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/roles.sql
- ./volumes/db/data:/var/lib/postgresql/data
+71 -30
View File
@@ -9827,6 +9827,36 @@
"react-dom": "<18.0.0"
}
},
"node_modules/@testing-library/react-hooks": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz",
"integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"react-error-boundary": "^3.1.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@types/react": "^16.9.0 || ^17.0.0",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-test-renderer": "^16.9.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-test-renderer": {
"optional": true
}
}
},
"node_modules/@testing-library/user-event": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
@@ -9963,12 +9993,6 @@
"classnames": "*"
}
},
"node_modules/@types/crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==",
"dev": true
},
"node_modules/@types/d3-color": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz",
@@ -14330,11 +14354,6 @@
"node": "*"
}
},
"node_modules/crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
},
"node_modules/css-blank-pseudo": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
@@ -29842,6 +29861,22 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/react-error-boundary": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
@@ -37558,7 +37593,6 @@
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
@@ -37621,11 +37655,11 @@
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"@tailwindcss/typography": "^0.5.2",
"@testing-library/dom": "^8.11.1",
"@testing-library/react": "^12.1.2",
"@testing-library/dom": "^8.19.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/blueimp-md5": "^2.18.0",
"@types/crypto-js": "^4.0.2",
"@types/file-saver": "^2.0.2",
"@types/json-logic-js": "^1.2.1",
"@types/lodash": "^4.14.172",
@@ -44568,6 +44602,16 @@
"@types/react-dom": "<18.0.0"
}
},
"@testing-library/react-hooks": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz",
"integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"react-error-boundary": "^3.1.0"
}
},
"@testing-library/user-event": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
@@ -44693,12 +44737,6 @@
"classnames": "*"
}
},
"@types/crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==",
"dev": true
},
"@types/d3-color": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz",
@@ -48249,11 +48287,6 @@
"randomfill": "^1.0.3"
}
},
"crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
},
"css-blank-pseudo": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
@@ -60519,6 +60552,15 @@
}
}
},
"react-error-boundary": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5"
}
},
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
@@ -63325,11 +63367,11 @@
"@tailwindcss/typography": "^0.5.2",
"@tanstack/react-query": "^4.16.1",
"@tanstack/react-query-devtools": "^4.16.1",
"@testing-library/dom": "^8.11.1",
"@testing-library/react": "^12.1.2",
"@testing-library/dom": "^8.19.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "^13.5.0",
"@types/blueimp-md5": "^2.18.0",
"@types/crypto-js": "^4.0.2",
"@types/file-saver": "^2.0.2",
"@types/json-logic-js": "^1.2.1",
"@types/lodash": "^4.14.172",
@@ -63360,7 +63402,6 @@
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+1 -1
View File
@@ -78,7 +78,7 @@ const tweets = [
img_url: '/images/twitter-profiles/_ki30kYo_400x400.jpg',
},
{
text: 'Badass Supabase is amazing, literally saves our small team a while engineer’s worth of work constantly Founders and everyone I’ve chatted with at supabase are just awesome people as well :)',
text: 'Badass! Supabase is amazing. literally saves our small team a whole engineer’s worth of work constantly. The founders and everyone I’ve chatted with at supabase are just awesome people as well :)',
url: 'https://twitter.com/KennethCassel/status/1524359528619384834',
handle: 'KennethCassel',
img_url: '/images/twitter-profiles/pmQj3TX-_400x400.jpg',
+1 -1
View File
@@ -73,7 +73,6 @@ function Select({
useFormContext()
if (values && !value) value = values[id]
function handleBlurEvent(e: React.FocusEvent<HTMLSelectElement>) {
if (handleBlur) handleBlur(e)
if (onBlur) onBlur(e)
@@ -126,6 +125,7 @@ function Select({
<select
id={id}
name={name}
defaultValue={defaultValue}
autoComplete={autoComplete}
autoFocus={autofocus}
className={classes.join(' ')}
+7
View File
@@ -26,6 +26,13 @@
"type": "markdown",
"libs": ["js", "js_v1"]
},
{
"title": "Upgrade guide",
"id": "upgrade-guide",
"slug": "upgrade-guide",
"type": "markdown",
"libs": ["js"]
},
{
"title": "Database",
"libs": ["js", "js_v1", "dart", "dart_v0"],
+1 -1
View File
@@ -749,7 +749,7 @@ functions:
- By default, Supabase projects return a maximum of 1,000 rows. This setting can be changed in your project's [API settings](https://app.supabase.com/project/_/settings/api). It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data.
- `select()` can be combined with [Filters](/docs/reference/javascript/using-filters)
- `select()` can be combined with [Modifiers](/docs/reference/javascript/using-modifiers)
- `apikey` is a reserved keyword if you're using the [Supabase Platform](/docs/guides/platform) and should be avoided as a column name](https://github.com/supabase/supabase/issues/5465).
- `apikey` is a reserved keyword if you're using the [Supabase Platform](/docs/guides/platform) and [should be avoided as a column name](https://github.com/supabase/supabase/issues/5465).
examples:
- id: getting-your-data
name: Getting your data
+29 -3089
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -6,11 +6,13 @@ DEFAULT_PROJECT_NAME=Default Project
SUPABASE_URL=http://localhost:8000
SUPABASE_PUBLIC_URL=http://localhost:8000
NEXT_PUBLIC_SUPABASE_URL=$SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLIC_URL=$SUPABASE_PUBLIC_URL
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
# TODO: evaluate these variables at runtime instead
# https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_GOTRUE_URL=$SUPABASE_URL/auth/v1
NEXT_PUBLIC_GOTRUE_URL=$NEXT_PUBLIC_SUPABASE_PUBLIC_URL/auth/v1
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
+2
View File
@@ -16,3 +16,5 @@ window.matchMedia = jest.fn().mockReturnValue({
removeEventListener: jest.fn(),
})
export const StoreProvider = StoreProvider
export const withAuth = jest.fn()
+1
View File
@@ -1 +1,2 @@
export const get = jest.fn()
export const post = jest.fn()
@@ -17,7 +17,7 @@ export const ForeignKeyFormatter = (
const rowData = deepClone(p.row)
rowData[p.column.key] = _value
const { error } = state.rowService!.update(rowData, p.column.key)
const { error } = state.rowService!.update(rowData, p.row, p.column.key)
if (error) {
if (state.onError) state.onError(error)
} else {
@@ -41,12 +41,17 @@ export const Grid = memo(
)
if (changedColumn) {
const { error } = state.rowService!.update(rowData, changedColumn, (payload) => {
dispatch({
type: 'EDIT_ROW',
payload,
})
})
const { error } = state.rowService!.update(
rowData,
originRowData,
changedColumn,
(payload) => {
dispatch({
type: 'EDIT_ROW',
payload,
})
}
)
if (error && onErrorFunc) onErrorFunc(error)
}
}
@@ -5,7 +5,6 @@ import { saveAs } from 'file-saver'
import { useStore } from 'hooks'
import FilterDropdown from './filter'
import SortPopover from './sort'
import StatusLabel from './StatusLabel'
import RefreshButton from './RefreshButton'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { Sort, Filter } from 'components/grid/types'
@@ -41,10 +40,7 @@ const Header: FC<HeaderProps> = ({ sorts, filters, onAddColumn, onAddRow, header
onAddRow={onAddRow}
/>
)}
<div className="sb-grid-header__inner">
{headerActions}
<StatusLabel />
</div>
<div className="sb-grid-header__inner">{headerActions}</div>
</div>
)
}
@@ -1,5 +1,6 @@
import { FC, useState } from 'react'
import { Button, IconRefreshCw } from 'ui'
import { FC, useState, useEffect } from 'react'
import { Button, IconRefreshCw, IconCheck } from 'ui'
import { SupabaseGridQueue } from '../../constants'
import { Filter, Sort } from 'components/grid/types'
import { useDispatch, useTrackedState } from 'components/grid/store'
import { fetchCount, fetchPage } from 'components/grid/utils'
@@ -10,10 +11,34 @@ interface Props {
}
const RefreshButton: FC<Props> = ({ sorts, filters }) => {
const [loading, setLoading] = useState(false)
const state = useTrackedState()
const dispatch = useDispatch()
const [loading, setLoading] = useState(false)
const [status, setStatus] = useState<string>()
useEffect(() => {
let isMounted = true
let timer: number | null
SupabaseGridQueue.on('active', () => {
if (timer) clearTimeout(timer)
if (isMounted) setStatus('saving')
})
SupabaseGridQueue.on('idle', () => {
if (timer) clearTimeout(timer)
timer = window.setTimeout(() => setStatus(undefined), 2000)
if (isMounted) setStatus('saved')
})
return () => {
isMounted = false
if (timer) clearTimeout(timer)
}
}, [])
async function onClick() {
setLoading(true)
await fetchCount(state, dispatch, filters)
@@ -25,14 +50,20 @@ const RefreshButton: FC<Props> = ({ sorts, filters }) => {
<Button
type="text"
icon={
<div className="text-scale-1000">
<IconRefreshCw strokeWidth={1.5} />
</div>
!status ? (
<IconRefreshCw className="text-scale-1000" strokeWidth={1.5} />
) : status === 'saved' ? (
<IconCheck className="text-brand-900" strokeWidth={3} />
) : (
<></>
)
}
onClick={onClick}
loading={loading}
onClick={() => {
if (!status) onClick()
}}
loading={loading || status === 'saving'}
>
Refresh
{!status ? 'Refresh' : status === 'saved' ? 'Changes saved' : 'Saving changes'}
</Button>
)
}
@@ -1,81 +0,0 @@
import { useState, FC, useEffect } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { IconLoader, IconCheck } from 'ui'
import { SupabaseGridQueue } from '../../constants'
interface Props {}
const StatusLabel: FC<Props> = ({}) => {
const [status, setStatus] = useState<string>()
useEffect(() => {
let isMounted = true
let timer: number | null
SupabaseGridQueue.on('active', () => {
if (timer) clearTimeout(timer)
if (isMounted) setStatus('saving')
})
SupabaseGridQueue.on('idle', () => {
if (timer) clearTimeout(timer)
timer = window.setTimeout(() => setStatus(undefined), 4000)
if (isMounted) setStatus('saved')
})
return () => {
isMounted = false
if (timer) clearTimeout(timer)
}
}, [])
return (
<div className="sb-grid-status-label flex items-center justify-center">
{status === 'saving' ? (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconLoader className="mx-1 animate-spin text-scale-1100" size={14} strokeWidth={2} />
</Tooltip.Trigger>
<Tooltip.Content side="left">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">Saving changes...</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
) : status === 'saved' ? (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconCheck className="mx-1 text-brand-900" size={14} strokeWidth={3} />
</Tooltip.Trigger>
<Tooltip.Content side="left">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 ',
].join(' ')}
>
<span className="text-xs text-scale-1200">All changes saved</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
) : (
<></>
)}
{!status && (
<div className="sb-grid-status-label__no-msg">
<div></div>
</div>
)}
</div>
)
}
export default StatusLabel
@@ -1,10 +1,14 @@
import { FC } from 'react'
import { FC, useState, useMemo, useCallback } from 'react'
import { isEqual } from 'lodash'
import { Button, IconPlus, IconFilter, Popover } from 'ui'
import update from 'immutability-helper'
import { useUrlState } from 'hooks'
import FilterRow from './FilterRow'
import { useTrackedState } from 'components/grid/store'
import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils'
import { Filter } from 'components/grid/types'
import { FilterOperatorOptions } from './Filter.constants'
const FilterPopover: FC = () => {
const [{ filter: filters }]: any = useUrlState({ arrayKeys: ['filter'] })
@@ -14,7 +18,12 @@ const FilterPopover: FC = () => {
: 'Filter'
return (
<Popover size="large" align="start" className="sb-grid-filter-popover" overlay={<Filter />}>
<Popover
size="large"
align="start"
className="sb-grid-filter-popover"
overlay={<FilterOverlay />}
>
<Button
as="span"
type={(filters || []).length > 0 ? 'link' : 'text'}
@@ -31,19 +40,60 @@ const FilterPopover: FC = () => {
}
export default FilterPopover
const Filter: FC = () => {
const FilterOverlay: FC = () => {
const state = useTrackedState()
const [{ filter: filters }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const formattedFilters = formatFilterURLParams(filters as string[])
const [{ filter: filtersFromUrl }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const initialFilters = useMemo(
() => formatFilterURLParams((filtersFromUrl as string[]) ?? []),
[filtersFromUrl]
)
const [filters, setFilters] = useState<Filter[]>(initialFilters)
function onAddFilter() {
const column = state.table?.columns[0]?.name
if (column) {
setFilters([
...filters,
{
column,
operator: '=',
value: '',
},
])
}
}
const onChangeFilter = useCallback((index: number, filter: Filter) => {
setFilters((currentFilters) =>
update(currentFilters, {
[index]: {
$set: filter,
},
})
)
}, [])
const onDeleteFilter = useCallback((index: number) => {
setFilters((currentFilters) =>
update(currentFilters, {
$splice: [[index, 1]],
})
)
}, [])
function onApplyFilter() {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const column = state.table?.columns[0].name
return {
...prevParams,
filter: existingFilters.concat([`${column}:eq:`]),
filter: filters.map((filter) => {
const selectedOperator = FilterOperatorOptions.find(
(option) => option.value === filter.operator
)
return `${filter.column}:${selectedOperator?.abbrev}:${filter.value}`
}),
}
})
}
@@ -51,14 +101,16 @@ const Filter: FC = () => {
return (
<div className="space-y-2 py-2">
<div className="space-y-2">
{formattedFilters.map((filter, index) => (
{filters.map((filter, index) => (
<FilterRow
key={`filter-${filter.column}-${[index]}`}
filter={filter}
filterIdx={index}
onChange={onChangeFilter}
onDelete={onDeleteFilter}
/>
))}
{formattedFilters.length == 0 && (
{filters.length == 0 && (
<div className="space-y-1 px-3">
<h5 className="text-sm text-scale-1100">No filters applied to this view</h5>
<p className="text-xs text-scale-900">Add a column below to filter the view</p>
@@ -66,10 +118,13 @@ const Filter: FC = () => {
)}
</div>
<Popover.Separator />
<div className="px-3">
<div className="px-3 flex flex-row justify-between">
<Button icon={<IconPlus />} type="text" onClick={onAddFilter}>
Add filter
</Button>
<Button disabled={isEqual(filters, initialFilters)} type="default" onClick={onApplyFilter}>
Apply filter
</Button>
</div>
</div>
)
@@ -1,113 +1,26 @@
import { debounce } from 'lodash'
import { FC, memo, useState, useEffect, ChangeEvent, useCallback } from 'react'
import { FC, memo } from 'react'
import { Button, Input, IconChevronDown, IconX } from 'ui'
import { useUrlState } from 'hooks'
import { Filter } from 'components/grid/types'
import { Filter, FilterOperator } from 'components/grid/types'
import { DropdownControl } from 'components/grid/components/common'
import { useTrackedState } from 'components/grid/store'
import { FilterOperatorOptions } from './Filter.constants'
interface Props {
filter: Filter
filterIdx: number
filter: Filter
onChange: (index: number, filter: Filter) => void
onDelete: (index: number) => void
}
// [Area of improvement] Input field loses focus after the debounce (because of useUrlState?)
const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
const FilterRow: FC<Props> = ({ filter, filterIdx, onChange, onDelete }) => {
const state = useTrackedState()
const [{ filter: filters }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const column = state.table?.columns.find((x) => x.name === filter.column)
const columnOptions =
state.table?.columns?.map((x) => {
return { value: x.name, label: x.name, postLabel: x.dataType }
}) || []
const [filterValue, setFilterValue] = useState(filter.value)
useEffect(() => {
setFilterValue(filter.value)
}, [filterIdx])
function onRemoveFilter() {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const updatedFilters = existingFilters.filter((filter: string, idx: number) => {
if (idx !== filterIdx) return filter
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
function onColumnChange(column: string | number) {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const updatedFilters = existingFilters.map((filter: string, idx: number) => {
if (idx === filterIdx) {
const [_, operator, ...value] = filter.split(':')
const formattedValue = value.join(':')
return `${column}:${operator}:${formattedValue}`
} else {
return filter
}
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
function onOperatorChange(operator: string | number) {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const updatedFilters = existingFilters.map((filter: string, idx: number) => {
if (idx === filterIdx) {
const [column, _, ...value] = filter.split(':')
const formattedValue = value.join(':')
const selectedOperator = FilterOperatorOptions.find((option) => option.value === operator)
return `${column}:${selectedOperator?.abbrev}:${formattedValue}`
} else {
return filter
}
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
function onValueChange(event: ChangeEvent<HTMLInputElement>) {
const value = event.target.value
setFilterValue(value)
debounceHandler({
filterIdx,
value: { ...filter, value: value },
})
}
const updateFilterValue = (payload: { filterIdx: number; value: Filter }) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const updatedFilters = existingFilters.map((filter: string, idx: number) => {
if (idx === filterIdx) {
const [column, operator] = filter.split(':')
return `${column}:${operator}:${payload.value.value}`
} else {
return filter
}
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
const debounceHandler = useCallback(debounce(updateFilterValue, 600), [filters])
const placeholder =
column?.format === 'timestamptz'
@@ -118,7 +31,11 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
return (
<div className="sb-grid-filter-row px-3">
<DropdownControl align="start" options={columnOptions} onSelect={onColumnChange}>
<DropdownControl
align="start"
options={columnOptions}
onSelect={(nextColumn) => onChange(filterIdx, { ...filter, column: nextColumn as string })}
>
<Button
as="span"
type="outline"
@@ -129,10 +46,19 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
}
className="w-32"
>
{column?.name || ''}
{column?.name ?? ''}
</Button>
</DropdownControl>
<DropdownControl align="start" options={FilterOperatorOptions} onSelect={onOperatorChange}>
<DropdownControl
align="start"
options={FilterOperatorOptions}
onSelect={(nextOperator) =>
onChange(filterIdx, {
...filter,
operator: nextOperator as FilterOperator,
})
}
>
<Button
as="span"
type="outline"
@@ -149,14 +75,19 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
size="tiny"
className="w-full"
placeholder={placeholder}
value={filterValue}
onChange={onValueChange}
value={filter.value}
onChange={(event) =>
onChange(filterIdx, {
...filter,
value: event.target.value,
})
}
/>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={onRemoveFilter}
onClick={() => onDelete(filterIdx)}
/>
</div>
)
@@ -1,10 +1,13 @@
import React, { FC } from 'react'
import React, { FC, useState, useMemo, useCallback } from 'react'
import { isEqual } from 'lodash'
import update from 'immutability-helper'
import { Button, IconList, IconChevronDown, Popover } from 'ui'
import { useUrlState } from 'hooks'
import SortRow from './SortRow'
import { useTrackedState } from 'components/grid/store'
import { DropdownControl } from 'components/grid/components/common'
import { Sort } from 'components/grid/types'
import { formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
const SortPopover: FC = () => {
@@ -15,7 +18,7 @@ const SortPopover: FC = () => {
: 'Sort'
return (
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<Sort />}>
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<SortOverlay />}>
<Button
as="span"
type={(sorts || []).length > 0 ? 'link' : 'text'}
@@ -32,14 +35,18 @@ const SortPopover: FC = () => {
}
export default SortPopover
const Sort: FC = () => {
const SortOverlay: FC = () => {
const state = useTrackedState()
const [{ sort: sorts }, setParams] = useUrlState({ arrayKeys: ['sort'] })
const formattedSorts = formatSortURLParams(sorts as string[])
const [{ sort: sortsFromUrl }, setParams] = useUrlState({ arrayKeys: ['sort'] })
const initialSorts = useMemo(
() => formatSortURLParams((sortsFromUrl as string[]) ?? []),
[sortsFromUrl]
)
const [sorts, setSorts] = useState<Sort[]>(initialSorts)
const columns = state?.table?.columns!.filter((x) => {
const found = formattedSorts.find((y) => y.column == x.name)
const found = sorts.find((y) => y.column == x.name)
return !found
})
@@ -49,21 +56,59 @@ const Sort: FC = () => {
}) || []
function onAddSort(columnName: string | number) {
setSorts([...sorts, { column: columnName as string, ascending: true }])
}
function onApplySort() {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
sort: existingSorts.concat([`${columnName}:asc`]),
sort: sorts.map((sort) => `${sort.column}:${sort.ascending ? 'asc' : 'desc'}`),
}
})
}
const onDeleteSort = useCallback((column: string) => {
setSorts((currentSorts) => currentSorts.filter((sort) => sort.column !== column))
}, [])
const onToggleSort = useCallback((column: string, ascending: boolean) => {
setSorts((currentSorts) => {
const idx = currentSorts.findIndex((x) => x.column === column)
return update(currentSorts, {
[idx]: {
$merge: { ascending },
},
})
})
}, [])
const onDragSort = useCallback((dragIndex: number, hoverIndex: number) => {
setSorts((currentSort) =>
update(currentSort, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, currentSort[dragIndex]],
],
})
)
}, [])
return (
<div className="space-y-2 py-2">
{formattedSorts.map((sort, index) => (
<SortRow key={sort.column} index={index} columnName={sort.column} sort={sort} />
{sorts.map((sort, index) => (
<SortRow
key={sort.column}
index={index}
columnName={sort.column}
sort={sort}
onDelete={onDeleteSort}
onToggle={onToggleSort}
onDrag={onDragSort}
/>
))}
{formattedSorts.length === 0 && (
{sorts.length === 0 && (
<div className="space-y-1 px-3">
<h5 className="text-sm text-scale-1100">No sorts applied to this view</h5>
<p className="text-xs text-scale-900">Add a column below to sort the view</p>
@@ -71,7 +116,7 @@ const Sort: FC = () => {
)}
<Popover.Separator />
<div className="px-3">
<div className="px-3 flex flex-row justify-between">
{columns && columns.length > 0 ? (
<DropdownControl
options={dropdownOptions}
@@ -85,12 +130,15 @@ const Sort: FC = () => {
iconRight={<IconChevronDown />}
className="sb-grid-dropdown__item-trigger"
>
{`Pick ${formattedSorts.length > 1 ? 'another' : 'a'} column to sort by`}
{`Pick ${sorts.length > 1 ? 'another' : 'a'} column to sort by`}
</Button>
</DropdownControl>
) : (
<p className="text-sm text-scale-1100">All columns have been added</p>
)}
<Button disabled={isEqual(sorts, initialSorts)} type="default" onClick={onApplySort}>
Apply sorting
</Button>
</div>
</div>
)
@@ -1,10 +1,8 @@
import update from 'immutability-helper'
import { FC, useRef, memo } from 'react'
import { Button, IconMenu, Toggle, IconX } from 'ui'
import { XYCoord } from 'dnd-core'
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'
import { useUrlState } from 'hooks'
import { DragItem, Sort } from 'components/grid/types'
import { useTrackedState } from 'components/grid/store'
@@ -12,11 +10,13 @@ type SortRowProps = {
index: number
columnName: string
sort: Sort
onDelete: (columnName: string) => void
onToggle: (columnName: string, ascending: boolean) => void
onDrag: (dragIndex: number, hoverIndex: number) => void
}
const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
const SortRow: FC<SortRowProps> = ({ index, columnName, sort, onDelete, onToggle, onDrag }) => {
const state = useTrackedState()
const [_, setParams] = useUrlState({ arrayKeys: ['sort'] })
const column = state?.table?.columns.find((x) => x.name === columnName)
if (!column) return null
@@ -89,49 +89,9 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
},
})
function onToggle(value: boolean) {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = existingSorts.map((sort: string) => {
const [column] = sort.split(':')
return column === columnName ? `${columnName}:${value ? 'asc' : 'desc'}` : sort
})
return {
...prevParams,
sort: updatedSorts,
}
})
}
function onRemoveSort() {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
})
return {
...prevParams,
sort: updatedSorts,
}
})
}
const moveSort = (dragIndex: number, hoverIndex: number) => {
if (dragIndex == hoverIndex) return
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = update(existingSorts, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, existingSorts[dragIndex]],
],
})
return {
...prevParams,
sort: updatedSorts,
}
})
onDrag(dragIndex, hoverIndex)
}
const opacity = isDragging ? 0 : 1
@@ -144,12 +104,9 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
style={{ opacity }}
data-handler-id={handlerId}
>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={onRemoveSort}
/>
<span className="transition-color text-scale-900 hover:text-scale-1100">
<IconMenu strokeWidth={2} size={16} />
</span>
<div className="grow">
<span className="flex grow items-center gap-1 truncate text-sm text-scale-1200">
<span className="text-xs text-scale-900">{index > 0 ? 'then by' : 'sort by'}</span>
@@ -163,12 +120,15 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
layout="flex"
defaultChecked={sort.ascending}
// @ts-ignore
onChange={(e: boolean) => onToggle(e)}
onChange={(e: boolean) => onToggle(columnName, e)}
/>
</div>
<span className="transition-color text-scale-900 hover:text-scale-1100">
<IconMenu strokeWidth={2} size={16} />
</span>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={() => onDelete(columnName)}
/>
</div>
)
}
@@ -163,12 +163,21 @@ export class SqlRowService implements IRowService {
return rows
}
update(row: SupaRow, changedColumn?: string, onRowUpdate?: (value: any) => void) {
update(
row: SupaRow,
originalRow: SupaRow,
changedColumn?: string,
onRowUpdate?: (value: any) => void
) {
const { primaryKeys, error } = this.getPrimaryKeys()
if (error) {
return { error }
}
const { idx, ...value } = row
// Optimistic rendering
if (onRowUpdate) onRowUpdate({ row: value, idx })
const matchValues: any = {}
primaryKeys!.forEach((key) => {
matchValues[key] = row[key]
@@ -199,6 +208,9 @@ export class SqlRowService implements IRowService {
if (error) throw error
if (onRowUpdate) onRowUpdate({ row: data[0], idx })
}).catch((error) => {
const { idx, ...originalRowData } = originalRow
// Revert optimistic rendering if any errors
if (onRowUpdate) onRowUpdate({ row: originalRowData, idx })
this.onError(error)
})
@@ -28,6 +28,7 @@ export interface IRowService {
update: (
row: SupaRow,
originalRow: SupaRow,
changedColumn?: string,
onRowUpdate?: (value: any) => void
) => { row?: SupaRow; error?: ServiceError }
@@ -15,7 +15,7 @@ const PortalToast = () => (
position="top-right"
toastOptions={{
className:
'bg-bg-primary-light dark:bg-bg-primary-dark text-typography-body-strong-light dark:text-typography-body-strong-dark border dark:border-dark',
'bg-bg-primary-light dark:bg-bg-primary-dark text-typography-body-strong-light dark:text-typography-body-strong-dark border dark:border-dark !max-w-[380px]',
style: {
padding: '8px',
paddingLeft: '16px',
@@ -33,7 +33,13 @@ const PortalToast = () => (
<>
{icon}
<div className="flex items-center">
<div className="toast-message w-full">{message}</div>
<div
className={`toast-message w-full ${
t.type === 'loading' ? 'max-w-[350px]' : 'max-w-[260px]'
}`}
>
{message}
</div>
{t.type !== 'loading' && (
<div className="ml-4">
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
@@ -33,7 +33,7 @@ export const formatComputeSizes = (addons: SubscriptionAddon[]) => {
metadata: {
default_price_id: undefined,
supabase_prod_id: 'addon_instance_micro',
features: '2-core ARM (shared) • 1GB memory • 2,085Mbps Disk IO',
features: '2-core ARM (shared) • 1GB memory • 2,606Mbps Disk IO',
},
prices: [
{
@@ -52,8 +52,7 @@ export const formSubscriptionUpdatePayload = (
},
nonChangeableAddons: SubscriptionAddon[],
selectedPaymentMethod: string,
region: string,
hcaptchaToken: string | undefined
region: string
) => {
const { computeSize, pitrDuration, customDomains } = selectedAddons
@@ -78,8 +77,7 @@ export const formSubscriptionUpdatePayload = (
...(tierPriceId && { tier: tierPriceId.id }),
addons,
proration_date,
payment_method: selectedPaymentMethod,
hcaptchaToken
payment_method: selectedPaymentMethod
}
}
@@ -114,8 +114,7 @@ const EnterpriseUpdate: FC<Props> = ({
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion,
undefined
projectRegion
),
tier: currentSubscription.tier.price_id,
}
@@ -178,8 +177,7 @@ const EnterpriseUpdate: FC<Props> = ({
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion,
token ?? undefined
projectRegion
),
tier: currentSubscription.tier.price_id,
}
@@ -109,7 +109,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
setMessage(values.message)
return setShowConfirmModal(true)
} else {
downgradeProject(values, token as string)
downgradeProject(values)
}
}
@@ -118,7 +118,7 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
captchaRef.current?.resetCaptcha()
}
const downgradeProject = async (values?: any, hcaptchaToken?: string) => {
const downgradeProject = async (values?: any) => {
const downgradeMessage = values?.message ?? message
try {
@@ -132,7 +132,6 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
tier,
addons,
proration_date,
hcaptchaToken: captchaToken ?? hcaptchaToken,
})
resetCaptcha()
@@ -137,8 +137,7 @@ const ProUpgrade: FC<Props> = ({
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion,
undefined
projectRegion
)
setIsRefreshingPreview(true)
@@ -184,8 +183,7 @@ const ProUpgrade: FC<Props> = ({
selectedAddons,
nonChangeableAddons,
selectedPaymentMethodId,
projectRegion,
captchaToken ?? undefined
projectRegion
)
const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload)
resetCaptcha()
@@ -12,4 +12,6 @@ export const HIDDEN_EXTENSIONS = [
'pg_visibility',
'pgstattuple',
'supabase_vault',
'intagg',
'xml2',
]
@@ -1,7 +1,7 @@
import { FC, useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { partition, isNull } from 'lodash'
import { Input, IconSearch, IconAlertCircle } from 'ui'
import { Input, IconSearch, IconAlertCircle, Button, IconBookOpen } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useStore, checkPermissions, useParams } from 'hooks'
@@ -9,6 +9,7 @@ import ExtensionCard from './ExtensionCard'
import { HIDDEN_EXTENSIONS } from './Extensions.constants'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import InformationBox from 'components/ui/InformationBox'
import Link from 'next/link'
interface Props {}
@@ -49,13 +50,21 @@ const Extensions: FC<Props> = ({}) => {
onChange={(e) => setFilterString(e.target.value)}
icon={<IconSearch size="tiny" />}
/>
{!canUpdateExtensions && (
{!canUpdateExtensions ? (
<div className="w-[500px]">
<InformationBox
icon={<IconAlertCircle className="text-scale-1100" strokeWidth={2} />}
title="You need additional permissions to update database extensions"
/>
</div>
) : (
<Link passHref href="https://supabase.com/docs/guides/database/extensions">
<a target="_blank" rel="noreferrer">
<Button type="default" iconRight={<IconBookOpen />}>
Learn more about extensions
</Button>
</a>
</Link>
)}
</div>
</div>
@@ -1,7 +1,7 @@
import { FC, useEffect, useContext, createContext, FormEvent, useState } from 'react'
import { isEmpty, mapValues, has, filter, keyBy, isUndefined, partition, isNull } from 'lodash'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { Button, Input, Select, SidePanel, IconTrash, Radio, IconPlus, Toggle, Modal } from 'ui'
import { Button, Input, SidePanel, IconTrash, Radio, IconPlus, Toggle, Modal, Listbox } from 'ui'
import { Dictionary } from 'components/grid'
import { makeAutoObservable } from 'mobx'
@@ -11,6 +11,8 @@ import SqlEditor from 'components/ui/SqlEditor'
import { POSTGRES_DATA_TYPES } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants'
import ConfirmationModal from 'components/ui/ConfirmationModal'
// [Refactor] Remove local state, just use the Form component
class CreateFunctionFormState {
id: number | undefined
originalName: string | undefined
@@ -557,8 +559,7 @@ const InputArgument: FC<InputArgumentProps> = observer(({ idx, name, type, error
})
}
function onTypeChange(e: FormEvent<HTMLSelectElement>) {
const _value = e.currentTarget.value
function onTypeChange(_value: string) {
_localState!.onFormArrayChange({
key: 'args',
value: { name, type: _value },
@@ -587,21 +588,23 @@ const InputArgument: FC<InputArgumentProps> = observer(({ idx, name, type, error
error={error}
disabled={readonly}
/>
<Select
<Listbox
id={`type-${idx}`}
className="flex-1"
value={type}
onChange={onTypeChange}
size="small"
onChange={onTypeChange}
disabled={readonly}
>
<Select.Option value="integer">integer</Select.Option>
<Listbox.Option value="integer" label="integer">
integer
</Listbox.Option>
{POSTGRES_DATA_TYPES.map((x: string) => (
<Select.Option key={x} value={x}>
<Listbox.Option key={x} value={x} label={x}>
{x}
</Select.Option>
</Listbox.Option>
))}
</Select>
</Listbox>
{!readonly && (
<div>
<Button
@@ -761,27 +764,22 @@ const SelectSchema: FC = observer(({}) => {
const _localState = useContext(CreateFunctionContext)
return (
<Select
<Listbox
id="schema"
label="Schema"
layout="horizontal"
value={_localState!.formState.schema.value}
onChange={(e) =>
_localState!.onFormChange({
key: 'schema',
value: e.target.value,
})
}
placeholder="Pick a schema"
size="small"
layout="horizontal"
placeholder="Pick a schema"
value={_localState!.formState.schema.value}
descriptionText="Tables made in the table editor will be in 'public'"
onChange={(value) => _localState!.onFormChange({ key: 'schema', value })}
>
{_localState!.schemas.map((x) => (
<Select.Option key={x.name} value={x.name}>
<Listbox.Option key={x.name} label={x.name} value={x.name}>
{x.name}
</Select.Option>
</Listbox.Option>
))}
</Select>
</Listbox>
)
})
@@ -795,36 +793,31 @@ const SelectLanguage: FC = observer(({}) => {
)
return (
<div className="space-y-4">
<Select
id="language"
label="Language"
layout="horizontal"
value={_localState!.formState.language.value}
onChange={(e) =>
_localState!.onFormChange({
key: 'language',
value: e.target.value,
<Listbox
id="language"
size="small"
label="Language"
layout="horizontal"
value={_localState!.formState.language.value}
placeholder="Pick a language"
onChange={(value) => _localState!.onFormChange({ key: 'language', value })}
>
<Listbox.Option value="sql" label="sql">
sql
</Listbox.Option>
{
//map through all selected extensions that start with pl
enabledExtensions
.filter((ex: any) => {
return ex.name.startsWith('pl')
})
}
placeholder="Pick a language"
size="small"
>
<Select.Option value="sql">sql</Select.Option>
{
//map through all selected extensions that start with pl
enabledExtensions
.filter((ex: any) => {
return ex.name.startsWith('pl')
})
.map((ex) => (
<Select.Option key={ex.name} value={ex.name}>
{ex.name}
</Select.Option>
))
}
</Select>
</div>
.map((ex) => (
<Listbox.Option key={ex.name} value={ex.name} label={ex.name}>
{ex.name}
</Listbox.Option>
))
}
</Listbox>
)
})
@@ -832,31 +825,32 @@ const SelectReturnType: FC = observer(({}) => {
const _localState = useContext(CreateFunctionContext)
return (
<div className="space-y-4">
<Select
id="returnType"
label="Return type"
layout="horizontal"
value={_localState!.formState.returnType.value}
onChange={(e) =>
_localState!.onFormChange({
key: 'returnType',
value: e.target.value,
})
}
size="small"
>
<Select.Option value="void">void</Select.Option>
<Select.Option value="record">record</Select.Option>
<Select.Option value="trigger">trigger</Select.Option>
<Select.Option value="integer">integer</Select.Option>
{POSTGRES_DATA_TYPES.map((x: string) => (
<Select.Option key={x} value={x}>
{x}
</Select.Option>
))}
</Select>
</div>
<Listbox
id="returnType"
size="small"
label="Return type"
layout="horizontal"
value={_localState!.formState.returnType.value}
onChange={(value) => _localState!.onFormChange({ key: 'returnType', value })}
>
<Listbox.Option value="void" label="void">
void
</Listbox.Option>
<Listbox.Option value="record" label="record">
record
</Listbox.Option>
<Listbox.Option value="trigger" label="trigger">
trigger
</Listbox.Option>
<Listbox.Option value="integer" label="integer">
integer
</Listbox.Option>
{POSTGRES_DATA_TYPES.map((x: string) => (
<Listbox.Option key={x} value={x} label={x}>
{x}
</Listbox.Option>
))}
</Listbox>
)
})
@@ -864,25 +858,24 @@ const SelectBehavior: FC = observer(({}) => {
const _localState = useContext(CreateFunctionContext)
return (
<div className="space-y-4">
<Select
id="behavior"
label="Behavior"
layout="horizontal"
value={_localState!.formState.behavior.value}
onChange={(e) =>
_localState!.onFormChange({
key: 'behavior',
value: e.target.value,
})
}
size="small"
>
<Select.Option value="IMMUTABLE">immutable</Select.Option>
<Select.Option value="STABLE">stable</Select.Option>
<Select.Option value="VOLATILE">volatile</Select.Option>
</Select>
</div>
<Listbox
id="behavior"
size="small"
label="Behavior"
layout="horizontal"
value={_localState!.formState.behavior.value}
onChange={(value) => _localState!.onFormChange({ key: 'behavior', value })}
>
<Listbox.Option value="IMMUTABLE" label="immutable">
immutable
</Listbox.Option>
<Listbox.Option value="STABLE" label="stable">
stable
</Listbox.Option>
<Listbox.Option value="VOLATILE" label="volatile">
volatile
</Listbox.Option>
</Listbox>
)
})
@@ -24,8 +24,7 @@ const GeneralContent = ({ autoApiService, selectedLang, page, showApiKey }: any)
showApiKey={showApiKey}
/>
)
if (selected == 'tables-intro')
return <TablesIntroduction autoApiService={autoApiService} selectedLang={selectedLang} />
if (selected == 'tables-intro') return <TablesIntroduction selectedLang={selectedLang} />
if (selected == 'rpc-intro') return <RpcIntroduction />
else
return (
@@ -15,7 +15,7 @@ export const CLIENT_LIBRARIES = [
},
{
language: 'Flutter',
officialSupport: false,
officialSupport: true,
releaseState: undefined,
docsUrl: 'https://supabase.com/docs/reference/dart/installing',
gitUrl: 'https://github.com/supabase/supabase-flutter',
@@ -147,6 +147,7 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
<div className="space-y-2">
{roles && (
<Select
defaultValue={roles.find((role) => role.name === 'Developer')?.id}
name="role"
label="Member role"
error={
@@ -1,113 +0,0 @@
import { Button, IconRefreshCw } from 'ui'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { DatePickerToFrom } from '../Settings/Logs'
import DatePickers from '../Settings/Logs/Logs.DatePickers'
import {
renderErrorRateChart,
renderRequestsPathsTable,
renderStatusCodesChart,
} from './renderers/OverviewRenderers'
import { PRESET_CONFIG, REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
import { Presets } from './Reports.types'
import ReportWidget from './ReportWidget'
interface Props {
preset: Presets
projectRef: string
}
const DEFAULT_PARAMS = {
iso_timestamp_start: REPORTS_DATEPICKER_HELPERS[0].calcFrom(),
iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
}
const PresetReport: React.FC<Props> = ({ projectRef, preset }) => {
const config = PRESET_CONFIG[preset]
const [statusCodes, statusCodesHandlers] = useLogsQuery(projectRef, {
sql: config.sql.statusCodes,
...DEFAULT_PARAMS,
})
const [requestPaths, requestPathsHandlers] = useLogsQuery(projectRef, {
sql: config.sql.requestPaths,
...DEFAULT_PARAMS,
})
const [errorRate, errorRateHandlers] = useLogsQuery(projectRef, {
sql: config.sql.errorRate,
...DEFAULT_PARAMS,
})
const handleRefresh = () => {
statusCodesHandlers.runQuery()
requestPathsHandlers.runQuery()
errorRateHandlers.runQuery()
}
const handleDatepickerChange = ({ to, from }: DatePickerToFrom) => {
const newParams = { iso_timestamp_start: from || '', iso_timestamp_end: to || '' }
statusCodesHandlers.setParams((prev) => ({ ...prev, ...newParams }))
requestPathsHandlers.setParams((prev) => ({ ...prev, ...newParams }))
errorRateHandlers.setParams((prev) => ({ ...prev, ...newParams }))
}
const isLoading = statusCodes.isLoading || requestPaths.isLoading
return (
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32">
<h1 className="text-2xl text-scale-1200">{config.title}</h1>
<div className="flex flex-row justify-between">
<DatePickers
onChange={handleDatepickerChange}
to={statusCodes.params.iso_timestamp_end || ''}
from={statusCodes.params.iso_timestamp_end || ''}
helpers={REPORTS_DATEPICKER_HELPERS}
/>
<Button
type="default"
size="tiny"
onClick={handleRefresh}
disabled={isLoading ? true : false}
icon={
<IconRefreshCw
size="tiny"
className={`text-scale-1100 ${isLoading ? 'animate-spin' : ''}`}
/>
}
>
Refresh
</Button>
</div>
<div className="grid lg:grid-cols-4">
<ReportWidget
isLoading={isLoading}
params={statusCodes.params}
className="col-span-4 col-start-1"
title="API Status Codes"
description="Distribution of API responses by status codes."
data={statusCodes.logData}
renderer={renderStatusCodesChart}
/>
<ReportWidget
isLoading={isLoading}
params={errorRate.params}
className="col-span-4 col-start-1"
title="Error Rate"
description="Percentage of API 5XX and 4XX error responses."
data={errorRate.logData}
renderer={renderErrorRateChart}
/>
<ReportWidget
isLoading={isLoading}
params={requestPaths.params}
className="col-span-4 col-start-1"
title="Slow API Requests"
description="Most frequently used requests, sorted by total querying time"
data={requestPaths.logData}
renderer={renderRequestsPathsTable}
/>
</div>
</div>
)
}
export default PresetReport
@@ -2,15 +2,19 @@ import { Button, Dropdown, IconMoreVertical } from 'ui'
import LoadingOpacity from 'components/ui/LoadingOpacity'
import Panel from 'components/ui/Panel'
import { useRouter } from 'next/router'
import { BaseReportParams } from './Reports.types'
import { LogsEndpointParams } from '../Settings/Logs'
import * as Tooltip from '@radix-ui/react-tooltip'
import { IconHelpCircle } from '@supabase/ui'
export interface ReportWidgetProps<T = any> {
data: T[]
title: string
description: string
description?: string
tooltip?: string
className?: string
renderer: (props: ReportWidgetProps) => React.ReactNode
params: LogsEndpointParams
params: BaseReportParams | LogsEndpointParams
isLoading: boolean
}
@@ -18,11 +22,37 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
const router = useRouter()
const { ref } = router.query
return (
<Panel className={'pb-0 ' + props.className} bodyClassName="h-full" wrapWithLoading={false}>
<Panel
noMargin
className={'pb-0 ' + props.className}
bodyClassName="h-full"
wrapWithLoading={false}
>
<Panel.Content className="space-y-4">
<div className="flex flex-row items-start justify-between">
<div className="gap-2">
<h3>{props.title}</h3>
<div className="flex flex-row gap-2">
<h3 className="w-full h-6">{props.title}</h3>{' '}
{props?.tooltip && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle className="text-scale-900" size="tiny" />
</Tooltip.Trigger>
<Tooltip.Content side="top">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 max-w-xs leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">{props.tooltip}</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
<p className="text-sm text-scale-1100">{props.description}</p>
</div>
<Dropdown
@@ -34,7 +64,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
router.push({
pathname: `/project/${ref}/logs/explorer`,
query: {
q: props.params.sql,
q: props.params?.sql,
its: props.params.iso_timestamp_start,
ite: props.params.iso_timestamp_end,
},
@@ -51,7 +81,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
title="Actions..."
as="span"
type="text"
icon={<IconMoreVertical className="text-scale-1100" size="small" />}
icon={<IconMoreVertical className="text-scale-1000" size="tiny" />}
></Button>
</Dropdown>
</div>
@@ -1,6 +1,6 @@
import dayjs from 'dayjs'
import { DatetimeHelper } from '../Settings/Logs'
import { Presets } from './Reports.types'
import { PresetConfig, Presets } from './Reports.types'
export const LAYOUT_COLUMN_COUNT = 24
@@ -15,78 +15,231 @@ export const REPORTS_DATEPICKER_HELPERS: DatetimeHelper[] = [
text: 'Last 14 days',
calcFrom: () => dayjs().subtract(7, 'day').startOf('day').toISOString(),
calcTo: () => '',
default: true,
},
{
text: 'Last 30 days',
calcFrom: () => dayjs().subtract(30, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
{
text: 'Last 90 days',
calcFrom: () => dayjs().subtract(90, 'day').startOf('day').toISOString(),
calcTo: () => '',
},
]
export const PRESET_CONFIG = {
[Presets.OVERVIEW]: {
title: '',
sql: {
statusCodes: `
select
timestamp_trunc(timestamp, hour) as timestamp,
r.status_code as status_code,
count(status_code) as count
FROM
edge_logs
CROSS JOIN UNNEST(metadata) AS m
CROSS JOIN UNNEST(m.response) AS r
GROUP BY
timestamp,
status_code
ORDER BY
timestamp ASC`,
errorRate: `
select
timestamp_trunc(timestamp, hour) as timestamp,
count(timestamp) as count
FROM
edge_logs
LEFT JOIN UNNEST(metadata) AS f1 ON TRUE
LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE
WHERE
f3.status_code >= 400
GROUP BY
export const DEFAULT_QUERY_PARAMS = {
iso_timestamp_start: REPORTS_DATEPICKER_HELPERS[0].calcFrom(),
iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
}
export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
[Presets.API]: {
title: 'API',
queries: {
statusCodes: {
queryType: 'logs',
sql: `
select
timestamp_trunc(timestamp, hour) as timestamp,
r.status_code as status_code,
count(status_code) as count
FROM
edge_logs
CROSS JOIN UNNEST(metadata) AS m
CROSS JOIN UNNEST(m.response) AS r
GROUP BY
timestamp,
status_code
ORDER BY
timestamp ASC`,
},
errorRates: {
queryType: 'logs',
sql: `
select
timestamp_trunc(timestamp, hour) as timestamp,
count(timestamp) as count
FROM
edge_logs
LEFT JOIN UNNEST(metadata) AS f1 ON TRUE
LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE
WHERE
f3.status_code >= 400
GROUP BY
timestamp
ORDER BY
timestamp ASC
`,
},
requestPaths: {
queryType: 'logs',
sql: `
select
f2.path as path,
f2.search as query_params,
f2.method as method,
f3.status_code as status_code,
avg(f3.origin_time) as avg_origin_time,
sum(f3.origin_time) as sum,
APPROX_QUANTILES(f3.origin_time, 100) as quantiles,
count(timestamp) as count
FROM
edge_logs
LEFT JOIN UNNEST(metadata) AS f1 ON TRUE
LEFT JOIN UNNEST(f1.request) AS f2 ON TRUE
LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE
GROUP BY
path,
query_params,
method,
status_code
ORDER BY
sum DESC,
count desc,
avg_origin_time DESC
LIMIT 50
`,
},
userAgents: {
queryType: 'logs',
sql: `
select
h.user_agent as user_agent,
cf.asOrganization as request_source,
count(f.id) as count
from
edge_logs as f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(r.cf) as cf
cross join unnest(r.headers) as h
group by
user_agent,
request_source
order by
count desc
limit 12
`,
},
botScores: {
queryType: 'logs',
sql: `
select
h.cf_connecting_ip as ip,
h.cf_ipcountry as country,
h.user_agent as user_agent,
r.path as path,
b.score as bot_score,
b.verifiedBot as bot_verified,
count(f.id) as count
from
edge_logs as f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(r.cf) as cf
cross join unnest(r.headers) as h
cross join unnest(cf.botManagement) as b
group by
ip,
country,
path,
user_agent,
bot_score,
bot_verified
order by
bot_score desc
limit 20
`,
},
},
},
[Presets.AUTH]: {
title: 'Auth',
queries: {
bannedUsers: {
queryType: 'db',
sql: (_params) => `
select
count(distinct u.id) as count
from auth.users u
where u.banned_until is not null and u.banned_until > now()
limit 1
`,
},
unverifiedUsers: {
queryType: 'db',
sql: (params) => `
select
date_trunc('day', u.created_at) as timestamp,
count(distinct u.id) as count
from auth.users u
where u.confirmed_at is null and '${params.iso_timestamp_start}' < u.created_at
group by timestamp
order by timestamp desc
`,
},
signUpProviders: {
queryType: 'db',
sql: (params) => `
select
sum(count(u.id)) over (order by date_trunc('day', u.created_at)) as count,
date_trunc('day', u.created_at) as timestamp,
i.provider as provider
from auth.users u
join auth.identities as i on u.id = i.user_id
where date_trunc('day', u.created_at) > '${params.iso_timestamp_start}'
group by timestamp, provider
order by timestamp desc
`,
},
failedMigrations: {
queryType: 'logs',
sql: `
select count(f.id)
from auth_logs
where json_value(f.event_message, "$.level") = "fatal"
and regexp_contains(f.event_message, "error executing migrations")
limit 1
`,
},
dailyActiveUsers: {
queryType: 'logs',
sql: `
select
timestamp_trunc(timestamp, day) as timestamp,
count(distinct json_value(f.event_message, "$.user_id")) as count
from auth_logs f
where json_value(f.event_message, "$.action") = "login"
group by
timestamp
ORDER BY
timestamp ASC
order by
timestamp desc
`,
requestPaths: `
select
f2.path as path,
f2.search as query_params,
f2.method as method,
f3.status_code as status_code,
avg(f3.origin_time) as avg_origin_time,
sum(f3.origin_time) as sum,
APPROX_QUANTILES(f3.origin_time, 100) as quantiles,
count(timestamp) as count
FROM
edge_logs
LEFT JOIN UNNEST(metadata) AS f1 ON TRUE
LEFT JOIN UNNEST(f1.request) AS f2 ON TRUE
LEFT JOIN UNNEST(f1.response) AS f3 ON TRUE
GROUP BY
path,
query_params,
method,
status_code
ORDER BY
sum DESC,
count desc,
avg_origin_time DESC
LIMIT 50
},
cumulativeUsers: {
queryType: 'db',
sql: (params) => `
select
date_trunc('day', u.created_at) as timestamp,
sum(count(u.id)) over (order by date_trunc('day', u.created_at)) as count
from auth.users as u
where u.confirmed_at is not null and date_trunc('day', u.created_at) > '${params.iso_timestamp_start}'
group by timestamp
order by timestamp desc
`,
},
newUsers: {
queryType: 'db',
sql: (params) => `
select
date_trunc('day', u.created_at) as timestamp,
count(u.id) as count
from auth.users as u
where u.confirmed_at is not null and date_trunc('day', u.created_at) > '${params.iso_timestamp_start}'
group by
timestamp
order by
timestamp desc
`,
},
},
},
}
@@ -1,5 +1,41 @@
import { ResponseError } from 'types'
import { DEFAULT_QUERY_PARAMS } from './Reports.constants'
export enum Presets {
OVERVIEW = 'overview',
API = 'api',
AUTH = 'auth',
}
export interface QueryDataBase {
isLoading: boolean
error: string
}
export interface DbQueryData<T = any> extends QueryDataBase {
data: T[]
params: BaseReportParams
logData?: never
}
export interface DbQueryHandler {
runQuery: () => void
setParams?: never
changeQuery?: never
}
export type MetaQueryResponse = any & { error: ResponseError }
export type BaseReportParams = typeof DEFAULT_QUERY_PARAMS & { sql?: string } & unknown
export interface PresetConfig {
title: string
queries: Record<string, DbQuery | LogsQuery>
}
export interface DbQuery {
queryType: 'db'
sql: string | ((params: BaseReportParams) => string)
}
export interface LogsQuery {
queryType: 'logs'
sql: string
}
export interface StatusCodesDatum {
timestamp: number
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more