mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
149 files changed
+3420
-4666
No files matched your search
@@ -2,3 +2,4 @@
|
||||
/apps/www/ @supabase/frontend
|
||||
/apps/docs/ @supabase/docs
|
||||
/spec/ @supabase/docs
|
||||
/docker/ @supabase/cli
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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' : ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -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
|
||||
)}`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
Generated
+71
-30
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(' ')}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -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
|
||||
@@ -16,3 +16,5 @@ window.matchMedia = jest.fn().mockReturnValue({
|
||||
removeEventListener: jest.fn(),
|
||||
})
|
||||
export const StoreProvider = StoreProvider
|
||||
|
||||
export const withAuth = jest.fn()
|
||||
@@ -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
Reference in new issue
Block a user