Merge pull request #12406 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-02-14 15:22:30 +08:00
commit 49624c644f
71 files changed
+8937 -270

No files matched your search

+16 -48
View File
@@ -1,53 +1,21 @@
import providers from '../data/authProviders'
import ButtonCard from './ButtonCard'
import { IconPanel } from 'ui'
import Link from 'next/link'
export default function AuthProviders({ type }: { type: string }) {
const filterProviders = providers.filter((item) => item.authType === type)
export default function AuthProviders() {
return (
<div className="grid md:grid-cols-12 gap-4">
{providers.map((x) => (
<div key={x.name} className="col-span-6">
<ButtonCard to={x.href} title={x.name}>
<div className="px-6 py-4">
<div className="flex justify-between">
<p className="mt-0">{x.name}</p>
<p className="mt-0">
{x.official ? (
<span className={`badge badge--official`}>Official</span>
) : (
<span className={`badge badge--unofficial`}>Unofficial</span>
)}
</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Platform:</span>
<span>{x.platform.toString()}</span>
</div>
<div
className="code-block"
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
fontSize: '0.7rem',
}}
>
<span>Self-Hosted:</span>
<span>{x.selfHosted.toString()}</span>
</div>
</div>
</div>
</ButtonCard>
</div>
))}
</div>
<>
<div className="grid grid-cols-12 gap-10 not-prose py-8">
{filterProviders.map((x) => (
<Link href={`${x.href}`} key={x.name} passHref>
<a className="col-span-6 lg:col-span-4 xl:col-span-3">
<IconPanel title={x.name} icon={x.logo} />
</a>
</Link>
))}
</div>
</>
)
}
+3 -3
View File
@@ -30,12 +30,12 @@ const questions = [
'How do I connect to my database?',
'How do I run migrations? ',
'How do I listen to changes in a table?',
'How do I setup authentication?',
'How do I set up authentication?',
]
function getEdgeFunctionUrl() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL.replace(/\/$/, '')
const isPlatform = supabaseUrl.match(/(supabase\.co)|(supabase\.in)/)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
const isPlatform = supabaseUrl?.match(/(supabase\.co)|(supabase\.in)/)
if (isPlatform) {
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
@@ -25,9 +25,10 @@ interface Props {
* expected tocList format (text, link,level).
*
* In tsx files, we can generate this tocList directly. For these files, we don't
* need to parse the <a> and generate anchors
* need to parse the <a> and generate anchors. Custom anchors are used in tsx files.
* (see: /pages/reference/cli/config.tsx)
*/
const Heading: React.FC<Props> = ({ tag, parseAnchors = false, customAnchor, children }) => {
const Heading: React.FC<Props> = ({ tag, customAnchor, children }) => {
const HeadingTag = `${tag}` as any
const anchor = customAnchor ? customAnchor : getAnchor(children)
const link = `#${anchor}`
@@ -43,7 +44,7 @@ const Heading: React.FC<Props> = ({ tag, parseAnchors = false, customAnchor, chi
return (
<HeadingTag id={anchor} ref={ref} className="group scroll-mt-24">
{parseAnchors ? removeAnchor(children) : children}
{removeAnchor(children)}
{anchor && (
<a href={link} className="ml-2 opacity-0 group-hover:opacity-100 transition">
#
+19 -20
View File
@@ -1,34 +1,33 @@
import { FC, useEffect, useState } from 'react'
import createConfigCatClient from 'configcat-js'
// import createConfigCatClient from 'configcat-js'
import FlagContext from './FlagContext'
const FlagProvider: FC = ({ children }) => {
const [store, setStore] = useState({})
const { Provider } = FlagContext
useEffect(() => {
getFlags()
}, [])
// useEffect(() => {
// getFlags()
// }, [])
const getFlags = async () => {
const setFlagValues = async () => {
const flagValues = await client.getAllValuesAsync()
const flagStore: any = {}
// const getFlags = async () => {
// const setFlagValues = async () => {
// const flagValues = await client.getAllValuesAsync()
// const flagStore: any = {}
flagValues.forEach((item: any) => {
flagStore[item.settingKey] = item.settingValue
})
setStore(flagStore)
}
// flagValues.forEach((item: any) => {
// flagStore[item.settingKey] = item.settingValue
// })
// setStore(flagStore)
// }
const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
configChanged: setFlagValues,
pollIntervalSeconds: 600,
})
// const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
// configChanged: setFlagValues,
// pollIntervalSeconds: 600,
// })
await setFlagValues()
}
// await setFlagValues()
// }
return <Provider value={store}>{children}</Provider>
}
@@ -400,7 +400,7 @@ export const database = {
},
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
{
name: 'PGRoonga: Multilingual Full Text Search',
name: 'PGroonga: Multilingual Full Text Search',
url: '/guides/database/extensions/pgroonga',
items: [],
},
@@ -658,6 +658,11 @@ export const resources = {
url: '/guides/resources/migrating-to-supabase/heroku',
items: [],
},
{
name: 'Render',
url: '/guides/resources/migrating-to-supabase/render',
items: [],
},
],
},
{
@@ -720,6 +725,7 @@ export const migrate = {
{ name: 'Firestore Data', url: '/guides/migrations/firestore-data', items: [] },
{ name: 'Firebase Storage', url: '/guides/migrations/firebase-storage', items: [] },
{ name: 'Heroku', url: '/guides/migrations/heroku', items: [] },
{ name: 'Render', url: '/guides/migrations/render', items: [] },
],
}
+38 -17
View File
@@ -1,172 +1,193 @@
const authProviders = [
{
name: 'Apple',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/apple-icon',
href: '/guides/auth/social-login/auth-apple',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Azure',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/microsoft-icon',
href: '/guides/auth/social-login/auth-azure',
official: false,
supporter: 'TBD',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Bitbucket',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/bitbucket-icon',
href: '/guides/auth/social-login/auth-bitbucket',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Discord',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/discord-icon',
href: '/guides/auth/social-login/auth-discord',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Facebook',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/facebook-icon',
href: '/guides/auth/social-login/auth-facebook',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitHub',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/github-icon',
href: '/guides/auth/social-login/auth-github',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitLab',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/gitlab-icon',
href: '/guides/auth/social-login/auth-gitlab',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Google',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/google-icon',
href: '/guides/auth/social-login/auth-google',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Keycloak',
logo: '/docs/img/icons/keycloak-icon',
href: '/guides/auth/social-login/auth-keycloak',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'LinkedIn',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/linkedin-icon',
href: '/guides/auth/social-login/auth-linkedin',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'MessageBird',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/messagebird-icon',
href: '/guides/auth/phone-login/messagebird',
official: false,
supporter: 'MessageBird',
platform: true,
selfHosted: true,
authType: 'phone'
},
{
name: 'Notion',
// logo: '/img/libraries/notion-icon.svg',
logo: '/docs/img/icons/notion-icon',
href: '/guides/auth/social-login/auth-notion',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Slack',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/slack-icon',
href: '/guides/auth/social-login/auth-slack',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Spotify',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/spotify-icon',
href: '/guides/auth/social-login/auth-spotify',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitter',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitter-icon',
href: '/guides/auth/social-login/auth-twitter',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twitch',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twitch-icon',
href: '/guides/auth/social-login/auth-twitch',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Zoom',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/zoom-icon',
href: '/guides/auth/social-login/auth-zoom',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'Twilio',
// logo: '/img/libraries/dart-icon.svg',
logo: '/docs/img/icons/twilio-icon',
href: '/guides/auth/phone-login/twilio',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
{
name: 'Vonage',
logo: '/docs/img/icons/vonage-icon',
href: '/guides/auth/phone-login/vonage',
official: false,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'phone',
},
]
+1
View File
@@ -53,6 +53,7 @@ const nextConfig = {
'github.com',
'user-images.githubusercontent.com',
'raw.githubusercontent.com',
'weweb-changelog.ghost.io',
],
},
experimental: {
@@ -257,7 +257,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
```ts title=src/routes/posts/+page.server.ts
import type { Actions } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { error, invalid } from '@sveltejs/kit'
import { error, fail } from '@sveltejs/kit'
export const actions: Actions = {
createPost: async (event) => {
@@ -276,7 +276,7 @@ export const actions: Actions = {
.insert({ content })
if (createPostError) {
return invalid(500, {
return fail(500, {
supabaseErrorMessage: createPostError.message,
})
}
@@ -293,7 +293,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
```ts
import type { Actions } from './$types'
import { invalid, redirect } from '@sveltejs/kit'
import { fail, redirect } from '@sveltejs/kit'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
import { AuthApiError } from '@supabase/supabase-js'
@@ -313,14 +313,14 @@ export const actions: Actions = {
if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return invalid(400, {
return fail(400, {
error: 'Invalid credentials.',
values: {
email,
},
})
}
return invalid(500, {
return fail(500, {
error: 'Server error. Try again later.',
values: {
email,
+9 -1
View File
@@ -44,8 +44,16 @@ You can authenticate your users in several ways:
We provide a suite of Providers and login methods, as well as [Auth helpers](/docs/guides/auth/auth-helpers/).
#### Social Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders />
<AuthProviders type="social" />
</div>
#### Phone Auth
<div className="container" style={{ padding: 0 }}>
<AuthProviders type="phone" />
</div>
### Configure third-party providers
+6 -6
View File
@@ -209,12 +209,12 @@ curl '<SUPABASE_URL>/rest/v1/todos' \
</TabPanel>
</Tabs>
JS Reference: [select()](../reference/javascript/select),
[insert()](../reference/javascript/insert),
[update()](../reference/javascript/update),
[upsert()](../reference/javascript/upsert),
[delete()](../reference/javascript/delete),
[rpc()](../reference/javascript/rpc) (call Postgres functions).
JS Reference: [select()](/reference/javascript/select),
[insert()](/reference/javascript/insert),
[update()](/reference/javascript/update),
[upsert()](/reference/javascript/upsert),
[delete()](/reference/javascript/delete),
[rpc()](/reference/javascript/rpc) (call Postgres functions).
### GraphQL API
@@ -127,7 +127,7 @@ Edge Functions supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`.
- Deno Deploy limitations
- Deno does not support outgoing connections to ports `25`, `465`, and `587`.
- Cannot write to File System
- Cannot read or write to File System
- Edge Functions
- Local development - only one function at a time
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
@@ -6,37 +6,52 @@ export const meta = {
description: 'Understanding how database usage applies to your subscription.',
}
Database storage refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily.
Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily.
Database size is the total size of used storage from your database, whereas disk size describes the size of the underlying available storage.
For an instantaneous live view of the DB storage usage, you can execute in Postgres:
For an instantaneous live view of the DB size, you can execute in Postgres:
```sql
select sum(pg_database_size(pg_database.datname)) / (1024 * 1024) as db_size_mb
from pg_database;
select
sum(pg_database_size (pg_database.datname)) / (1024 * 1024) as db_size_mb
from
pg_database;
```
This value is also reported in the [database settings page](https://app.supabase.com/project/_/settings/database).
## Database storage management
Supabase uses network-attached storage to balance performance with scalability. For Pro and Enterprise projects, database storage expands ~1.5x automatically (e.g., 8GB -> 12GB) when you reach 90% of the database storage quota. Automatic database storage expansion can only occur once every six hours. Pro projects can store up to 1024TB.
Supabase uses network-attached storage to balance performance with scalability.
For Pro and Enterprise projects, disk size expands ~1.5x automatically (e.g., 8GB -> 12GB) when you reach 90% of the disk size.
Automatic disk size expansion can only occur once every six hours.
Pro projects can store up to 1024TB.
All projects enter read-only mode when you reach 95% of the database storage quota. In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`. Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the database storage quota.
All projects enter read-only mode when you reach 95% of the disk size. In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`.
Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the disk size.
If you need more than 1024TB of database storage or require multiple storage expansions in a short period of time, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
If you need more than 1024TB of disk size or require multiple storage expansions in a short period of time, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
### Increasing available database storage
### Increasing available disk size
1. [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to increase your storage quota.
1. [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to increase your quota and expand your disk size automatically.
2. Delete data from your project's database to lower its disk usage. If your database is already in read-only mode, run the following command to change the transaction mode to read-write for your session:
```sql
SET default_transaction_read_only = 'off';
SET
default_transaction_read_only = 'off';
```
This allows you to delete data from within the session.
### Preoccupied Space
When launching a new project, your database size will be roughly ~40-60mb.
The space is used up by preinstalled extensions, schema/data used by our services that are offered with each project and default Postgres data.
When installing additional extensions, even if you don't actively use them, additional database size is used.
## Vacuum operations
Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. Instead, they are internally marked as removed until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed.
+6
View File
@@ -135,6 +135,12 @@ export const migrationGuides = [
href: '/guides/resources/migrating-to-supabase/heroku',
description: 'Migrate your Heroku Postgres database to Supabase.',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
description: 'Migrate your Render Postgres database to Supabase.',
},
]
export const postgres = [
@@ -0,0 +1,88 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Migrate from Render to Supabase',
description: 'Migrate your Render Postgres database to Supabase.',
}
Render is a popular Web Hosting service in the online services category that also has a managed Postgres service. Render has a great developer experience, allowing users to deploy straight from GitHub or GitLab. This is the core of their product and they do it really well. However, when it comes to Postgres databases, it may not be the best option.
Supabase is one of the best free alternative to Render Postgres. Supabase provide all the backend features developers need to build a product: a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, and storage. Postgres is the core of Supabase—for example, you can use row-level security and there are more than 40 Postgres extensions available.
This guide demonstrates how to migrate from Render to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
## Retrieve your Render database credentials [#retrieve-render-credentials]
1. Log in to your [Render account](https://render.com) and select the project you want to migrate.
1. Click **Dashboard** in the menu and click in your **Postgres** database.
1. Scroll down in the **Info** tab.
1. Click on **PSQL Command** and edit it adding the content after `PSQL_COMMAND=`.
![Copying PSQL command from Render dashboard](/docs/img/guides/resources/migrating-to-supabase/render/render_dashboard.png)
Example:
```bash
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.render.com -U my_db_pxl0_user my_db_pxl0
```
## Retrieve your Supabase Host [#retrieve-supabase-host]
1. If you're new to Supabase, [create a project](https://app.supabase.com).
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
1. Save your password or [reset it](https://app.supabase.com/project/_/settings/database) if you forgot it.
![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/render/supabase_dashboard.png)
## Migrate the database
The fastest way to migrate your database is with the Supabase migration tool on [Google Colab](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb). Alternatively, you can use the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full PostgreSQL installation.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="colab"
>
<TabPanel id="colab" label="Migrate using Colab">
1. Set the environment variables (`PSQL_COMMAND`, `SUPABASE_HOST`, `SUPABASE_PASSWORD`) in the Colab notebook.
1. Run the first two steps in [the notebook](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb) in order. The first sets the variables and the second installs PSQL and the migration script.
1. Run the third step to start the migration. This will take a few minutes.
</TabPanel>
<TabPanel id="cli" label="Migrate using CLI tools">
### Export your Render database to a file in console [#export-render-database]
Use `pg_dump` with your Render credentials to export your Render database to a file (e.g., `render_dump.sql`).
```bash
pg_dump --clean --if-exists --quote-all-identifiers \
-h $RENDER_HOST -U $RENDER_USER -d $RENDER_DATABASE \
--no-owner --no-privileges > render_dump.sql
```
### Import the database to your Supabase project [#import-database-to-supabase]
Use `psql` to import the Render database file to your Supabase project.
```bash
psql -h $SUPA_URL -U postgres --file render_dump.sql -p 6543 -d postgres
```
### Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
- To exclude a schema: `--exclude-schema=PATTERN`.
- To only migrate a single table: `--table=PATTERN`.
- To exclude a table: `--exclude-table=PATTERN`.
Run `pg_dump --help` for a full list of options.
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
-1
View File
@@ -13,7 +13,6 @@ Bo Lu
Bobbie Soedirgo
Chase Granberry
Chris Copplestone
Danny Ng
Dave Wilson
Div Arora
Egor Romanov
Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

@@ -0,0 +1,13 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17.4653" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="13.1709" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.285156 7.2979C0.285156 3.72312 3.18309 0.825195 6.75786 0.825195H11.6109V3.65042H6.75786C4.74341 3.65042 3.11038 5.28345 3.11038 7.2979V16.259H0.285156V7.2979Z" fill="#46E7AA"/>
<rect x="8.87671" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="4.58228" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="0.288086" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="13.478" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="9.26123" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="5.04492" width="2.81974" height="2.81974" fill="#46E7AA"/>
<rect x="17.4653" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+1
View File
@@ -7,6 +7,7 @@
.DocSearch-Container {
background: rgba(0, 0, 0, 0.3);
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
@apply cursor-auto #{!important};
}
.DocSearch-LoadingIndicator svg {
@@ -119,9 +119,9 @@ begin
select
id,
content,
(documents.embedding <=> query_embedding) as similarity
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where (documents.embedding <=> query_embedding) > similarity_threshold
where 1 - (documents.embedding <=> query_embedding) > similarity_threshold
order by documents.embedding <=> query_embedding
limit match_count;
end;
+2 -2
View File
@@ -21,8 +21,8 @@
}
},
{
"title": "Database space",
"tooltip": "Billing is based on the average database size in GB throughout the billing period.",
"title": "Database size",
"tooltip": "Billing is based on the average daily database size in GB throughout the billing period.",
"tiers": {
"free": "Up to 500 MB",
"pro": "8 GB included, then $0.125 per GB",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 667 KiB

@@ -2,7 +2,7 @@ import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
const router = new Router()
router
// Note: path will be prefixed with function name
// Note: path should be prefixed with function name
.get('/oak-server', (context) => {
context.response.body = 'This is an example Oak server running on Edge Functions!'
})
@@ -0,0 +1,2 @@
#Use PolyScale connection string with application_name parameter to get code-free database caching
DATABASE_URL="postgresql:/DB_USERNAME:DB_PASSWORD@psedge.global:5432/postgres?application_name=POLYSCALE_CACHE_ID"
@@ -0,0 +1,18 @@
# postgres-on-the-edge
This function allows you to access your Supabase database directly via TCP from an edge function.
You can add a global cache to your database for fast access to your data, globally by simply connecting your Supabase database to [PolyScale](https://polyscale.ai).
## Setup
1. Rename `.env.example` to `.env`
2. Insert Supabase database connection string for `DATABASE_URL` in `.env` file
3. (Optional) Create PolyScale cache - see [instructions](https://supabase.com/docs/guides/integrations/polyscale) for adding a PolyScale cache.
4. Replace `DATABASE_URL` with PolyScale connection string.
## Deploy
1. Run `supabase functions deploy --no-verify-jwt postgres-on-the-edge`
2. Run `supabase secrets set --env-file supabase/functions/postgres-on-the-edge/.env`
Learn more about creating this function [here](https://www.youtube.com/watch?v=cl7EuF1-RsY).
@@ -0,0 +1,43 @@
import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts'
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get('DATABASE_URL')!
// Create a database pool with three connections that are lazily established
const pool = new postgres.Pool(databaseUrl, 3, true)
serve(async (_req) => {
try {
// Grab a connection from the pool
const connection = await pool.connect()
try {
// Run a query
const result = await connection.queryObject`SELECT * FROM animals`
const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
console.log(animals)
// Encode the result as pretty printed JSON
const body = JSON.stringify(
animals,
(_key, value) => (typeof value === 'bigint' ? value.toString() : value),
2
)
// Return the response with the correct content type header
return new Response(body, {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
} finally {
// Release the connection back into the pool
connection.release()
}
} catch (err) {
console.error(err)
return new Response(String(err?.message ?? err), { status: 500 })
}
})
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,81 @@
# Supabase Realtime Presence API Sample Program
This is an example program for Supabase Realtime Presence APIs.
User get authenticated using Supabase Auth API. Once Logged-in you can see which users are 'present' and viewing the page.
- Frontend:
- Next.js.
- [Supabase.js v2 (realtime presence support)](https://supabase.io/docs/library/getting-started)
- Backend:
- [app.supabase.io](https://app.supabase.io/): hosted postgres database with realtime support.
## Real time Presence APIs used.
This program shows usage of channel presence API calls such as , channel.on('presence', ...), channel.subscribe function usage.
## Getting Started
### 1. Create new project
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
### 2. Run "User Management Starter" Quickstart
This will create user tables and profile tables for user management.
### 3. Get the URL and Key
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token.
![Supabase Anon Key](supabase_anon_key.jpg?raw=true 'Supabase Anon Key')
### 4. Pull this example git repository
`git clone <<this repository url>> `
### 5. Create a .env.local file
Create a .env.local file and add following environment variables.
```
NEXT_PUBLIC_SUPABASE_URL=<<insert-your-db-url-here>>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<<insert-your-anon-key-here>>
```
### 5. Now run the development server!
Now run
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## How to Test?
When you visit http://localhost:3000, you will be redirected to auth login/signup screen.
Signup if you haven't and then login.
You will see your email listed on the page.
Open another browser window. And sign-in as another user.
As you login from other browser window, you will see list of current users updated.
## Deployment
Since this is next.js application, simplest method to deploy this repository is on Vercel.
## Conclusion/Next Steps
- Need to implement Profile page
- Need to implement ability to upload User Avatars
- Need ability to read user avatar from social media.
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
swcMinify: true,
}
module.exports = nextConfig
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,29 @@
{
"name": "nextjs-auth-presence",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write \"pages/**/*.{js,jsx,ts,tsx,css,md,json}\""
},
"dependencies": {
"@supabase/auth-helpers-nextjs": "^0.5.4",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/auth-ui-react": "^0.2.7",
"@supabase/supabase-js": "^2.7.1",
"next": "^13.1.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "18.11.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.10",
"eslint": "8.25.0",
"eslint-config-next": "12.3.1",
"typescript": "4.8.4"
}
}
@@ -0,0 +1,43 @@
import { createBrowserSupabaseClient, Session } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import '../styles/globals.css'
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session
}>) {
const router = useRouter()
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
useEffect(() => {
const {
data: { subscription },
} = supabaseClient.auth.onAuthStateChange((event, session) => {
switch (event) {
case 'SIGNED_IN':
router.push('/')
return
case 'SIGNED_OUT':
router.push('/login')
return
}
})
return subscription.unsubscribe
}, [])
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
export default MyApp
@@ -0,0 +1,80 @@
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useSupabaseClient, useUser } from '@supabase/auth-helpers-react'
import { RealtimePresenceState } from '@supabase/supabase-js'
import type { GetServerSidePropsContext, NextPage } from 'next'
import { useEffect, useState } from 'react'
const HomePage: NextPage = () => {
const supabaseClient = useSupabaseClient()
const this_user = useUser()
const [userState, setUserState] = useState<RealtimePresenceState>({})
useEffect(() => {
console.log('user: ', this_user)
const channel = supabaseClient.channel('online-users', {
config: {
presence: {
key: this_user?.email ? this_user?.email : 'Unknown',
},
},
})
channel.on('presence', { event: 'sync' }, () => {
const presentState = channel.presenceState()
console.log('inside presence: ', presentState)
setUserState({ ...presentState })
})
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('New users have joined: ', newPresences)
})
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
const status = await channel.track({
user_name: this_user?.email ? this_user?.email : 'Unknown',
})
console.log('status: ', status)
}
})
}, [])
return (
<>
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
<p> List of Currently Logged in Users: </p>
{Object.keys(userState).map((key) => (
<p key={key}>Hi {key}</p>
))}
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/login',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
export default HomePage
@@ -0,0 +1,31 @@
import { useSupabaseClient, useUser } from '@supabase/auth-helpers-react'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import type { NextPage } from 'next'
import styles from '../styles/Home.module.css'
const LoginPage: NextPage = () => {
const supabaseClient = useSupabaseClient()
const user = useUser()
if (!user) {
return (
<main className={styles.main}>
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
/>
</main>
)
}
return (
<>
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export default LoginPage
@@ -0,0 +1,39 @@
import { createServerSupabaseClient, User } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
import Link from 'next/link'
export default function Profile({ user }: { user: User }) {
return (
<>
<p>
[<Link href="/">Home</Link>] | [<Link href="/protected-page">supabaseServerClient</Link>]
</p>
<div>Hello {user.email}</div>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/login',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL="replace-this-with-your-supabase-instance"
NEXT_PUBLIC_SUPABASE_ANON_KEY="replace-this-with-your-supabasedb-anon-key"
@@ -0,0 +1,129 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}
@@ -0,0 +1,26 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
File diff suppressed because it is too large. Load diff
+36 -29
View File
@@ -14115,20 +14115,6 @@
"resolved": "packages/config",
"link": true
},
"node_modules/configcat-common": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
},
"node_modules/configcat-js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
"deprecated": "Please upgrade to version 6 or higher.",
"dependencies": {
"configcat-common": "^5.3.0"
}
},
"node_modules/console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -38931,7 +38917,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
@@ -39196,6 +39182,23 @@
"postcss": "^8.1.0"
}
},
"studio/node_modules/configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
"dependencies": {
"tslib": "^2.4.1"
}
},
"studio/node_modules/configcat-js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
"dependencies": {
"configcat-common": "^7.0.1",
"tslib": "^2.4.1"
}
},
"studio/node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -49533,19 +49536,6 @@
}
}
},
"configcat-common": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
},
"configcat-js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
"requires": {
"configcat-common": "^5.3.0"
}
},
"console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -65797,7 +65787,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
@@ -65960,6 +65950,23 @@
"postcss-value-parser": "^4.2.0"
}
},
"configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
"requires": {
"tslib": "^2.4.1"
}
},
"configcat-js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
"requires": {
"configcat-common": "^7.0.1",
"tslib": "^2.4.1"
}
},
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -18,8 +18,7 @@ function classNames(...classes: any) {
return classes.filter(Boolean).join(' ')
}
export interface Props
extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
export interface Props extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
className?: string
children: React.ReactNode
descriptionText?: string
@@ -73,14 +72,8 @@ function Listbox({
const triggerRef = useRef<HTMLButtonElement>(null)
const {
formContextOnChange,
values,
errors,
handleBlur,
touched,
fieldLevelValidation,
} = useFormContext()
const { formContextOnChange, values, errors, handleBlur, touched, fieldLevelValidation } =
useFormContext()
if (values && !value) {
defaultValue = values[id || name]
@@ -158,7 +151,7 @@ function Listbox({
/*
* if no selected value (including a `defaultvalue`), then use first child
*/
setSelectedNode(content[0].props)
setSelectedNode(content[0]?.props)
return
}
}, [selected])
@@ -257,9 +250,7 @@ function Listbox({
className={__styles.options_container}
>
<div>
<SelectContext.Provider
value={{ onChange: handleOnChange, selected }}
>
<SelectContext.Provider value={{ onChange: handleOnChange, selected }}>
{children}
</SelectContext.Provider>
</div>
@@ -313,9 +304,7 @@ function SelectOption({
<div className={__styles.option_inner}>
{addOnBefore && addOnBefore({ active, selected })}
<span>
{typeof children === 'function'
? children({ active, selected })
: children}
{typeof children === 'function' ? children({ active, selected }) : children}
</span>
</div>
@@ -326,10 +315,7 @@ function SelectOption({
active ? __styles.option_check_active : ''
)}
>
<IconCheck
className={__styles.option_check_icon}
aria-hidden="true"
/>
<IconCheck className={__styles.option_check_icon} aria-hidden="true" />
</span>
) : null}
</DropdownMenuPrimitive.Item>
+1 -1
View File
@@ -244,7 +244,7 @@ functions:
options: {
redirectTo: 'https://example.com/welcome'
}
}
})
```
- id: sign-in-with-scopes
name: Sign in with scopes
+1 -1
View File
@@ -287,7 +287,7 @@ functions:
options: {
redirectTo: 'https://example.com/welcome'
}
}
})
```
- id: sign-in-with-scopes
name: Sign in with scopes
+149 -24
View File
@@ -55,7 +55,7 @@ functions:
- If **Confirm email** is enabled, a `user` is returned but `session` is null.
- If **Confirm email** is disabled, both a `user` and a `session` are returned.
- By default, when the user confirms their email address, they are redirected to the [`SITE_URL`](https://supabase.com/docs/reference/auth/config#site_url). You can modify your `SITE_URL` or add additional redirect URLs in [your project](https://app.supabase.com/project/_/auth/url-configuration).
- If signUp() is called for an existing confirmed user:
- If sign_up() is called for an existing confirmed user:
- If **Confirm email** is enabled in [your project](https://app.supabase.com/project/_/auth/providers), an obfuscated/fake user object is returned.
- If **Confirm email** is disabled, the error message, `User already registered` is returned.
- To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/javascript/auth-getuser).
@@ -64,26 +64,131 @@ functions:
name: Sign up
code: |
```
res = supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
res = supabase.auth.sign_up({
"email": 'example@email.com',
"password": 'example-password',
})
```
- id: sign-up-with-additional-user-metadata
name: Sign up with additional user metadata
code: |
```
res = supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
options: {
data: {
first_name: 'John',
age: 27,
res = supabase.auth.sign_up({
"email": 'example@email.com',
"password": 'example-password',
"options": {
"data": {
"first_name": 'John',
"age": 27,
}
}
})
```
- id: sign-in-with-password
title: 'sign_in_with_password'
notes: |
- Requires either an email and password or a phone number and password.
examples:
- id: sign-in-with-email-and-password
name: Sign in with email and password
isSpotlight: true
code: |
```
data = supabase.auth.sign_in_with_password({"email": "j0@supabase.io", "password": "testsupabasenow"})
```
- id: sign-in-with-phone-and-password
name: Sign in with phone and password
isSpotlight: false
code: |
```
data = supabase.auth.sign_in_with_password({"phone": "+1234566", password": "testsupabasenow"})
# After receiving a SMS with a OTP.
data = supabase.auth.verify_otp({
"phone": '+13334445555',
"token": '123456',
})
```
- id: sign-in-with-otp
title: 'sign_in_with_otp'
notes: |
- Requires either an email or phone number.
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
- If the user doesn't exist, `sign_in_with_otp()` will signup the user instead. To restrict this behavior, you can set `should_create_user` in `SignInWithPasswordlessCredentials.options` to `false`.
- If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP.
- If you're using phone, you can configure whether you want the user to receive a OTP.
- The magic link's destination URL is determined by the [`SITE_URL`](/docs/reference/auth/config#site_url).
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
- Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](https://app.supabase.com/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`.
examples:
- id: sign-in-with-email
name: Sign in with email
isSpotlight: true
description: The user will be sent an email which contains either a magiclink or a OTP or both. By default, a given user can only request a OTP once every 60 seconds.
code: |
```
data = supabase.auth.sign_in_with_otp({
"email": 'example@email.com',
"options": {
"email_redirect_to": 'https://example.com/welcome'
}
})
```
- id: sign-in-with-sms-otp
name: Sign in with SMS OTP
isSpotlight: false
description: The user will be sent a SMS which contains a OTP. By default, a given user can only request a OTP once every 60 seconds.
code: |
```
data = supabase.auth.sign_in_with_otp({
phone: '+13334445555',
})
```
- id: sign-in-with-oauth
title: 'sign_in_with_oauth'
notes: |
- This method is used for signing in using a third-party provider.
- Supabase supports many different [third-party providers](https://supabase.com/docs/guides/auth#providers).
examples:
- id: sign-in-using-a-third-party-provider
name: Sign in using a third-party provider
isSpotlight: true
code: |
```
data = supabase.auth.sign_in_with_oauth({
"provider": 'github'
})
```
- id: sign-in-using-a-third-party-provider-with-redirect
name: Sign in using a third-party provider with redirect
isSpotlight: false
description: |
- When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/reference/auth/config#site_url). It does not redirect the user immediately after invoking this method.
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
code: |
```
data = supabase.auth.sign_in_with_oauth({
"provider": 'github',
"options": {
"redirect_to": 'https://example.com/welcome'
}
})
```
- id: sign-in-with-scopes
name: Sign in with scopes
isSpotlight: false
description: |
If you need additional data from an OAuth provider, you can include a space-separated list of scopes in your request to get back an OAuth provider token.
You may also need to specify the scopes in the provider's OAuth app settings, depending on the provider. The list of scopes will be documented by the third-party provider you are using and specifying scopes will enable you to use the OAuth provider token to call additional APIs supported by the third-party provider to get more information.
code: |
```
data = supabase.auth.sign_in_with_oauth({
"provider": 'github',
"options": {
"scopes": 'repo gist notifications'
}
})
oauth_token = data.session.provider_token # use to access provider API
```
- id: sign-out
title: 'sign_out()'
notes: |
@@ -96,10 +201,10 @@ functions:
res = supabase.auth.sign_out()
```
- id: verify-otp
title: 'verify_otp()'
title: 'verify_otp'
notes: |
- The `verifyOtp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `signup`, `magiclink`, `recovery`, `invite` or `email_change`.
- The verification type used should be determined based on the corresponding auth method called before `verifyOtp` to sign up / sign-in a user.
- The `verify_otp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `signup`, `magiclink`, `recovery`, `invite` or `email_change`.
- The verification type used should be determined based on the corresponding auth method called before `verify_otp` to sign up / sign-in a user.
examples:
- id: verify-sms-one-time-password
name: Verify SMS One-Time Password (OTP)
@@ -107,15 +212,35 @@ functions:
```
res = supabase.auth.verify_otp(phone, token)
```
- id: get-session
title: 'get_session_from_url'
- id: get-user
title: 'get_user'
notes: |
- This method gets the user object from the current session.
- Fetches the user object from the database instead of local session.
examples:
- id: get-session
name: Get the session data from URL
- id: get-the-logged-in-user-with-the-current-existing-session
name: Get the logged in user with the current existing session
isSpotlight: true
code: |
```
res = supabase.auth.get_session_from_url(url)
data = supabase.auth.get_user()
```
- id: get-the-logged-in-user-with-a-custom-access-token-jwt
name: Get the logged in user with a custom access token jwt
isSpotlight: false
code: |
```
data = supabase.auth.get_user(jwt)
```
- id: get-session
title: 'get_session'
examples:
- id: get-session
name: Get the session data
code: |
```
res = supabase.auth.get_session()
```
- id: set-session
@@ -132,10 +257,10 @@ functions:
description: Sets the session data from refresh_token and returns current session or an error if the refresh_token is invalid.
code: |
```
res = supabase.auth.set_session(refresh_token)
res = supabase.auth.set_session({"access_token": access_token, "refresh_token": refresh_token)
```
- id: refresh-session
title: 'refresh_ession()'
title: 'refresh_session()'
notes: |
- This method will refresh the session whether the current one is expired or not.
- Both examples destructure `user` and `session` from `data`. This is not required; so `const { data, error } =` is also valid.
@@ -158,7 +283,7 @@ functions:
```
r = supabase
.from('countries')
.select("*")
.select("*").execute()
```
- id: selecting-specific-columns
name: Selecting specific columns
@@ -166,7 +291,7 @@ functions:
```
r = await supabase
.from('countries')
.select('name')
.select('name').execute()
```
- id: invoke
+3 -4
View File
@@ -203,11 +203,10 @@ function isFilterSql(filter: Filter) {
function filterLiteral(value: any) {
if (typeof value === 'string') {
const temp = value.trim()
if (temp?.startsWith('ARRAY[') && temp?.endsWith(']')) {
return temp
if (value?.startsWith('ARRAY[') && value?.endsWith(']')) {
return value
} else {
return literal(temp)
return literal(value)
}
}
return value
@@ -1,5 +1,5 @@
import { FC } from 'react'
import { Button } from 'ui'
import { Button, IconExternalLink } from 'ui'
interface Props {
showTemplates: boolean
@@ -8,15 +8,20 @@ interface Props {
}
const PolicyEditorFooter: FC<Props> = ({ showTemplates, onViewTemplates, onReviewPolicy }) => (
<div className="flex w-full items-center justify-end gap-2 border-t px-6 py-4 dark:border-dark">
{showTemplates && (
<Button type="default" onClick={onViewTemplates}>
View templates
<div className="flex justify-between items-center border-t px-6 py-4 dark:border-dark">
<a href='https://supabase.com/docs/learn/auth-deep-dive/auth-policies' target="_blank">
<Button type='link' icon={<IconExternalLink size={14} strokeWidth={1.5} />}>Documentation</Button>
</a>
<div className="flex w-full items-center justify-end gap-2">
{showTemplates && (
<Button type="default" onClick={onViewTemplates}>
View templates
</Button>
)}
<Button type="primary" onClick={onReviewPolicy}>
Review
</Button>
)}
<Button type="primary" onClick={onReviewPolicy}>
Review
</Button>
</div>
</div>
)
@@ -26,7 +26,9 @@ const PolicyTableRow: FC<Props> = ({
onSelectDeletePolicy = () => {},
}) => {
const { meta } = useStore()
const policies = meta.policies.list((x: PostgresPolicy) => x.table === table.name)
const policies = meta.policies.list(
(policy: PostgresPolicy) => policy.schema === table.schema && policy.table === table.name
)
return (
<Panel
@@ -1,4 +1,4 @@
import { IconArchive, IconCode, IconDatabase, IconKey } from 'ui'
import { Badge, IconArchive, IconCode, IconDatabase, IconKey } from 'ui'
export const CANCELLATION_REASONS = [
'Pricing',
@@ -25,13 +25,15 @@ export const USAGE_BASED_PRODUCTS = [
{
key: 'db_size',
attribute: 'total_db_size_bytes',
title: 'Database space',
title: 'Database size',
units: 'bytes',
costPerUnit: 0.125,
tooltip: (
<span>
We continuously monitor the total size of your database. Billing is based on the average
database size in GB throughout the billing period.
Billing is based on the average daily database size in GB throughout the billing period.{' '}
<a href="https://supabase.com/docs/guides/platform/database-usage" target="_blank">
Docs
</a>
</span>
),
},
@@ -130,8 +132,9 @@ export const USAGE_BASED_PRODUCTS = [
costPerUnit: 0.1,
tooltip: (
<span>
Every single serverless function invocation independent of response status is counted.
Billing is based on the sum of all invocations throughout your billing period.
We continuously monitor the amount of serverless functions in your project. Billing is
based on the maximum amount of functions at any point in time throughout your billing
period.
</span>
),
},
@@ -144,9 +147,8 @@ export const USAGE_BASED_PRODUCTS = [
costPerUnit: 0.000002,
tooltip: (
<span>
We continuously monitor the amount of serverless functions in your project. Billing is
based on the maximum amount of functions at any point in time throughout your billing
period.
Every single serverless function invocation independent of response status is counted.
Billing is based on the sum of all invocations throughout your billing period.
</span>
),
},
@@ -394,7 +394,7 @@ const ProUpgrade: FC<Props> = ({
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Database space</p>
<p className="w-[50%] text-sm">Database size</p>
<p className="w-[25%] text-sm">8GB</p>
<p className="w-[25%] text-sm">$0.125/GB</p>
</div>
@@ -14,7 +14,7 @@ import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import NoPermission from 'components/ui/NoPermission'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
interface Props {
project: any
@@ -62,7 +62,7 @@ const Subscription: FC<Props> = ({
? 0
: Object.keys(usage)
.map((productKey) => {
return usage[productKey as keyof ProjectUsageResponse].cost ?? 0
return usage[productKey as ProjectUsageResponseUsageKeys].cost ?? 0
})
.reduce((prev, current) => prev + current, 0)
@@ -183,10 +183,10 @@ const Subscription: FC<Props> = ({
{isPayg &&
USAGE_BASED_PRODUCTS.map((product) => {
return product.features.map((feature) => {
const amount = usage?.[feature.key as keyof ProjectUsageResponse]?.usage ?? 0
const limit = usage?.[feature.key as keyof ProjectUsageResponse]?.limit ?? 0
const amount = usage?.[feature.key as ProjectUsageResponseUsageKeys]?.usage ?? 0
const limit = usage?.[feature.key as ProjectUsageResponseUsageKeys]?.limit ?? 0
const cost = (
usage?.[feature.key as keyof ProjectUsageResponse]?.cost ?? 0
usage?.[feature.key as ProjectUsageResponseUsageKeys]?.cost ?? 0
).toFixed(2)
return (
@@ -31,9 +31,9 @@ const Wrappers = () => {
const isWrappersEnabled =
wrappersExtension !== undefined &&
wrappersExtension?.installed_version !== null &&
wrappersExtension.installed_version !== null &&
vaultExtension !== undefined &&
vaultExtension?.installed_version !== null
vaultExtension.installed_version !== null
return (
<div>
@@ -5,7 +5,7 @@ import { IconLoader, IconAlertCircle } from 'ui'
import { useParams } from 'hooks'
import { ProjectUsage, NewProjectPanel } from 'components/interfaces/Home'
import InformationBox from 'components/ui/InformationBox'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
const ProjectUsageSection: FC = observer(({}) => {
const { ref: projectRef } = useParams()
@@ -24,7 +24,7 @@ const ProjectUsageSection: FC = observer(({}) => {
const hasProjectData = usage
? Object.keys(usage)
.map((key) => usage[key as keyof ProjectUsageResponse].usage)
.map((key) => usage[key as ProjectUsageResponseUsageKeys].usage)
.some((usage) => (usage ?? 0) > 0)
: false
@@ -122,7 +122,7 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain
>
Please add a CAA record allowing "digicert.com" to issue certificates for{' '}
<code className="text-xs">{customDomain.hostname}</code>. For example:{' '}
<code className="text-xs">0 issue "digitcert.com"</code>
<code className="text-xs">0 issue "digicert.com"</code>
</Alert>
)}
@@ -1,5 +1,5 @@
import { FC, useEffect } from 'react'
import { Badge, Button, IconAlertCircle, IconInfo, Loading } from 'ui'
import { Badge, Button, IconAlertCircle, IconInfo, Loading, IconExternalLink } from 'ui'
import { useStore } from 'hooks'
import { formatBytes } from 'lib/helpers'
@@ -8,7 +8,7 @@ import SparkBar from 'components/ui/SparkBar'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { useRouter } from 'next/router'
import * as Tooltip from '@radix-ui/react-tooltip'
@@ -60,6 +60,26 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
)
}
const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE
const featureFootnotes: Record<string, JSX.Element> = {
db_size: (
<div className="flex justify-between items-center">
<div className="flex flex-row space-x-4 text-scale-1000">
{usage?.disk_volume_size_gb && <span>Disk Size: {usage.disk_volume_size_gb} GB</span>}
{isPaidTier && <Badge>Auto-Scaling</Badge>}
</div>
<Button type="default" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
<a target="_blank" href="https://supabase.com/docs/guides/platform/database-usage">
What is disk size?
</a>
</Button>
</div>
),
}
return (
<Loading active={isLoading}>
{usage && (
@@ -69,7 +89,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
showUsageExceedMessage &&
product.features
.map((feature) => {
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
const featureUsage = usage[feature.key as ProjectUsageResponseUsageKeys]
return (featureUsage.usage ?? 0) / featureUsage.limit > 1
})
.some((x) => x === true)
@@ -116,7 +136,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
) : (
<tbody>
{product.features.map((feature) => {
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
const featureUsage = usage[feature.key as ProjectUsageResponseUsageKeys]
const usageValue = featureUsage.usage || 0
const usageRatio = usageValue / featureUsage.limit
const isApproaching = usageRatio >= USAGE_APPROACHING_THRESHOLD
@@ -171,7 +191,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
)
}
return (
return [
<tr
key={feature.title}
className="border-t border-panel-border-light dark:border-panel-border-dark"
@@ -217,8 +237,18 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
</td>
</>
)}
</tr>
)
</tr>,
featureFootnotes[feature.key] && (
<tr key={`${feature.title}-footnote`}>
<td
className="whitespace-nowrap px-6 py-3 text-sm text-scale-1200"
colSpan={3}
>
{featureFootnotes[feature.key]}
</td>
</tr>
),
]
})}
</tbody>
)}
@@ -1,7 +1,7 @@
import { FC } from 'react'
import { Loading } from 'ui'
import { ProjectUsageResponse, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
@@ -24,7 +24,7 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
if (
subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PAYG ||
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE ||
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
) {
return <></>
}
@@ -41,7 +41,7 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
{usage && (
<div className="space-y-8">
{product.features.map((feature) => {
const featureUsage = usage[feature.key as keyof ProjectUsageResponse]
const featureUsage = usage[feature.key as ProjectUsageResponseUsageKeys]
const usageRatio = (featureUsage.usage ?? 0) / featureUsage.limit
const isApproaching = usageRatio >= USAGE_APPROACHING_THRESHOLD
const isExceeded = usageRatio >= 1
@@ -375,7 +375,6 @@ const ColumnEditor: FC<Props> = ({
)}
<ForeignKeySelector
tables={tables}
column={columnFields}
visible={isEditingRelation}
closePanel={() => setIsEditingRelation(false)}
@@ -1,16 +1,16 @@
import React, { FC, useEffect, useState } from 'react'
import { get, find, isEmpty, sortBy } from 'lodash'
import { Dictionary } from 'components/grid'
import { SidePanel, Input, Listbox, IconHelpCircle } from 'ui'
import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase } from 'ui'
import type { PostgresTable, PostgresColumn, PostgresSchema } from '@supabase/postgres-meta'
import { useStore } from 'hooks'
import ActionBar from '../ActionBar'
import { ForeignKey } from './ForeignKeySelector.types'
import { ColumnField } from '../SidePanelEditor.types'
import InformationBox from 'components/ui/InformationBox'
interface Props {
tables: PostgresTable[]
column: ColumnField
metadata?: any
visible: boolean
@@ -18,15 +18,19 @@ interface Props {
saveChanges: (value: { table: PostgresTable; column: PostgresColumn } | undefined) => void
}
const ForeignKeySelector: FC<Props> = ({
tables = [] as PostgresTable[],
column,
visible = false,
closePanel,
saveChanges,
}) => {
const ForeignKeySelector: FC<Props> = ({ column, visible = false, closePanel, saveChanges }) => {
const { meta } = useStore()
const [errors, setErrors] = useState<any>({})
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>()
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>({
schema: 'public',
table: '',
column: '',
})
const schemas = meta.schemas.list()
const tables = meta.tables.list(
(table: PostgresTable) => table.schema === selectedForeignKey.schema
)
const foreignKey = column?.foreignKey
const selectedTable: PostgresTable | undefined = find(tables, {
@@ -41,6 +45,7 @@ const ForeignKeySelector: FC<Props> = ({
// Reset the state of the side panel
if (visible) {
setErrors({})
if (foreignKey) {
setSelectedForeignKey({
schema: foreignKey.target_table_schema,
@@ -49,7 +54,7 @@ const ForeignKeySelector: FC<Props> = ({
})
} else {
setSelectedForeignKey({
schema: '',
schema: 'public',
table: '',
column: '',
})
@@ -57,6 +62,12 @@ const ForeignKeySelector: FC<Props> = ({
}
}, [visible])
const updateSelectedSchema = (schema: string) => {
meta.tables.loadBySchema(schema)
const updatedForeignKey = { ...selectedForeignKey, schema, table: '', column: '' } as ForeignKey
setSelectedForeignKey(updatedForeignKey)
}
const updateSelectedTable = (tableId: number) => {
setErrors({})
if (!tableId) {
@@ -87,6 +98,9 @@ const ForeignKeySelector: FC<Props> = ({
const onSaveChanges = (resolve: () => void) => {
const errors = {} as Dictionary<any>
if (!selectedForeignKey?.table) {
errors['table'] = 'Please select a table'
}
if (selectedForeignKey?.table && !selectedForeignKey.column) {
errors['column'] = `The table ${selectedForeignKey.table} has no columns`
}
@@ -141,12 +155,38 @@ const ForeignKeySelector: FC<Props> = ({
/>
<Listbox
id="schema"
label="Select a schema"
value={selectedForeignKey.schema}
error={errors.schema}
onChange={(value: string) => updateSelectedSchema(value)}
>
{schemas.map((schema: PostgresSchema) => {
return (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
>
<div className="flex items-center gap-2">
{/* For aria searching to target the schema name instead of schema */}
<span className="hidden">{schema.name}</span>
<span className="text-scale-1200">{schema.name}</span>
</div>
</Listbox.Option>
)
})}
</Listbox>
<Listbox
id="table"
label="Select a table to reference to"
value={selectedTable?.id}
value={selectedTable?.id ?? 1}
error={errors.table}
onChange={(value: string) => updateSelectedTable(Number(value))}
>
<Listbox.Option key="empty" value="" label="---">
<Listbox.Option key="empty" value={1} label="---">
---
</Listbox.Option>
{/* @ts-ignore */}
@@ -179,6 +219,7 @@ const ForeignKeySelector: FC<Props> = ({
/>
) : (
<Listbox
id="column"
value={selectedColumn?.id}
// @ts-ignore
label={
@@ -353,7 +353,6 @@ const ColumnManagement: FC<Props> = ({
</div>
</div>
<ForeignKeySelector
tables={tables}
column={selectedColumnToEditRelation as ColumnField}
visible={!isUndefined(selectedColumnToEditRelation)}
closePanel={() => setSelectedColumnToEditRelation(undefined)}
@@ -22,6 +22,9 @@ const DatabaseLayout: FC<Props> = ({ title, children }) => {
const router = useRouter()
const page = router.pathname.split('/')[4]
const vaultExtension = meta.extensions.byId('supabase_vault')
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
const [loaded, setLoaded] = useState<boolean>(isInitialized)
useEffect(() => {
@@ -38,10 +41,15 @@ const DatabaseLayout: FC<Props> = ({ title, children }) => {
if (IS_PLATFORM) {
backups.load()
}
vault.load()
}
}, [ui.selectedProject?.ref])
useEffect(() => {
if (isVaultEnabled) {
vault.load()
}
}, [ui.selectedProject?.ref, isVaultEnabled])
// Optimization required: load logic should be at the page level
// e.g backups page is waiting for meta.tables to load finish when it doesnt even need that data
useEffect(() => {
@@ -37,7 +37,7 @@ const TableEditorLayout: FC<Props> = ({
const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
const vaultExtension = meta.extensions.byId('supabase_vault')
const isVaultEnabled = vaultExtension !== undefined && vaultExtension?.installed_version !== null
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
if (ui.selectedProject?.ref) {
@@ -43,7 +43,7 @@ const TableEditorMenu: FC<Props> = ({
onDeleteTable = () => {},
onDuplicateTable = () => {},
}) => {
const { meta, ui } = useStore()
const { meta } = useStore()
const { id, ref } = useParams()
const schemas: PostgresSchema[] = meta.schemas.list()
+3
View File
@@ -51,8 +51,11 @@ export type ProjectUsageResponse = {
storage_size: StorageSize
storage_egress: StorageEgress
monthly_active_users: MonthlyActiveUsers
disk_volume_size_gb: number
}
export type ProjectUsageResponseUsageKeys = keyof Omit<ProjectUsageResponse, 'disk_volume_size_gb'>
export async function getProjectUsage({ projectRef }: ProjectUsageVariables, signal?: AbortSignal) {
if (!projectRef) {
throw new Error('projectRef is required')
+1 -1
View File
@@ -39,7 +39,7 @@
"clipboard": "^2.0.8",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+7 -7
View File
@@ -138,14 +138,14 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
icon={<IconSearch size="tiny" />}
/>
</div>
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
<a
target="_blank"
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
>
<a
target="_blank"
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
>
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
What is RLS?
</a>
</Button>
</Button>
</a>
</div>
</div>
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
@@ -15,7 +15,7 @@ const VaultSettingsSecrets: NextPageWithLayout = () => {
const { ref } = useParams()
const vaultExtension = meta.extensions.byId('supabase_vault')
const isEnabled = vaultExtension !== undefined && vaultExtension?.installed_version !== null
const isEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
if (isEnabled) {
@@ -17,7 +17,7 @@ const VaultSettingsSecrets: NextPageWithLayout = () => {
const vaultExtension = meta.extensions.byId('supabase_vault')
const isLoading = meta.extensions.isLoading
const isEnabled = vaultExtension !== undefined && vaultExtension?.installed_version !== null
const isEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
if (isEnabled) {