Merge pull request #13118 from supabase/master

Prod deploy Studio
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-03-21 13:50:19 +08:00
commit 925a204bb3
135 files changed
+4502 -39450

No files matched your search

+1 -1
View File
@@ -1 +1 @@
apps/reference/static/.well-known/security.txt
apps/docs/public/.well-known/security.txt
@@ -5,26 +5,26 @@ from `public.profiles` and the older versions getting orphaned.
To automatically remove obsolete storage objects, extend the database
triggers. Note that it is not sufficient to delete the objects from the
`storage.objects` table because that would orphan and leak the actual storage objects in
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
Enable the [http extension for the `extensions` schema](https://app.supabase.com/project/_/database/extensions) in the Dashboard.
Then, define the following SQL functions in the SQL Editor to delete
storage objects via the API:
```SQL
create or replace function delete_storage_object(bucket text, object text, out status int, out content varchar)
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
returns record
language 'plpgsql'
security definer
as $$
declare
project_url varchar := '<YOURPROJECTURL>';
service_role_key varchar := '<YOURSERVICEROLEKEY>'; -- full access needed
url varchar := project_url||'/storage/v1/object/'||bucket||'/'||object;
project_url text := '<YOURPROJECTURL>';
service_role_key text := '<YOURSERVICEROLEKEY>'; -- full access needed
url text := project_url||'/storage/v1/object/'||bucket||'/'||object;
begin
select
into status, content
result.status::int, result.content::varchar
result.status::int, result.content::text
FROM extensions.http((
'DELETE',
url,
@@ -34,7 +34,7 @@ begin
end;
$$;
create or replace function delete_avatar(avatar_url text, out status int, out content varchar)
create or replace function delete_avatar(avatar_url text, out status int, out content text)
returns record
language 'plpgsql'
security definer
@@ -60,7 +60,7 @@ security definer
as $$
declare
status int;
content varchar;
content text;
begin
if coalesce(old.avatar_url, '') <> ''
and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then
@@ -685,6 +685,11 @@ export const platform = {
{ name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement', items: [] },
],
},
{
name: 'Billing',
url: undefined,
items: [{ name: 'Spend cap', url: '/guides/platform/spend-cap', items: [] }],
},
{
name: 'Single sign-on',
url: undefined,
+2
View File
@@ -1,5 +1,6 @@
import { Button, Tabs, Alert, GlassPanel } from 'ui'
import Link from 'next/link'
import StepHikeCompact from '~/components/StepHikeCompact'
// Common components
import Admonition from './Admonition'
@@ -50,6 +51,7 @@ const components = {
ProjectSetup,
SocialProviderSetup,
SocialProviderSettingsSupabase,
StepHikeCompact,
StorageManagement,
Mermaid,
Extensions,
@@ -1,3 +1,4 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
@@ -30,51 +31,78 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => {
}
})
return (
<RefSubLayout>
{props.sections.map((x, i) => {
switch (x.type) {
case 'markdown':
const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id)
function getPageTitle() {
switch (props.type) {
case 'client-lib':
return props.spec.info.title
case 'cli':
return 'Supabase CLI reference'
case 'api':
return 'Supabase API reference'
default:
return 'Supabase Docs'
}
}
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}
typeSpec={props.typeSpec}
/>
)
case 'cli-command':
return <CliCommandSection key={x.id + i} funcData={x} commonFuncData={x} />
break
case 'operation':
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}
typeSpec={props.typeSpec}
/>
)
break
}
})}
</RefSubLayout>
const pageTitle = getPageTitle()
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageTitle} />
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
<meta
name="twitter:image"
content={`https://supabase.com/docs/img/supabase-og-image.png`}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<RefSubLayout>
{props.sections.map((x, i) => {
switch (x.type) {
case 'markdown':
const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id)
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}
typeSpec={props.typeSpec}
/>
)
case 'cli-command':
return <CliCommandSection key={x.id + i} funcData={x} commonFuncData={x} />
break
case 'operation':
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}
typeSpec={props.typeSpec}
/>
)
break
}
})}
</RefSubLayout>
</>
)
}
+31 -25
View File
@@ -7,6 +7,7 @@ import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef'
import { memo, useEffect } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
import Head from 'next/head'
const levelsData = {
home: {
@@ -312,32 +313,37 @@ const SiteLayout = ({ children }) => {
}, [])
return (
<main>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
<TopNavBarRef />
</div>
<div
className={[
'sticky transition-all top-0',
'z-10',
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
].join(' ')}
>
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
<MobileHeader />
<>
<Head>
<title>Supabase Docs</title>
</Head>
<main>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
<TopNavBarRef />
</div>
</div>
<div className="grow px-5 max-w-7xl mx-auto py-16">
{children}
<Footer />
</div>
<MobileMenuBackdrop />
</Container>
</div>
</main>
<div
className={[
'sticky transition-all top-0',
'z-10',
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
].join(' ')}
>
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
<MobileHeader />
</div>
</div>
<div className="grow px-5 max-w-7xl mx-auto py-16">
{children}
<Footer />
</div>
<MobileMenuBackdrop />
</Container>
</div>
</main>
</>
)
}
+1 -1
View File
@@ -67,7 +67,7 @@ const Layout: FC<Props> = (props) => {
// page type, ie, Auth, Database, Storage etc
const ogPageType = asPath.split('/')[2]
// open graph image url constructor
const ogImageUrl = encodeURIComponent(
const ogImageUrl = encodeURI(
`https://obuldanrptloktxcffvn.functions.supabase.co/og-images?site=docs${
ogPageType ? `&type=${ogPageType}` : ''
}&title=${props.meta?.title}&description=${props.meta?.description}`
@@ -37,7 +37,6 @@ const RefSubLayoutNonFunc: FC<RefSubLayoutNonFuncType> & RefSubLayoutNonFuncSubC
}
const Section: FC<ISectionContainer> = (props) => {
// console.log({ props })
return (
<article
key={props.id}
@@ -158,7 +158,7 @@ import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import type { Database } from '../lib/database.types'
import type { Database } from './lib/database.types'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
@@ -185,7 +185,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
```jsx lines=10-17
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
@@ -91,13 +91,12 @@ export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
};
```
#### Shared Load functions and pages
### Shared Load functions and pages
To be able to use Supabase in shared load functions and inside pages you need to create a Supabase client in the root layout load:
```ts
// src/routes/+layout.ts
import { invalidate } from '$app/navigation';
import {
PUBLIC_SUPABASE_ANON_KEY,
PUBLIC_SUPABASE_URL
@@ -113,10 +112,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
onAuthStateChange() {
invalidate('supabase:auth');
}
serverSession: data.session
});
const {
@@ -129,10 +125,41 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data: PageData`.
The usage of `depends` and `invalidate` tells sveltekit that this load function should be executed whenever the session updates to keep the page store in sync.
The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync.
`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs.
### Setting up the event listener on the client side
We need to create an event listener in the root `+layout.svelte` file in order catch supabase events being triggered.
```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
$: ({ supabase } = data);
onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
});
return () => subscription.unsubscribe();
});
</script>
<slot />
```
The usage of `invalidate` tells sveltekit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync.
### Generate types from your database
In order to get the most out of TypeScript and it's intellisense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](https://supabase.com/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI.
@@ -183,7 +210,7 @@ You can now determine if a user is authenticated on the client-side by checking
For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side:
```html
<script>
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
@@ -272,7 +299,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
```ts
// src/routes/posts/+page.server.ts
import type { Actions } from './$types';
import { error, invalid } from '@sveltejs/kit';
import { error, fail } from '@sveltejs/kit';
export const actions: Actions = {
createPost: async ({ request, locals: { supabase, getSession } }) => {
@@ -291,7 +318,7 @@ export const actions: Actions = {
.insert({ content });
if (createPostError) {
return invalid(500, {
return fail(500, {
supabaseErrorMessage: createPostError.message
});
}
@@ -308,7 +335,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 { AuthApiError } from '@supabase/supabase-js';
export const actions: Actions = {
@@ -325,14 +352,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
@@ -461,7 +488,7 @@ export const handle: Handle = async ({ event, resolve }) => {
### Initialize the client [#migration-initialize-client]
In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` file.
In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` and create a `+layout.svelte` to handle our event listening for Auth events.
<Tabs
scrollable
@@ -473,7 +500,7 @@ In order to use the Supabase library in your client code you will need to setup
```html title=src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script>
<script lang="ts">
import { supabaseClient } from '$lib/db'
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
@@ -515,10 +542,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
onAuthStateChange() {
invalidate('supabase:auth');
}
serverSession: data.session
});
const {
@@ -529,6 +553,31 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
};
```
```svelte title=src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
$: ({ supabase } = data);
onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
});
return () => subscription.unsubscribe();
});
</script>
<slot />
```
</TabPanel>
</Tabs>
@@ -606,7 +655,7 @@ declare global {
```html title=src/routes/profile/+page.svelte
<!-- src/routes/profile/+page.svelte -->
<script>
<script lang="ts">
/** @type {import('./$types').PageData} */
export let data
$: ({ user, tableData } = data)
@@ -328,6 +328,9 @@ Tip: Make sure to enable RLS for all your tables, so that your tables are inacce
Supabase provides special "Service" keys, which can be used to bypass all RLS.
These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.
<Admonition type="caution">
Initializing a client with a Service Key will not override RLS if a user token is set. If a user is signed in with a client, Supabase will adhere to the RLS policy of the user.
</Admonition>
### Testing policies
@@ -160,7 +160,7 @@ curl -X POST \
-H 'apikey: <anon-jwt>' \
-H 'Authorization: Bearer <service_role-jwt>' \
--data-binary '@/tmp/body.json' \
'https://<project>.supabase.co/auth/v1/admin/sso/providers/<provider-uuid>'
'https://<project>.supabase.co/auth/v1/admin/sso/providers'
```
To create the `/tmp/body.json` file you can use this:
+222 -67
View File
@@ -7,7 +7,23 @@ export const meta = {
video: 'https://www.youtube.com/v/vyHyYpvjaks',
}
Learn how to use the Supabase CLI to develop your project locally and deploy to the Supabase Platform.
Supabase is a flexible platform that lets you decide how you want to build your projects. You can use the Dashboard directly to get up and running quickly, or use a proper local setup. We suggest you work locally and deploy your changes to a linked project on the [Supabase Platform](https://app.supabase.io/).
Doing things directly on the platform via the [Dashboard](https://app.supabase.io/) is fine when you're getting started, but it's a good idea to move to a proper local workflow before you get too far. Working locally, generating migrations as you change your tables, and appling those migrations to a linked project on the [Platform](https://app.supabase.io/) keeps everything nicely organized as you grow.
## Why develop locally?
The Dashboard provides a wide range of features for setting up your project: creating tables, adding columns, changing existing columns, creating views, setting up RLS policies, and more. Given all of the Dashboard's capabilities, you might question the need to work locally. Here's a few advantages to working this way:
1. **Faster Development**: Developing locally allows you to work without any network latency or internet disruptions.
2. **Easier Collaboration**: Developing locally can make it easier to collaborate with others on the same project.
3. **Cost-Effective**: Supabase provides a generous free tier and gives you two free projects to get started. But what if you need more than two? When you develop locally, you can spin up unlimited local projects and link them with live projects when you're ready to launch.
4. **Configuration in code**: If you directly change your tables via the Dashboard, none of that gets captured in code. If you follow these local development practices, you'll store all of your table schemas in code.
5. **Work offline**: Need to work from a train? A plain? An automobile? No problem. Developing your project locally allows you to work offline.
<div className="video-container">
<iframe
@@ -49,7 +65,7 @@ mkdir your-project
# move into the new folder
cd your-project
# start a new git repository
# start a new git repository — important, don't skip this step
git init
```
@@ -68,12 +84,26 @@ This command may take a while to run if this is the first time using the CLI.
supabase start
```
Once all of the Supabase services are running, you'll see output containing your local Supabase credentials.
You can use the [stop](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services.
Once all of the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like this, with urls and keys that you'll use in your local project:
## Access services
```
You can access services directly with any Postgres client or through the API Gateway ([Kong](https://github.com/Kong/kong)).
Started supabase local development setup.
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324
anon key: eyJh......
service_role key: eyJh......
```
You can use the [supabase stop](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services.
## Access your project's services
You can now visit your local Dashboard at [http://localhost:54323](http://localhost:54323), and access the database directly with any Postgres client via `postgresql://postgres:postgres@localhost:54322/postgres`.
<Tabs
scrollable
@@ -147,95 +177,218 @@ Database changes are managed through "migrations." Database migrations are a com
></iframe>
</div>
### Make database changes
For this guide, we'll create a table called `employees` and see how we can make changes to it.
For this guide, create a table called `employees`. In Supabase Studio, navigate to the **SQL Editor** page and run the following SQL command:
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create your first migration file">
To get started, generate a [new migration](https://supabase.com/docs/reference/cli/supabase-migration-new) to store the SQL needed to create our `employees` table
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash
supabase migration new create_employees_table
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Add the SQL to your migration file">
This creates a new migration: supabase/migrations/\<timestamp\>
_create_employees_table.sql.
To that file, add the SQL to create this `employees` table
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
```sql
create table
employees (
id integer primary key generated always as identity,
name text
);
```
employees (
id bigint primary key generated always as identity,
name text,
email text,
created_at timestampz default now()
);
<Admonition type="note">
````
You can execute any SQL using the `DB URL` shown by [`supabase status`](/docs/reference/cli/usage#supabase-status).
</StepHikeCompact.Code>
</Admonition>
</StepHikeCompact.Step>
</StepHikeCompact>
Run the [`db diff`](/docs/reference/cli/usage#supabase-db-diff) command to detect changes in the local database:
```sh
supabase db diff create_employees -f create_employees
```
This creates a new migration named `supabase/migrations/<timestamp>_create_employees.sql`, representing any changes made to the local database since [`supabase start`](/docs/reference/cli/usage#supabase-start).
<StepHikeCompact>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Apply your migration">
Now that you have a migration file, you can run this migration and create the `employees` table.
Use the `reset` command here to reset the database to the current migrations
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash
supabase db reset
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Modify your employees table">
Now you can visit your new `employees` table in the Dashboard.
Next, modify your `employees` table by adding a column for department. Create a new migration file for that.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash
supabase migration new add_department_to_employees_table
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Add a new column to your table">
This creates a new migration file: supabase/migrations/\<timestamp\>
_add_department_to_employees_table.sql.
To that file, add the SQL to create a new department column
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
alter table
if exists public.employees add department text default 'Hooli';
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
### Add sample data
Use the seed script in `supabase/seed.sql` (created with [`supabase init`](/docs/reference/cli/usage#supabase-init)) to add sample data to the table.
Now that you are managing your database with migrations scripts, it would be great have some seed data to use every time you reset the database.
For this, you can use the seed script in `supabase/seed.sql`. This file was automatically created when you ran [`supabase init`](/docs/reference/cli/usage#supabase-init)) at the beginning.
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Populate your table">
Insert data into your `employees` table with your `supabase/seed.sql` file.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
```sql
-- in supabase/seed.sql
insert into public.employees
(name)
insert into
public.employees (name)
values
('Erlich Bachman'),
('Richard Hendricks'),
('Monica Hall');
```
('Erlich Bachman'),
('Richard Hendricks'),
('Monica Hall');
Rerun the migration and seed scripts:
````
</StepHikeCompact.Code>
```bash
</StepHikeCompact.Step>
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Reset your database">
Reset your database (apply current migrations), and populate with seed data
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash
supabase db reset
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
You should now see the `employees` table, along with your seed data in the Dashboard! All of your database changes are captured in code, and you can reset to a known state at any time, complete with seed data.
### Diffing changes
This workflow is great if you know SQL and are comfortable creating tables and columns. If not, you can still use the Dashboard to create tables and columns, and then use the CLI to diff your changes and create migrations.
Create a new table called `cities`, with columns `id`, `name` and `population`. To see the corresponding SQL for this, you can use the `supabase db diff --schema public` command. This will show you the SQL that will be run to create the table and columns. The output of `supabase db diff` will look something like this:
```
Diffing schemas: public
Finished supabase db diff on branch main.
create table "public"."cities" (
"id" bigint primary key generated always as identity,
"name" text,
"population" bigint
);
```
You should now see the contents of `employees` in Studio.
You can then copy this SQL into a new migration file, and run `supabase db reset` to apply the changes.
### Reset database changes
Use the [`reset`](/docs/reference/cli/usage#supabase-db-reset) command to revert any changes to the local database.
```sql
-- run on local database to make a change
alter table employees
add department text default 'Hooli';
```
Run the following command to reset the local database:
```sh
supabase db reset
```
The last step is deploying these changes to a live Supabase project.
## Deploy your project
Go to the [Supabase Dashboard](https://app.supabase.com) and create a project to deploy the changes.
You've been developing your project locally, making changes to your tables via migrations. It's time to deploy your project to the Supabase Platform and start scaling up to millions of users! Head over to [Supabase](https://app.supabase.com) and create a new project to deploy to.
### Link your project
Associate your project with your remote project using [`supabase link`](/docs/reference/cli/usage#supabase-link).
```bash
supabase link --project-ref <project-id>
# You can get <project-id> from your project's dashboard URL: https://app.supabase.com/project/<project-id>
supabase db remote commit
# Capture any changes that you have made to your remote database before you went through the steps above
# If you have not made any changes to the remote database, skip this step
```
`supabase/migrations` is now populated with a migration in `..._remote_commit.sql`.
This migration captures any changes required for your local database to match the schema of your remote Supabase project.
<Admonition type="note">
There are a few commands required to link your project. We are in the process of consolidating these commands into a single command. Bear with us!
</Admonition>
Associate your project with your remote project using [`supabase link`](/docs/reference/cli/usage#supabase-link).
```bash
supabase link --project-ref <project-id>
# You can get <project-id> from your project’s dashboard URL: https://app.supabase.com/project/<project-id>
supabase db remote commit
# Capture any changes that you have made to your database before setting up the CLI
```
`supabase/migrations` is now populated with a migration in `..._remote_commit.sql`.
This migration captures any changes required for your local database to match the schema of your remote Supabase project.
### Deploy database changes
Deploy any local database migrations using [`db push`](/docs/reference/cli/usage#supabase-db-push):
@@ -244,21 +397,23 @@ Deploy any local database migrations using [`db push`](/docs/reference/cli/usage
supabase db push
```
Visiting your live project on [Supabase](https://app.supabase.com), you'll see a new `employees` table, complete with the `department` column you added in the second migration above.
### Deploy Edge Functions
Deploy any Edge Functions using [`functions deploy`](/docs/reference/cli/usage#supabase-functions-deploy):
If your project uses Edge Functions, you can deploy these using [`functions deploy`](/docs/reference/cli/usage#supabase-functions-deploy):
```sh
supabase functions deploy <function_name>
```
## Limitations
## Limitations and considerations
The local development environment is not as feature-complete as the Supabase Platform. Here are some of the differences:
The local development environment is not as feature-complete as the Supabase Platform. We're working towards parity between the hosted platform and the local environment. Here are some of the differences:
- 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.
- Logs are not supported through the interface, but you can access them through the Docker containers.
- You cannot update your project settings in the Dashboard. This must be done using the CLI.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -54,8 +54,8 @@ Given a table
```sql
create table "Blog"(
id serial primary key,
name varchar(255) not null,
description varchar(255),
name text not null,
description text,
);
insert into "Blog"(name)
@@ -22,8 +22,8 @@ Supabase Edge Functions allow you to go beyond HTTP and can connect to your Post
import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts'
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get('DATABASE_URL')!
// Get the connection string from the environment variable "SUPABASE_DB_URL"
const databaseUrl = Deno.env.get('SUPABASE_DB_URL')!
// Create a database pool with three connections that are lazily established
const pool = new postgres.Pool(databaseUrl, 3, true)
@@ -50,7 +50,7 @@ This will register a Slash Command named `hello` that accepts a parameter named
import { json, serve, validateRequest } from 'sift'
// TweetNaCl is a cryptography library that we use to verify requests
// from Discord.
import nacl from 'nacl'
import nacl from 'tweetnacl'
enum DiscordCommandType {
Ping = 1,
@@ -128,8 +128,8 @@ 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 read or write to File System
- NPM modules are not supported.
- 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`).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -55,7 +55,7 @@ We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extens
- Official Docs: [postgrest.org](https://postgrest.org/)
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE)
- Language: Haskel
- Language: Haskell
### Realtime (API & multiplayer)
@@ -24,8 +24,8 @@ export const meta = {
```sql SQL_EDITOR
-- Create the table
CREATE TABLE countries (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
-- Insert some sample data into the table
INSERT INTO countries (name) VALUES ('United States');
@@ -41,14 +41,14 @@ export const meta = {
<StepHikeCompact.Details title="Create a React app">
Create a React app using the `create-react-app` command.
Create a React app using a [Vite](https://vitejs.dev/guide/) template.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
npx create-react-app my-app
npm create vite@latest my-app -- --template react
```
</StepHikeCompact.Code>
@@ -77,25 +77,42 @@ export const meta = {
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Query data from the app">
In `index.js`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
In `App.jsx`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
Add a `getCountries` function to fetch the data and log the query result to the browser console.
Add a `getCountries` function to fetch the data and display the query result to the page.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```js src/index.js
import { createClient } from '@supabase/supabase-js'
```js src/App.jsx
import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
const supabase = createClient("https://<project>.supabase.co", "<your-anon-key>");
async function getCountries() {
const countries = await supabase.from('countries').select()
console.log(countries)
function App() {
const [countries, setCountries] = useState([]);
useEffect(() => {
getCountries();
}, []);
async function getCountries() {
const { data } = await supabase.from("countries").select();
setCountries(data);
}
return (
<ul>
{countries.map((country) => (
<li key={country.name}>{country.name}</li>
))}
</ul>
);
}
getCountries()
export default App;
```
</StepHikeCompact.Code>
@@ -105,14 +122,14 @@ export const meta = {
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Start the app">
Start the app, go to http://localhost:3000 in a browser, and open the browser console and you should see the list of countries.
Start the app, go to http://localhost:5173 in a browser, and open the browser console and you should see the list of countries.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
npm start
npm run dev
```
</StepHikeCompact.Code>
@@ -22,11 +22,11 @@ Let's start building the React app from scratch.
### Initialize a React app
We can use [Create React App](https://create-react-app.dev/docs/getting-started/) to initialize
We can use [Vite](https://vitejs.dev/guide/) to initialize
an app called `supabase-react`:
```bash
npx create-react-app supabase-react
npm create vite@latest supabase-react -- --template react
cd supabase-react
```
@@ -40,8 +40,8 @@ And finally we want to save the environment variables in a `.env.local` file.
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
```bash title=.env
REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL
REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_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
@@ -52,8 +52,8 @@ Create and edit `src/supabaseClient.js`:
```js title=src/supabaseClient.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```
@@ -65,9 +65,9 @@ You can find the full contents of this file [here](https://raw.githubusercontent
Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
Create and edit `src/Auth.js`:
Create and edit `src/Auth.jsx`:
```jsx title=src/Auth.js
```jsx title=src/Auth.jsx
import { useState } from 'react'
import { supabase } from './supabaseClient'
@@ -75,44 +75,42 @@ export default function Auth() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const handleLogin = async (e) => {
e.preventDefault()
const handleLogin = async (event) => {
event.preventDefault()
try {
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) {
alert(error.error_description || error.message)
} finally {
setLoading(false)
} else {
alert('Check your email for the login link!')
}
setLoading(false)
}
return (
<div className="row flex-center flex">
<div className="col-6 form-widget" aria-live="polite">
<div className="row flex flex-center">
<div className="col-6 form-widget">
<h1 className="header">Supabase + React</h1>
<p className="description">Sign in via magic link with your email below</p>
{loading ? (
'Sending magic link...'
) : (
<form onSubmit={handleLogin}>
<label htmlFor="email">Email</label>
<form className="form-widget" onSubmit={handleLogin}>
<div>
<input
id="email"
className="inputField"
type="email"
placeholder="Your email"
value={email}
required={true}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="button block" aria-live="polite">
Send magic link
</div>
<div>
<button className={'button block'} disabled={loading}>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</form>
)}
</div>
</form>
</div>
</div>
)
@@ -123,130 +121,119 @@ export default function Auth() {
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `src/Account.js`.
Let's create a new component for that called `src/Account.jsx`.
```jsx title=src/Account.js
```jsx title=src/Account.jsx
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
const Account = ({ session }) => {
export default function Account({ session }) {
const [loading, setLoading] = useState(true)
const [username, setUsername] = useState(null)
const [website, setWebsite] = useState(null)
const [avatar_url, setAvatarUrl] = useState(null)
useEffect(() => {
getProfile()
}, [session])
const getProfile = async () => {
try {
async function getProfile() {
setLoading(true)
const { user } = session
let { data, error, status } = await supabase
let { data, error } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error && status !== 406) {
throw error
}
if (data) {
if (error) {
console.warn(error)
} else if (data) {
setUsername(data.username)
setWebsite(data.website)
setAvatarUrl(data.avatar_url)
}
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
}
}
const updateProfile = async (e) => {
e.preventDefault()
getProfile()
}, [session])
try {
setLoading(true)
const { user } = session
async function updateProfile(event) {
event.preventDefault()
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
}
setLoading(true)
const { user } = session
let { error } = await supabase.from('profiles').upsert(updates)
if (error) {
throw error
}
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) {
alert(error.message)
}
setLoading(false)
}
return (
<div aria-live="polite">
{loading ? (
'Saving ...'
) : (
<form onSubmit={updateProfile} className="form-widget">
<div>Email: {session.user.email}</div>
<div>
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="url"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button className="button primary block" disabled={loading}>
Update profile
</button>
</div>
</form>
)}
<button type="button" className="button block" onClick={() => supabase.auth.signOut()}>
Sign Out
</button>
</div>
<form onSubmit={updateProfile} className="form-widget">
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
required
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="website"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button className="button block primary" type="submit" disabled={loading}>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<button className="button block" type="button" onClick={() => supabase.auth.signOut()}>
Sign Out
</button>
</div>
</form>
)
}
export default Account
```
### Launch!
Now that we have all the components in place, let's update `src/App.js`:
Now that we have all the components in place, let's update `src/App.jsx`:
```jsx title=src/App.js
import './index.css'
```jsx title=src/App.jsx
import './App.css'
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
import Auth from './Auth'
import Account from './Account'
export default function App() {
function App() {
const [session, setSession] = useState(null)
useEffect(() => {
@@ -265,15 +252,17 @@ export default function App() {
</div>
)
}
export default App
```
Once that's done, run this in a terminal window:
```bash
npm start
npm run dev
```
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app.
![Supabase React](/docs/img/supabase-react-demo.png)
@@ -285,9 +274,9 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
Create and edit `src/Avatar.js`:
Create and edit `src/Avatar.jsx`:
```jsx title=src/Avatar.js
```jsx title=src/Avatar.jsx
import { useEffect, useState } from 'react'
import { supabase } from './supabaseClient'
@@ -299,7 +288,7 @@ export default function Avatar({ url, size, onUpload }) {
if (url) downloadImage(url)
}, [url])
const downloadImage = async (path) => {
async function downloadImage(path) {
try {
const { data, error } = await supabase.storage.from('avatars').download(path)
if (error) {
@@ -312,7 +301,7 @@ export default function Avatar({ url, size, onUpload }) {
}
}
const uploadAvatar = async (event) => {
async function uploadAvatar(event) {
try {
setUploading(true)
@@ -331,7 +320,7 @@ export default function Avatar({ url, size, onUpload }) {
throw uploadError
}
onUpload(filePath)
onUpload(event, filePath)
} catch (error) {
alert(error.message)
} finally {
@@ -340,31 +329,33 @@ export default function Avatar({ url, size, onUpload }) {
}
return (
<div style={{ width: size }} aria-live="polite">
<img
src={avatarUrl ? avatarUrl : `https://place-hold.it/${size}x${size}`}
alt={avatarUrl ? 'Avatar' : 'No image'}
className="avatar image"
style={{ height: size, width: size }}
/>
{uploading ? (
'Uploading...'
<div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="avatar image"
style={{ height: size, width: size }}
/>
) : (
<>
<label className="button primary block" htmlFor="single">
Upload an avatar
</label>
<div className="visually-hidden">
<input
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</>
<div className="avatar no-image" style={{ height: size, width: size }} />
)}
<div style={{ width: size }}>
<label className="button primary block" htmlFor="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
)
}
@@ -372,9 +363,9 @@ export default function Avatar({ url, size, onUpload }) {
### Add the new widget
And then we can add the widget to the Account page at `src/Account.js`:
And then we can add the widget to the Account page at `src/Account.jsx`:
```jsx title=src/Account.js
```jsx title=src/Account.jsx
// Import the new component
import Avatar from './Avatar'
@@ -386,9 +377,9 @@ return (
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
onUpload={(event, url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
updateProfile(event)
}}
/>
{/* ... */}
@@ -175,9 +175,9 @@ Update your `src/routes/+layout.svelte`:
import '../styles.css';
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import type { PageData } from './$types'
import type { LayoutData } from './$types'
export let data: PageData
export let data: LayoutData
$: ({ supabase } = data)
@@ -560,7 +560,7 @@ And then we can add the widget to the Account page:
```svelte title=src/routes/account/+page.svelte
<!-- src/routes/account/+page.svelte -->
<script>
<script lang="ts">
// Import the new component
import Avatar from './Avatar.svelte'
</script>
@@ -43,8 +43,8 @@ Remove the default columns
Create two new columns:
- `user_id` as `varchar` as primary key
- `email` as `varchar`
- `user_id` as `text` as primary key
- `email` as `text`
Click `Save` to create the new table.
@@ -20,11 +20,11 @@ If you haven't already, create a new project in Supabase and create a table call
- id (int8)
- created_at (timestamptz)
- make (varchar)
- model (varchar)
- make (text)
- model (text)
- year (int8)
- rating (int2)
- author (varchar)
- author (text)
Manually enter a couple of rows of data, so that we have something to read from the DB.
@@ -0,0 +1,55 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Spend cap',
description: 'Learn about the cost control of Supabase.',
}
Supabase offers a "Spend Cap" on each project to manage your usage and costs.
This Spend Cap determines whether your project can exceed the free quota allowance of any [line item](#what-can-i-be-charged-for) in a given billing cycle.
If you exceed the free quota allowance of a line item, and the Spend Cap is **"off"**, then you will be billed for any additional usage on that line item.
## What additional usage can I be charged for?
Supabase charges for certain usage line items in your project. Those line items include:
- Database Size
- Storage Size
- Egress
- Asset transformations
- Realtime concurrent peak connections
- Realtime messages
- Edge function invocations
- Number of Edge functions
You can view the pricing, and free quota allowance of each line item on the [pricing page](/pricing).
You can monitor your usage and costs using the [Usage section of your project](https://app.supabase.com/project/_/settings/billing/usage) in the dashboard.
This will show you how much you have spent so far in the current billing cycle, as well as an estimate of your expected total cost for the cycle based on your current usage.
## How can I turn the Spend Cap on or off?
To set up a Spend Cap in your project, navigate to the [Subscription section](https://app.supabase.com/project/_/settings/billing/subscription) of your project.
From there, you can click on "Change subscription" to select the plan you want to use and, at the same time, turn on or off the Spend Cap.
## What happens when the Spend Cap is on?
Once you use all your free quota allowance for a [line item](#what-can-i-be-charged-for), your project will not allow any further usage on that line item until the next billing cycle begins.
This can help you to avoid unexpected costs and manage your expenses more effectively.
## What happens when Spend Cap is off?
Your project will continue to operate after using all the free quota allowance for each [line item](#what-can-i-be-charged-for).
This additional usage will be charged based on the line item's cost per unit outlined on the [pricing page](/pricing).
<Admonition type="note">
When your Spend Cap is **off**, we recommend monitoring your consumption and costs using the [Usage
section of your project](https://app.supabase.com/project/_/settings/billing/usage) in the
dashboard.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -64,7 +64,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
- [Svelte Todo List.](https://github.com/supabase/supabase/tree/master/examples/todo-list/sveltejs-todo-list)
- [Vue 3 Todo List (Typescript).](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/vue3-ts-todo-list)
- [Angular Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/angular-todo-list)
- [Nuxt 3 Todo List.](https://github.com/nuxt-community/supabase-module/tree/main/demo)
- [Nuxt 3 Todo List.](https://github.com/nuxt-modules/supabase/tree/main/demo)
### Auth examples
@@ -92,8 +92,8 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
- `useSupabase`: Supabase React Hooks. [GitHub](https://github.com/gbibeaul/use-supabase)
- `vue-supabase`: Supabase Vue wrapper. [GitHub](https://github.com/supabase/vue-supabase)
- `vue-3-supabase`: Supabase Vue 3 wrapper. [GitHub](https://github.com/DidoMarchet/vue-3-supabase)
- `nuxt-supabase`: Supabase Nuxt wrapper. [GitHub](https://github.com/supabase/nuxt-supabase)
- `@nuxtjs/supabase`: Supabase Nuxt 3 module. [GitHub](https://github.com/nuxt-community/supabase-module)
- `nuxt-supabase`: Supabase Nuxt wrapper. [GitHub](https://github.com/supabase-community/nuxt-supabase)
- `@nuxtjs/supabase`: Supabase Nuxt 3 module. [GitHub](https://github.com/nuxt-modules/supabase)
- `react-supabase`: Supabase React Hooks. [Docs](https://react-supabase.vercel.app) [GitHub](https://github.com/tmm/react-supabase)
- ` @triniwiz/nativescript-supabase`: Supabase NativeScript. [GitHub](https://github.com/triniwiz/nativescript-plugins/tree/master/packages/nativescript-supabase)
@@ -133,10 +133,11 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
- Supabase + Nuxt3 (Nuxt Starter Kit) [GitHub](https://github.com/one-aalam/nuxt-starter-kit)
- Supabase + Remix (Remix Starter Kit) [GitHub](https://github.com/one-aalam/remix-starter-kit)
- Supabase + Angular (Angular Starter Kit) [GitHub](https://github.com/one-aalam/ng-starter-kit)
- Supabase + Nuxt3 + nuxtjs/supabase [Github](https://github.com/nuxt-community/supabase-module/tree/main/demo)
- Supabase + Nuxt3 + nuxtjs/supabase [Github](https://github.com/nuxt-modules/supabase/tree/main/demo)
- Supabase + Ory Kratos & Ory Oathkeeper [Github](https://github.com/ory/examples/tree/master/kratos-keto-oathkeeper-supabase)
- Supabase + Ory Cloud [Github](https://github.com/ory/examples/tree/master/supabase-ory-cloud)
- Supabase Auth with React Native + Next.js (Monorepo) [Github](https://github.com/mateoguzmana/react-native-next-supabase-auth-monorepo)
- Supabase Auth with Nuxt3 [Github](https://github.com/zackha/supaAuth)
### Blog Posts
@@ -7,9 +7,7 @@ export const meta = {
sidebar_label: 'Quickstart',
}
This guide shows the basic functionality of Supabase Storage. Find a full [example application on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) or deploy it with Vercel for a preview:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
This guide shows the basic functionality of Supabase Storage. Find a full [example application on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) or deploy it with [Vercel for a preview](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management).
<Admonition type="note">
+52 -46
View File
@@ -2,67 +2,73 @@
import specFile from '~/../../spec/gotrue_v1_config.yaml' assert { type: 'yml' }
import { Parameter } from '~/lib/refGenerator/refTypes'
import ReactMarkdown from 'react-markdown'
import Head from 'next/head'
// Parameters are grouped on the page by tag
const TAGS = ['general']
export default function Config() {
return (
<div>
<div className="flex my-16">
<div className="w-full prose">
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>
{TAGS.map((tag) =>
specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag)
.map((parameter: Parameter, index) => (
<div>
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
<div className="mt-8">
<div>
<h2 className="text-xl font-medium text-scale-1200 font-mono">
<span className="mr-2">$</span>
{parameter.title}
</h2>
<div className="grid" id={parameter.id}>
<div className="border-b pb-8" key={parameter.id}>
<div className=" mb-16">
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
<ReactMarkdown>{parameter.description}</ReactMarkdown>
</p>
<div className="grid gap-2">
<div className="flex gap-2">
Required: <code>{parameter.required.toString()}</code>
</div>
<div className="flex gap-2">
Default:
<code>
{parameter.default ? parameter.default.toString() : 'None'}
</code>
<>
<Head>
<title>Auth config</title>
</Head>
<div>
<div className="flex my-16">
<div className="w-full prose">
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>
{TAGS.map((tag) =>
specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag)
.map((parameter: Parameter, index) => (
<div>
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
<div className="mt-8">
<div>
<h2 className="text-xl font-medium text-scale-1200 font-mono">
<span className="mr-2">$</span>
{parameter.title}
</h2>
<div className="grid" id={parameter.id}>
<div className="border-b pb-8" key={parameter.id}>
<div className=" mb-16">
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
<ReactMarkdown>{parameter.description}</ReactMarkdown>
</p>
<div className="grid gap-2">
<div className="flex gap-2">
Required: <code>{parameter.required.toString()}</code>
</div>
<div className="flex gap-2">
Default:
<code>
{parameter.default ? parameter.default.toString() : 'None'}
</code>
</div>
</div>
</div>
{parameter.links &&
parameter.links.map((link) => (
<div>
<h3>See also:</h3>
<li>
<a href={link.link}>{link.name}</a>
</li>
</div>
))}
</div>
{parameter.links &&
parameter.links.map((link) => (
<div>
<h3>See also:</h3>
<li>
<a href={link.link}>{link.name}</a>
</li>
</div>
))}
</div>
</div>
</div>
</div>
</div>
))
)}
))
)}
</div>
</div>
</div>
</div>
</div>
</>
)
}
+60 -54
View File
@@ -4,6 +4,7 @@ import { Parameter } from '~/lib/refGenerator/refTypes'
import ReactMarkdown from 'react-markdown'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { Heading } from '~/components/CustomHTMLElements'
import Head from 'next/head'
// Parameters are grouped on the page by tag
const TAGS = ['general', 'auth', 'api', 'database', 'dashboard', 'local', 'edge-functions']
@@ -21,69 +22,74 @@ const tocList = TAGS.map((tag) =>
export default function Config() {
return (
<div className="grid grid-cols-12 relative gap-4">
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
<div className="w-full prose">
<h1 className="">CLI configuration</h1>
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>
{TAGS.map((tag) =>
specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag)
.map((parameter: Parameter, index) => (
<div>
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
<div className="mt-8">
<div>
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
<code>{parameter.title}</code>
</Heading>
<div className="grid">
<div className="border-b pb-8" key={parameter.id}>
<div className=" mb-16">
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
<ReactMarkdown>{parameter.description}</ReactMarkdown>
</p>
<div className="grid gap-2">
<div className="flex gap-2">
Required: <code>{parameter.required.toString()}</code>
</div>
<div className="flex gap-2">
Default:
<code>
{parameter.default ? parameter.default.toString() : 'None'}
</code>
<>
<Head>
<title>Supabase CLI config</title>
</Head>
<div className="grid grid-cols-12 relative gap-4">
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
<div className="w-full prose">
<h1 className="">CLI configuration</h1>
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>
{TAGS.map((tag) =>
specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag)
.map((parameter: Parameter, index) => (
<div>
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
<div className="mt-8">
<div>
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
<code>{parameter.title}</code>
</Heading>
<div className="grid">
<div className="border-b pb-8" key={parameter.id}>
<div className=" mb-16">
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
<ReactMarkdown>{parameter.description}</ReactMarkdown>
</p>
<div className="grid gap-2">
<div className="flex gap-2">
Required: <code>{parameter.required.toString()}</code>
</div>
<div className="flex gap-2">
Default:
<code>
{parameter.default ? parameter.default.toString() : 'None'}
</code>
</div>
</div>
</div>
{parameter.links &&
parameter.links.map((link) => (
<div>
<h3>See also:</h3>
<li>
<a href={link.link}>{link.name}</a>
</li>
</div>
))}
</div>
{parameter.links &&
parameter.links.map((link) => (
<div>
<h3>See also:</h3>
<li>
<a href={link.link}>{link.name}</a>
</li>
</div>
))}
</div>
</div>
</div>
</div>
</div>
))
)}
))
)}
</div>
</div>
</div>
<div className="md:col-span-3">
<div className="sticky top-20 border-l">
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</div>
</div>
<div className="md:col-span-3">
<div className="sticky top-20 border-l">
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</div>
</div>
</>
)
}
+20 -31
View File
@@ -25,12 +25,11 @@
{
"title": "Database size",
"tooltips": {
"main": "Billing is based on the average database size in GB throughout the billing period.",
"pro": "8 GB included, then $0.125 per GB"
"main": "Billing is based on the average database size in GB throughout the billing period."
},
"tiers": {
"free": "500 MB",
"pro": "8 GB",
"pro": "8 GB, then $0.125 per GB",
"enterprise": "Unlimited"
}
},
@@ -72,12 +71,11 @@
{
"title": "Database egress",
"tooltips": {
"main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period. Only download operations are counted towards the limit",
"pro": "50GB included per month, then $0.09 per GB"
"main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period. Only download operations are counted towards the limit"
},
"tiers": {
"free": "2GB",
"pro": "50GB",
"pro": "50 GB, then $0.09 per GB",
"enterprise": "Unlimited"
}
}
@@ -99,12 +97,11 @@
{
"title": "MAUs",
"tooltips": {
"main": "Distinct users requesting your API throughout the billing period.",
"pro": "100,000 included, then $0.00325 per MAU"
"main": "Distinct users requesting your API throughout the billing period."
},
"tiers": {
"free": "50,000",
"pro": "100,000",
"pro": "100,000, then $0.00325 per MAU",
"enterprise": "Unlimited"
}
},
@@ -175,24 +172,22 @@
{
"title": "Storage",
"tooltips": {
"pro": "100 GB included, then $0.021 per GB",
"main": "The sum of all objects' size in your storage buckets."
},
"tiers": {
"free": "1 GB",
"pro": "100 GB",
"pro": "100 GB, then $0.021 per GB",
"enterprise": "Unlimited"
}
},
{
"title": "Storage egress",
"tooltips": {
"main": "Total sum of outgoing traffic (egress) from your storage buckets, only download operations are counted.",
"pro": "200 GB included, then $0.09 per GB"
"main": "Total sum of outgoing traffic (egress) from your storage buckets, only download operations are counted."
},
"tiers": {
"free": "2 GB",
"pro": "200 GB",
"pro": "200 GB, then $0.09 per GB",
"enterprise": "Unlimited"
}
},
@@ -216,12 +211,11 @@
{
"title": "Asset transformations",
"tooltips": {
"main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations.",
"pro": "Unlimited for 100 origin images. $5 per 1000 origin images after the first 100"
"main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations."
},
"tiers": {
"free": false,
"pro": "Unlimited for 100 origin images"
"pro": "100 origin images, then $5 per 1000 origin images"
}
}
]
@@ -233,13 +227,12 @@
{
"title": "Invocations",
"tooltips": {
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period.",
"pro": "2 Million per month, then $2 per 1 million invocations"
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period."
},
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period.",
"tiers": {
"free": "500K/month",
"pro": "2 Million/month",
"pro": "2 Million, then $2 per 1 Million",
"enterprise": "Unlimited"
}
},
@@ -254,12 +247,11 @@
{
"title": "Number of functions",
"tooltips": {
"main": "Billing is based on the maximum amount of functions at any point in time throughout your billing period.",
"pro": "100, then $10 per additional 100 functions"
"main": "Billing is based on the maximum amount of functions at any point in time throughout your billing period."
},
"tiers": {
"free": "10",
"pro": "100",
"pro": "100, then $10 per additional 100",
"enterprise": "Unlimited"
}
}
@@ -280,25 +272,22 @@
{
"title": "Concurrent Peak Connections",
"tooltips": {
"main": "We only charge for successful connections - not connection attempts!",
"pro": "Up to 500, then $10 per 1000"
"main": "We only charge for successful connections - not connection attempts!"
},
"tiers": {
"free": "200",
"pro": "500",
"pro": "500, then $10 per 1000",
"enterprise": "Unlimited concurrent connections and volume discount"
}
},
{
"title": "Messages Per Month",
"tooltips": {
"main": "Billing is based on the total sum of Realtime messages throughout your billing period.",
"free": "2 Million (including database)",
"pro": "5 Million, then $2.50 per million"
"main": "Billing is based on the total sum of Realtime messages throughout your billing period."
},
"tiers": {
"free": "2 Million",
"pro": "5 Million",
"pro": "5 Million, then $2.50 per Million",
"enterprise": "Volume discounts on messages"
}
},
@@ -426,7 +415,7 @@
"tiers": {
"free": false,
"pro": "$10 per domain per month per project add on",
"enterprise": "1 included, additional $10/domain/month"
"enterprise": "1, additional $10/domain/month"
}
}
]
@@ -52,7 +52,7 @@ export default [
img: 'multiplayer-game.svg',
title: 'Multiplayer games',
description:
"Keep track of how long a player has been playing, who's turn it is and even share to other clients what the pllayer is doing.",
"Keep track of how long a player has been playing, who's turn it is and even share to other clients what the player is doing.",
},
{
img: 'form-presence.svg',
+2 -40
View File
@@ -6,53 +6,15 @@ import CodeBlock from '~/components/CodeBlock/CodeBlock'
import ImageGrid from '~/components/ImageGrid'
import DefaultLayout from '~/components/Layouts/Default'
import Quote from '~/components/Quote'
import mdxComponents from '~/lib/mdx/mdxComponents'
const ignoreClass = 'ignore-on-export'
function getComponents(type: any) {
const components = {
CodeBlock,
Quote,
Avatar,
code: (props: any) => {
if (props.className !== ignoreClass) {
return <CodeBlock {...props} />
} else {
return <code {...props} />
}
},
ImageGrid,
img: (props: any) => {
if (props.className !== ignoreClass) {
return (
<div
className={[
'next-image--dynamic-fill',
type === 'blog' && 'to-scale-400 from-scale-500 rounded-lg border bg-gradient-to-r',
].join(' ')}
>
<Image
{...props}
className={['next-image--dynamic-fill', type === 'blog' && 'rounded-md border'].join(
' '
)}
layout="fill"
/>
</div>
)
}
return <img {...props} />
},
}
return components
}
export default function ContentsLayout({ context }: any) {
return (
<DefaultLayout>
<NextSeo {...context.meta} />
<MDXProvider components={getComponents(context.type)}>
<MDXProvider components={mdxComponents()}>
<div className="prose max-w-none">{context.children}</div>
</MDXProvider>
</DefaultLayout>
+8
View File
@@ -3,6 +3,14 @@ module.exports = [
source: '/:path*',
destination: `/:path*`,
},
{
source: '/dashboard',
destination: `${process.env.NEXT_PUBLIC_STUDIO_URL}`,
},
{
source: '/dashboard/:path*',
destination: `${process.env.NEXT_PUBLIC_STUDIO_URL}/:path*`,
},
{
source: '/docs',
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}`,
+8 -5
View File
@@ -128,11 +128,14 @@ In addition to internal security reviews, we use various tools to scan our code
As a database company, being SOC2 compliant is important when handling sensitive customer data. Access more of Supabase’s security information by requesting for our latest SOC2 documents below.
<Link passHref href="https://security.supabase.com">
<Button type="default" size="medium" as="a" className="no-underline" target="_blank">
Get SOC2 documents
</Button>
</Link>
<div className="not-prose">
<Link passHref href="https://forms.supabase.com/soc2">
<Button size="medium" type="default" as="a" target="_blank">
Request SOC2 documents
</Button>
</Link>
</div>
</div>
</div>
+7 -5
View File
@@ -211,7 +211,7 @@ services:
# Comment out everything below this point if you are using an external Postgres database
db:
container_name: supabase-db
image: supabase/postgres:15.1.0.42-rc2
image: supabase/postgres:15.1.0.54-rc0
healthcheck:
test: pg_isready -U postgres -h localhost
interval: 5s
@@ -236,8 +236,10 @@ services:
PGDATABASE: ${POSTGRES_DB}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/realtime.sql:Z
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/roles.sql:Z
# Must be superuser to enable pg_net extension
- ./volumes/db/webhooks.sql:/etc/postgresql.schema.sql:Z
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
# Must be superuser to create event trigger
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
# Must be superuser to alter reserved role
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
# PGDATA directory is persisted between restarts
- ./volumes/db/data:/var/lib/postgresql/data:Z
+1
View File
@@ -4,4 +4,5 @@
ALTER USER authenticator WITH PASSWORD :'pgpass';
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
+22 -30
View File
@@ -2,11 +2,11 @@ BEGIN;
-- Create pg_net extension
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
-- Create supabase_functions schema
CREATE SCHEMA if not exists supabase_functions;
GRANT USAGE ON SCHEMA supabase_functions TO postgres;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres;
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
-- supabase_functions.migrations definition
CREATE TABLE supabase_functions.migrations (
version text PRIMARY KEY,
@@ -41,29 +41,29 @@ BEGIN;
IF url IS NULL OR url = 'null' THEN
RAISE EXCEPTION 'url argument is missing';
END IF;
IF method IS NULL OR method = 'null' THEN
RAISE EXCEPTION 'method argument is missing';
END IF;
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
headers = '{"Content-Type": "application/json"}'::jsonb;
ELSE
headers = TG_ARGV[2]::jsonb;
END IF;
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
params = '{}'::jsonb;
ELSE
params = TG_ARGV[3]::jsonb;
END IF;
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
timeout_ms = 1000;
ELSE
timeout_ms = TG_ARGV[4]::integer;
END IF;
CASE
WHEN method = 'GET' THEN
SELECT http_get INTO request_id FROM net.http_get(
@@ -74,13 +74,13 @@ BEGIN;
);
WHEN method = 'POST' THEN
payload = jsonb_build_object(
'old_record', OLD,
'record', NEW,
'old_record', OLD,
'record', NEW,
'type', TG_OP,
'table', TG_TABLE_NAME,
'schema', TG_TABLE_SCHEMA
);
SELECT http_post INTO request_id FROM net.http_post(
url,
payload,
@@ -91,12 +91,12 @@ BEGIN;
ELSE
RAISE EXCEPTION 'method argument % is invalid', method;
END CASE;
INSERT INTO supabase_functions.hooks
(hook_table_id, hook_name, request_id)
VALUES
(TG_RELID, TG_NAME, request_id);
RETURN NEW;
END
$function$;
@@ -148,19 +148,15 @@ BEGIN;
WHERE extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres;
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_collect_response(request_id bigint, async boolean) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_collect_response(request_id bigint, async boolean) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END
$$;
@@ -178,19 +174,15 @@ BEGIN;
WHERE ext.extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres;
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_collect_response(request_id bigint, async boolean) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_collect_response(request_id bigint, async boolean) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin, postgres;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END;
$$;
@@ -212,5 +204,5 @@ BEGIN;
ALTER function supabase_functions.http_request() SECURITY DEFINER;
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres;
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
COMMIT;
@@ -3,7 +3,6 @@
Generate Open Graph images with Deno and Supabase Edge Functions, no framework needed. This is a fork of the awesome [@vercel/og](https://www.npmjs.com/package/@vercel/og), ported to run on Deno.
- Docs: https://deno.land/x/og_edge@0.0.2
- Demo: https://zdtdtxajzydjqzuktnqx.functions.supabase.co/opengraph
## Run locally
@@ -1,23 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
@@ -1,39 +1,22 @@
{
"name": "react-user-management",
"version": "0.1.0",
"private": true,
"dependencies": {
"@supabase/supabase-js": "^2.0.4",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
},
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"dependencies": {
"@supabase/supabase-js": "^2.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.1.0"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -10,66 +10,59 @@ export default function Account({ session }) {
useEffect(() => {
async function getProfile() {
try {
setLoading(true)
const { user } = session
setLoading(true)
const { user } = session
let { data, error } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error) {
throw error
}
let { data, error } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (error) {
console.warn(error)
} else if (data) {
setUsername(data.username)
setWebsite(data.website)
setAvatarUrl(data.avatar_url)
} catch (error) {
console.warn(error.message)
} finally {
setLoading(false)
}
setLoading(false)
}
getProfile()
}, [session])
async function updateProfile({ username, website, avatar_url }) {
try {
setLoading(true)
const { user } = session
async function updateProfile(event) {
event.preventDefault()
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
}
setLoading(true)
const { user } = session
let { error } = await supabase.from('profiles').upsert(updates)
if (error) {
throw error
}
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) {
alert(error.message)
}
setLoading(false)
}
return (
<div className="form-widget">
<form onSubmit={updateProfile} className="form-widget">
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
onUpload={(event, url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
updateProfile(event)
}}
/>
<div>
@@ -81,6 +74,7 @@ export default function Account({ session }) {
<input
id="username"
type="text"
required
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
@@ -96,20 +90,16 @@ export default function Account({ session }) {
</div>
<div>
<button
className="button block primary"
onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
>
<button className="button block primary" type="submit" disabled={loading}>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<button className="button block" onClick={() => supabase.auth.signOut()}>
<button className="button block" type="button" onClick={() => supabase.auth.signOut()}>
Sign Out
</button>
</div>
</div>
</form>
)
}
@@ -1,10 +1,10 @@
import './index.css'
import { useState, useEffect } from 'react'
import './App.css'
import { supabase } from './supabaseClient'
import Auth from './Auth'
import Account from './Account'
export default function Home() {
function App() {
const [session, setSession] = useState(null)
useEffect(() => {
@@ -23,3 +23,5 @@ export default function Home() {
</div>
)
}
export default App
@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react'
import App from './App'
test('renders learn react link', () => {
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})
@@ -1,50 +0,0 @@
import { useState } from 'react'
import { supabase } from './supabaseClient'
export default function Auth() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const handleLogin = async (email) => {
try {
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
setLoading(false)
}
}
return (
<div className="row flex flex-center">
<div className="col-6 form-widget">
<h1 className="header">Supabase + React</h1>
<p className="description">Sign in via magic link with your email below</p>
<div>
<input
className="inputField"
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<button
onClick={(e) => {
e.preventDefault()
handleLogin(email)
}}
className={'button block'}
disabled={loading}
>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,47 @@
import { useState } from 'react'
import { supabase } from './supabaseClient'
export default function Auth() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const handleLogin = async (event) => {
event.preventDefault()
setLoading(true)
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) {
alert(error.error_description || error.message)
} else {
alert('Check your email for the login link!')
}
setLoading(false)
}
return (
<div className="row flex flex-center">
<div className="col-6 form-widget">
<h1 className="header">Supabase + React</h1>
<p className="description">Sign in via magic link with your email below</p>
<form className="form-widget" onSubmit={handleLogin}>
<div>
<input
className="inputField"
type="email"
placeholder="Your email"
value={email}
required={true}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<button className={'button block'} disabled={loading}>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</div>
</form>
</div>
</div>
)
}
@@ -41,7 +41,7 @@ export default function Avatar({ url, size, onUpload }) {
throw uploadError
}
onUpload(filePath)
onUpload(event, filePath)
} catch (error) {
alert(error.message)
} finally {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,17 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
@@ -1,13 +0,0 @@
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'
@@ -5,7 +5,7 @@
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
@@ -126,10 +126,16 @@ export function Separator() {
return <div className={__styles.separator}></div>
}
export function Content({ children }: { children: React.ReactNode }) {
export function Content({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
let __styles = styleHandler('sidepanel')
return <div className={__styles.content}>{children}</div>
return <div className={[__styles.content, className].join(' ').trim()}>{children}</div>
}
SidePanel.Content = Content
+24 -24
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/csharp
title: Getting started
title: Supabase C# Client
description: |
Supabase C#.
@@ -293,9 +293,9 @@ functions:
isSpotlight: true
code: |
```c#
var attrs = new UserAttributes
{
Data = new Dictionary<string, string> { {"example", "data" } }
var attrs = new UserAttributes
{
Data = new Dictionary<string, string> { {"example", "data" } }
};
var response = await supabase.Auth.Update(attrs);
```
@@ -516,10 +516,10 @@ functions:
public int CountryId { get; set; }
}
var model = new City
{
Name = "The Shire",
CountryId = 554
var model = new City
{
Name = "The Shire",
CountryId = 554
};
await supabase.From<City>().Insert(model);
@@ -603,10 +603,10 @@ functions:
isSpotlight: true
code: |
```c#
var model = new City
{
Id = 554,
Name = "Middle Earth"
var model = new City
{
Id = 554,
Name = "Middle Earth"
};
await supabase.From<City>().Upsert(model);
@@ -620,10 +620,10 @@ functions:
isSpotlight: true
code: |
```c#
var model = new City
{
Id = 554,
Name = "Middle Earth"
var model = new City
{
Id = 554,
Name = "Middle Earth"
};
await supabase
@@ -637,10 +637,10 @@ functions:
Allowed values for count option are `exact`, `planned` and `estimated`.
code: |
```c#
var model = new City
{
Id = 554,
Name = "Middle Earth"
var model = new City
{
Id = 554,
Name = "Middle Earth"
};
await supabase
@@ -702,7 +702,7 @@ functions:
isSpotlight: true
code: |
```c#
class CursorBroadcast : BaseBroadcast
class CursorBroadcast : BaseBroadcast
{
[JsonProperty("cursorX")]
public int CursorX {get; set;}
@@ -714,7 +714,7 @@ functions:
var channel = supabase.Realtime.Channel("any");
var broadcast = channel.Register<CursorBroadcast>();
broadcast.OnBroadcast += (sender, _) =>
broadcast.OnBroadcast += (sender, _) =>
{
var data = broadcast.Current();
};
@@ -729,7 +729,7 @@ functions:
isSpotlight: true
code: |
```c#
class UserPresence : BasePresence
class UserPresence : BasePresence
{
[JsonProperty("cursorX")]
public bool IsTyping {get; set;}
@@ -742,7 +742,7 @@ functions:
var presenceKey = Guid.NewGuid().ToString();
var presence = channel.Register<UserPresence>(presenceKey);
presence.OnSync += (sender, _) =>
presence.OnSync += (sender, _) =>
{
var state = presence.CurrentState;
};
+1 -1
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/dart
title: Getting started
title: Supabase Dart Client
description: |
Supabase Dart.
+2 -2
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/dart
title: Getting started
title: Supabase Dart Client
description: |
Supabase Dart.
@@ -39,7 +39,7 @@ functions:
runApp(MyApp());
}
// Get a reference your Supabase client
// Get a reference your Supabase client
final supabase = Supabase.instance.client;
```
- id: for-other-dart-projects
+3 -3
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/supabase-js
title: Supabase Client
title: Supabase Javascript Client
description: |
Supabase JavaScript.
@@ -61,7 +61,7 @@ functions:
```
description: |
By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard.
Go to `Settings > API > Schema` and add the schema which you want to expose to the API. You also need to grant `USAGE` on your new schema with the grants you desire, such as `SELECT, INSERT, UPDATE, DELETE`.
Go to `Settings > API > Schema` and add the schema which you want to expose to the API. You also need to grant `USAGE` on your new schema with the grants you desire, such as `SELECT, INSERT, UPDATE, DELETE`.
Note: each client connection can only access a single schema, so the code above can access the `other_schema` schema but cannot access the `public` schema.
- id: custom-fetch-implementation
@@ -567,7 +567,7 @@ functions:
notes: |
Sends a password reset request to an email address.
- When the user clicks the reset link in the email they are redirected back to your application.
You can configure the URL that the user is redirected to via the `redirectTo` param.
You can configure the URL that the user is redirected to via the `redirectTo` param.
See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
- After the user has been redirected successfully, prompt them for a new password and call `updateUser()`:
```js
+9 -11
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/supabase-js
title: Supabase Client
title: Supabase Javascript Client
description: |
Supabase JavaScript.
@@ -812,9 +812,7 @@ functions:
const { data, error } = await supabase.auth.admin.generateLink({
type: 'signup',
email: 'email@example.com',
options: {
'password': 'secret'
}
password: 'secret'
})
```
- id: generate-an-invite-link
@@ -1320,21 +1318,21 @@ functions:
sql: |
```sql
create table countries (
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text
);
create table cities (
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text,
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text,
"country_id" "uuid" references public.countries on delete cascade
);
with country as (
insert into countries (name)
insert into countries (name)
values ('united kingdom') returning id
)
insert into cities (name, country_id) values
insert into cities (name, country_id) values
('London', (select id from country)),
('Manchester', (select id from country)),
('Liverpool', (select id from country)),
@@ -3889,7 +3887,7 @@ functions:
}
```
description: |
You can use an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to abort requests.
You can use an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to abort requests.
Note that `status` and `statusText` don't mean anything for aborted requests as the request wasn't fulfilled.
hideCodeBlock: true
isSpotlight: true
+7 -7
View File
@@ -2,7 +2,7 @@ openref: 0.1
info:
id: reference/supabase-py
title: Supabase Client
title: Supabase Python Client
description: |
Supabase Python(Community).
@@ -519,21 +519,21 @@ functions:
sql: |
```sql
create table countries (
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text
);
create table cities (
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text,
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
"name" text,
"country_id" "uuid" references public.countries on delete cascade
);
with country as (
insert into countries (name)
insert into countries (name)
values ('united kingdom') returning id
)
insert into cities (name, country_id) values
insert into cities (name, country_id) values
('London', (select id from country)),
('Manchester', (select id from country)),
('Liverpool', (select id from country)),
@@ -543,7 +543,7 @@ functions:
```
APIResponse(data=[{'id': 1, 'name': 'Germany', 'cities': [{'count': 1}]},
{'id': 2, 'name': 'Indonesia', 'cities': [{'count': 1}]}],
count=None)
count=None)
```
description: |
You can get the number of rows in a related table by using the
+7 -1
View File
@@ -198,7 +198,13 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
onAddColumn={editable ? props.onAddColumn : undefined}
headerActions={headerActions}
/>
<Grid ref={gridRef} {...gridProps} rows={data?.rows ?? []} updateRow={props.updateTableRow} />
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
updateRow={props.updateTableRow}
onEditForeignKeyColumnValue={props.onEditForeignKeyColumnValue}
/>
<Footer isLoading={isLoading || isRefetching} />
<Shortcuts gridRef={gridRef} />
{mounted &&
+10 -6
View File
@@ -215,9 +215,12 @@ export function parseSupaTable(
isEncrypted: encryptedColumns.includes(column.name),
enum: column.enums,
comment: column.comment,
targetTableSchema: null,
targetTableName: null,
targetColumnName: null,
foreignKey: {
targetTableSchema: null,
targetTableName: null,
targetColumnName: null,
deletionAction: undefined,
},
}
const primaryKey = primaryKeys.find((pk) => pk.name == column.name)
temp.isPrimaryKey = !!primaryKey
@@ -230,9 +233,10 @@ export function parseSupaTable(
)
})
if (relationship) {
temp.targetTableSchema = relationship.target_table_schema
temp.targetTableName = relationship.target_table_name
temp.targetColumnName = relationship.target_column_name
temp.foreignKey.targetTableSchema = relationship.target_table_schema
temp.foreignKey.targetTableName = relationship.target_table_name
temp.foreignKey.targetColumnName = relationship.target_column_name
temp.foreignKey.deletionAction = relationship.deletion_action
}
return temp
})
@@ -1,110 +0,0 @@
import React, { FC, useState } from 'react'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { Button, Input, IconChevronDown, IconSearch } from 'ui'
import { DropdownControl } from '../../common'
import { FilterOperatorOptions } from '../../header/filter'
import { Filter, FilterOperator } from '../../../types'
interface FilterProps {
defaultColumnName?: string
defaultValue?: string
foreignColumnNames: string[]
onChange: (value: Filter) => void
}
export const FilterHeader: FC<FilterProps> = ({
defaultColumnName,
defaultValue,
foreignColumnNames,
onChange,
}) => {
const [columnName, setColumnName] = useState(
defaultColumnName
? defaultColumnName
: foreignColumnNames.length > 0
? foreignColumnNames[0]
: ''
)
const [condition, setCondition] = useState(FilterOperatorOptions[0].value)
const [filterText, setFilterText] = useState(defaultValue ?? '')
const columnOptions =
foreignColumnNames.map((x) => {
return { value: x, label: x }
}) || []
function triggerOnChange(columnName: string, condition: string, filterText: string) {
onFilterChangeDebounced(
{
column: columnName,
operator: condition as FilterOperator,
value: filterText,
},
onChange
)
}
function onColumnChange(value: string | number) {
const str = value + ''
setColumnName(str)
triggerOnChange(str, condition, filterText)
}
function onConditionChange(value: string | number) {
const str = value + ''
setCondition(str)
triggerOnChange(columnName, str, filterText)
}
function onFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value
setFilterText(value)
triggerOnChange(columnName, condition, value)
}
return (
<div className="foreign-table-modal__filter">
<DropdownControl
side="bottom"
align="start"
options={columnOptions}
onSelect={onColumnChange}
>
<Button as="span" type="outline" iconRight={<IconChevronDown />}>
<span className="foreign-table-modal__filter__trigger-content">
<span className="foreign-table-modal__filter__trigger-content__label">Column</span>
<span className="foreign-table-modal__filter__trigger-content__name">{columnName}</span>
</span>
</Button>
</DropdownControl>
<DropdownControl
side="bottom"
align="start"
options={FilterOperatorOptions}
onSelect={onConditionChange}
>
<Button as="span" type="outline" iconRight={<IconChevronDown />}>
<span className="foreign-table-modal__filter__trigger-content">
<span className="foreign-table-modal__filter__trigger-content__label">Filter</span>
<span className="foreign-table-modal__filter__trigger-content__name">{condition}</span>
</span>
</Button>
</DropdownControl>
<Input
size="tiny"
className="foreign-table-modal__filter__search-input"
placeholder="Find a record"
value={filterText}
onChange={onFilterChange}
icon={<IconSearch size="small" />}
/>
</div>
)
}
const onFilterChangeDebounced = AwesomeDebouncePromise(
(filter: Filter, onChange: (value: Filter) => void) => {
onChange(filter)
},
500
)
@@ -1,43 +0,0 @@
import * as React from 'react'
import { Menu } from 'ui'
import { Dictionary } from '../../../types'
interface RowItemProps {
item: Dictionary<any>
onSelect: (item: Dictionary<any>) => void
columnNames: string[]
}
export const RowItem: React.FC<RowItemProps> = ({ item, onSelect, columnNames }) => {
return (
<div className="foreign-table-modal__row-item">
<Menu.Item onClick={() => onSelect(item)} style={{ minWidth: 'min-content' }}>
<div className="foreign-table-modal__row-item__inner">
{columnNames.map((key, j) => {
//
// limit to 5 attributes
//
// this could be improved so the user could pick which attributes to display
// @mildtomato
if (j > 5) return null
return (
<div className="foreign-table-modal__row-item__inner__key-item" key={`item-${j}`}>
<p className="foreign-table-modal__row-item__inner__key-item__key text-sm text-scale-1000">
{key}
</p>
<p className="text-sm font-bold">
{item[key]
? typeof item[key] === 'object'
? JSON.stringify(item[key])
: item[key]
: '[null]'}
</p>
</div>
)
})}
</div>
</Menu.Item>
</div>
)
}
@@ -1,163 +0,0 @@
import * as React from 'react'
import { Button, Modal, Menu, IconExternalLink, IconTrash } from 'ui'
import { RowItem } from './RowItem'
import { FilterHeader } from './FilterHeader'
import Query from 'components/grid/query'
import { useTrackedState } from 'components/grid/store'
import { Dictionary, Filter } from 'components/grid/types'
type ForeignTableModalProps = {
columnName?: string
defaultValue?: string
onChange: (value: any | null) => void
}
export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
columnName,
defaultValue,
onChange,
}) => {
const [visible, setVisible] = React.useState(false)
const [foreignColumnNames, setForeignColumnNames] = React.useState<string[]>([])
const [rows, setRows] = React.useState<Dictionary<any>[] | null>(null)
const state = useTrackedState()
const columnDefinition = state.table?.columns.find((x) => x.name == columnName)
React.useEffect(() => {
if (!visible) return
fetchColumns()
if (defaultValue && columnDefinition) {
fetchData({
column: columnDefinition.targetColumnName!,
operator: '=',
value: defaultValue,
})
} else {
fetchData()
}
}, [visible])
async function fetchColumns() {
if (
!state.metaService ||
!columnDefinition?.targetTableName ||
!columnDefinition?.targetTableSchema
)
return
const { data } = await state.metaService.fetchColumns(
columnDefinition?.targetTableName,
columnDefinition?.targetTableSchema
)
if (data && data.length > 0) {
const columnNames = data.map((x) => x.name)
setForeignColumnNames(columnNames)
}
}
async function fetchData(filter?: Filter) {
if (!state.onSqlQuery || !columnDefinition?.targetTableName) return
const query = new Query()
let queryChains = query
.from(columnDefinition?.targetTableName, columnDefinition?.targetTableSchema ?? undefined)
.select()
if (filter && filter.value && filter.value != '') {
const { column, operator, value } = filter
queryChains = queryChains.filter(column, operator, value)
}
// TODO: How to let users know that filter result limit at 20 results
// should we allow a higher value?
const sql = queryChains.range(0, 20).toSql()
const { data, error } = await state.onSqlQuery(sql)
if (error) {
setRows(null)
} else {
setRows(data)
}
}
function toggle(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (event && event.stopPropagation) event.stopPropagation()
setVisible(!visible)
}
function onItemSelect(item: Dictionary<any>) {
if (item && columnDefinition && columnDefinition.targetColumnName) {
const value = item[columnDefinition.targetColumnName]
onChange(value)
}
setVisible(false)
}
function renderRows() {
if (!rows) return null
const temp = rows.map((x, i) => {
return (
<RowItem
key={`menu-${i}`}
item={x}
onSelect={onItemSelect}
columnNames={foreignColumnNames}
/>
)
})
return <Menu className="foreign-table-modal__menu">{temp}</Menu>
}
function onFilterChange(filter: Filter) {
if (filter && filter.value && filter.value != '') fetchData(filter)
}
function onClearValueClick() {
onChange(null)
setVisible(false)
}
return (
<>
<Button
type="default"
onClick={toggle}
icon={<IconExternalLink />}
style={{ padding: '3px' }}
/>
<Modal
visible={visible}
onCancel={toggle}
closable
customFooter={
<Button block danger icon={<IconTrash />} onClick={onClearValueClick}>
Clear value
</Button>
}
>
<Modal.Content>
<FilterHeader
defaultColumnName={columnDefinition?.targetColumnName ?? undefined}
defaultValue={defaultValue}
foreignColumnNames={foreignColumnNames}
onChange={onFilterChange}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="foreign-table-modal__content-container">
<div
className="foreign-table-modal__content-container__inner__overflow"
style={{ minHeight: '21rem', maxHeight: '20rem' }}
>
{renderRows()}
</div>
</div>
</Modal.Content>
</Modal>
</>
)
}
@@ -1,30 +1,46 @@
import * as React from 'react'
import { Select } from 'ui'
import { EditorProps } from '@supabase/react-data-grid'
import { useTrackedState } from 'components/grid/store'
interface Props<TRow, TSummaryRow = unknown> extends EditorProps<TRow, TSummaryRow> {
isNullable?: boolean
}
export function BooleanEditor<TRow, TSummaryRow = unknown>({
row,
column,
isNullable,
onRowChange,
onClose,
}: EditorProps<TRow, TSummaryRow>) {
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
onRowChange({ ...row, [column.key]: event.target.checked })
}
}: Props<TRow, TSummaryRow>) {
const state = useTrackedState()
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
const value = row[column.key as keyof TRow] as unknown as string
function onBlur() {
onClose(true)
const onBlur = () => onClose(false)
const onChange = (event: any) => {
const value = event.target.value
if (value === 'null') {
onRowChange({ ...row, [column.key]: null }, true)
} else {
onRowChange({ ...row, [column.key]: value === 'true' }, true)
}
}
return (
<div className="sb-grid-checkbox-editor">
<input
className="sb-grid-checkbox-editor__input"
checked={row[column.key as keyof TRow] as unknown as boolean}
onChange={onChange}
onBlur={onBlur}
type="checkbox"
style={{ margin: 'auto' }}
/>
</div>
<Select
autoFocus
id="boolean-editor"
name="boolean-editor"
size="small"
onBlur={onBlur}
onChange={onChange}
defaultValue={value === null ? 'null' : value.toString()}
style={{ width: `${gridColumn?.width || column.width}px` }}
>
<Select.Option value="true">TRUE</Select.Option>
<Select.Option value="false">FALSE</Select.Option>
{isNullable && <Select.Option value="null">NULL</Select.Option>}
</Select>
)
}
@@ -1,51 +0,0 @@
import React, { useEffect } from 'react'
import { EditorProps } from '@supabase/react-data-grid'
import { Button, IconX } from 'ui'
export function NullableBooleanEditor<TRow, TSummaryRow = unknown>({
row,
column,
onRowChange,
onClose,
}: EditorProps<TRow, TSummaryRow>) {
const value = row[column.key as keyof TRow] as unknown as boolean | null
useEffect(() => {
// if value is null, set it to false on initial render
if (value === null) {
onRowChange({ ...row, [column.key]: false })
}
}, [])
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
onRowChange({ ...row, [column.key]: event.target.checked })
}
function onBlur() {
onClose(true)
}
function onClear() {
onRowChange({ ...row, [column.key]: null }, true)
}
return (
<div className="sb-grid-checkbox-editor">
<input
className="sb-grid-checkbox-editor__input"
checked={value === null ? false : value}
onChange={onChange}
onBlur={onBlur}
type="checkbox"
style={{ margin: 'auto' }}
/>
<Button
type="text"
title="Clear"
icon={<IconX size="tiny" strokeWidth={2} />}
onClick={onClear}
style={{ padding: '3px', margin: 'auto 5px auto auto' }}
/>
</div>
)
}
@@ -2,13 +2,14 @@ import { useState, useCallback } from 'react'
import { EditorProps } from '@supabase/react-data-grid'
import { useTrackedState } from '../../store'
import { BlockKeys, MonacoEditor, NullValue, EmptyValue } from '../common'
import { Popover } from 'ui'
import { Button, Popover } from 'ui'
export function TextEditor<TRow, TSummaryRow = unknown>({
row,
column,
isNullable,
onRowChange,
}: EditorProps<TRow, TSummaryRow>) {
}: EditorProps<TRow, TSummaryRow> & { isNullable?: boolean }) {
const state = useTrackedState()
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
@@ -59,6 +60,13 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
<p className="text-xs text-scale-1100">Cancel changes</p>
</div>
</div>
<div className="space-y-1">
{isNullable && (
<Button type="default" size="tiny" onClick={() => saveChanges(null)}>
Set to NULL
</Button>
)}
</div>
</div>
</BlockKeys>
}
@@ -2,7 +2,6 @@ export * from './BooleanEditor'
export * from './DateEditor'
export * from './DateTimeEditor'
export * from './JsonEditor'
export * from './NullableBooleanEditor'
export * from './NumberEditor'
export * from './SelectEditor'
export * from './TextEditor'
@@ -6,5 +6,5 @@ import { NullValue } from '../common'
export const BooleanFormatter = (p: React.PropsWithChildren<FormatterProps<SupaRow, unknown>>) => {
const value = p.row[p.column.key] as boolean | null
if (value === null) return <NullValue />
return <>{value ? 'true' : 'false'}</>
return <>{value ? 'TRUE' : 'FALSE'}</>
}
@@ -1,43 +1,70 @@
import * as React from 'react'
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { FormatterProps } from '@supabase/react-data-grid'
import { SupaRow } from '../../types'
import { NullValue } from '../common'
import { ForeignTableModal } from '../common/ForeignTableModal'
import { useDispatch, useTrackedState } from '../../store'
import { deepClone } from '../../utils'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconArrowRight } from 'ui'
import { useParams, useStore } from 'hooks'
export const ForeignKeyFormatter = (
p: React.PropsWithChildren<FormatterProps<SupaRow, unknown>>
) => {
const state = useTrackedState()
const dispatch = useDispatch()
const value = p.row[p.column.key]
export const ForeignKeyFormatter = (props: PropsWithChildren<FormatterProps<SupaRow, unknown>>) => {
const { ref, id } = useParams()
const { meta } = useStore()
function onRowChange(_value: any | null) {
const rowData = deepClone(p.row)
rowData[p.column.key] = _value
const { row, column } = props
const selectedTable = meta.tables.byId(id as string)
const relationship = (selectedTable?.relationships ?? []).find(
(r) =>
r.source_schema === selectedTable?.schema &&
r.source_table_name === selectedTable?.name &&
r.source_column_name === column.name
)
const targetTable = meta.tables
.list()
.find(
(table) =>
table.schema === relationship?.target_table_schema &&
table.name === relationship.target_table_name
)
const { error } = state.rowService!.update(rowData, p.row, p.column.key)
if (error) {
if (state.onError) state.onError(error)
} else {
dispatch({
type: 'EDIT_ROW',
payload: { row: rowData, idx: p.row.idx },
})
}
}
function onChange(_value: any | null) {
onRowChange(_value)
}
const value = row[column.key]
return (
<div className="sb-grid-foreign-key-formatter flex justify-between">
<span className="sb-grid-foreign-key-formatter__text">
{value === null ? <NullValue /> : value}
</span>
<ForeignTableModal columnName={p.column.key} defaultValue={value} onChange={onChange} />
{relationship !== undefined && targetTable !== undefined && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Link
href={`/project/${ref}/editor/${targetTable?.id}?filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
>
<a>
<Button
type="default"
size="tiny"
className="translate-y-[2px]"
onClick={() => {}}
icon={<IconArrowRight size="tiny" />}
style={{ padding: '3px' }}
/>
</a>
</Link>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<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">View referencing record</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
)
}
@@ -1,12 +1,14 @@
import * as React from 'react'
import { IconKey, IconLink, IconLock } from 'ui'
import { IconArrowRight, IconKey, IconLink, IconLock } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'
import { XYCoord } from 'dnd-core'
import { useDispatch } from '../../store'
import { ColumnHeaderProps, ColumnType, DragItem } from '../../types'
import { ColumnHeaderProps, ColumnType, DragItem, GridForeignKey } from '../../types'
import { ColumnMenu } from '../menu'
import { useTrackedState } from '../../store'
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
import { getForeignKeyDeletionAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
export function ColumnHeader<R>({
column,
@@ -14,6 +16,7 @@ export function ColumnHeader<R>({
isPrimaryKey,
isEncrypted,
format,
foreignKey,
}: ColumnHeaderProps<R>) {
const ref = React.useRef<HTMLDivElement>(null)
const dispatch = useDispatch()
@@ -119,7 +122,7 @@ export function ColumnHeader<R>({
<div ref={ref} data-handler-id={handlerId} style={{ opacity }} className="w-full">
<div className={`sb-grid-column-header ${cursor}`}>
<div className="sb-grid-column-header__inner">
{renderColumnIcon(columnType)}
{renderColumnIcon(columnType, { name: column.name as string, foreignKey })}
{isPrimaryKey && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
@@ -169,13 +172,45 @@ export function ColumnHeader<R>({
)
}
function renderColumnIcon(type: ColumnType) {
function renderColumnIcon(
type: ColumnType,
columnMeta: { name?: string; foreignKey?: GridForeignKey }
) {
const { name, foreignKey } = columnMeta
switch (type) {
case 'foreign_key':
return (
<div>
<IconLink size="tiny" strokeWidth={2} />
</div>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconLink size="tiny" strokeWidth={2} />
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<div>
<p className="text-xs text-scale-1100">Foreign key relation:</p>
<div className="flex items-center space-x-1">
<p className="text-xs text-scale-1200">{name}</p>
<IconArrowRight size="tiny" strokeWidth={1.5} />
<p className="text-xs text-scale-1200">
{foreignKey?.targetTableSchema}.{foreignKey?.targetTableName}.
{foreignKey?.targetColumnName}
</p>
</div>
{foreignKey?.deletionAction !== FOREIGN_KEY_DELETION_ACTION.NO_ACTION && (
<p className="text-xs text-scale-1200 mt-1">
On delete: {getForeignKeyDeletionAction(foreignKey?.deletionAction)}
</p>
)}
</div>
</div>
</Tooltip.Content>
</Tooltip.Root>
)
default:
return null
@@ -6,7 +6,7 @@ import { GridProps, SupaRow } from '../../types'
import { useDispatch, useTrackedState } from '../../store'
import RowRenderer from './RowRenderer'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
function rowKeyGetter(row: SupaRow) {
return row?.idx ?? -1
@@ -15,13 +15,27 @@ function rowKeyGetter(row: SupaRow) {
interface IGrid extends GridProps {
rows: any[]
updateRow: (previousRow: any, updatedData: any) => void
onEditForeignKeyColumnValue: (args: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => void
}
// [Joshen] Just for visibility this is causing some hook errors in the browser
export const Grid = memo(
forwardRef<DataGridHandle, IGrid>(
(
{ width, height, containerClass, gridClass, rowClass, rows, updateRow },
{
width,
height,
containerClass,
gridClass,
rowClass,
rows,
updateRow,
onEditForeignKeyColumnValue,
},
ref: React.Ref<DataGridHandle> | undefined
) => {
const dispatch = useDispatch()
@@ -61,6 +75,34 @@ export const Grid = memo(
})
}
const table = state.table
function getColumnForeignKey(columnName: string) {
const foreignKey = table?.columns.find((x) => x.name == columnName)?.foreignKey ?? {}
return Boolean(
foreignKey.targetTableSchema && foreignKey.targetTableName && foreignKey.targetColumnName
)
? foreignKey
: undefined
}
function onRowDoubleClick(row: any, column: any) {
const foreignKey = getColumnForeignKey(column.name)
if (foreignKey) {
onEditForeignKeyColumnValue({
foreignKey: {
target_column_name: foreignKey.targetColumnName!,
target_table_name: foreignKey.targetTableName!,
target_table_schema: foreignKey.targetTableSchema!,
},
row,
column,
})
}
}
if (!columnHeaders || columnHeaders.length == 0) {
return (
<div
@@ -93,6 +135,7 @@ export const Grid = memo(
onRowsChange={onRowsChange}
onSelectedCellChange={onSelectedCellChange}
onSelectedRowsChange={onSelectedRowsChange}
onRowDoubleClick={onRowDoubleClick}
className={gridClass}
rowClass={rowClass}
style={{ height: '100%' }}
@@ -3,7 +3,7 @@ import { useState, ReactNode } from 'react'
import { Button, IconDownload, IconX, IconTrash, Dropdown, IconChevronDown } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore } from 'hooks'
import { checkPermissions, useStore, useUrlState } from 'hooks'
import FilterDropdown from './filter'
import SortPopover from './sort'
import RefreshButton from './RefreshButton'
@@ -61,12 +61,16 @@ const DefaultHeader = ({ table, onAddColumn, onAddRow }: DefaultHeaderProps) =>
// [Joshen] Using this logic to block both column and row creation/update/delete
const canCreateColumns = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
const [{ filter: filters, sort: sorts }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
return (
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<RefreshButton table={table} />
<FilterDropdown />
<SortPopover />
<FilterDropdown table={table} filters={filters as string[]} setParams={setParams} />
<SortPopover table={table} sorts={sorts as string[]} setParams={setParams} />
</div>
{canAddNew && (
<>
@@ -1,17 +1,21 @@
import { FC, useState, useMemo, useCallback } from 'react'
import { 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 { Filter, SupaTable } from 'components/grid/types'
import { FilterOperatorOptions } from './Filter.constants'
const FilterPopover: FC = () => {
const [{ filter: filters }]: any = useUrlState({ arrayKeys: ['filter'] })
export interface FilterPopoverProps {
table: SupaTable
filters: string[]
setParams: ReturnType<typeof useUrlState>[1]
}
const FilterPopover = ({ table, filters, setParams }: FilterPopoverProps) => {
const btnText =
(filters || []).length > 0
? `Filtered by ${filters.length} rule${filters.length > 1 ? 's' : ''}`
@@ -22,7 +26,7 @@ const FilterPopover: FC = () => {
size="large"
align="start"
className="sb-grid-filter-popover"
overlay={<FilterOverlay />}
overlay={<FilterOverlay table={table} filters={filters} setParams={setParams} />}
>
<Button
as="span"
@@ -38,12 +42,12 @@ const FilterPopover: FC = () => {
</Popover>
)
}
export default FilterPopover
const FilterOverlay: FC = () => {
const state = useTrackedState()
export interface FilterOverlayProps extends FilterPopoverProps {}
const [{ filter: filtersFromUrl }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const FilterOverlay = ({ table, filters: filtersFromUrl, setParams }: FilterOverlayProps) => {
const initialFilters = useMemo(
() => formatFilterURLParams((filtersFromUrl as string[]) ?? []),
[filtersFromUrl]
@@ -51,7 +55,7 @@ const FilterOverlay: FC = () => {
const [filters, setFilters] = useState<Filter[]>(initialFilters)
function onAddFilter() {
const column = state.table?.columns[0]?.name
const column = table.columns[0]?.name
if (column) {
setFilters([
@@ -104,6 +108,7 @@ const FilterOverlay: FC = () => {
{filters.map((filter, index) => (
<FilterRow
key={`filter-${filter.column}-${[index]}`}
table={table}
filter={filter}
filterIdx={index}
onChange={onChangeFilter}
@@ -1,24 +1,22 @@
import { FC, memo } from 'react'
import { memo } from 'react'
import { Button, Input, IconChevronDown, IconX } from 'ui'
import { Filter, FilterOperator } from 'components/grid/types'
import { Filter, FilterOperator, SupaTable } from 'components/grid/types'
import { DropdownControl } from 'components/grid/components/common'
import { useTrackedState } from 'components/grid/store'
import { FilterOperatorOptions } from './Filter.constants'
interface Props {
export interface FilterRowProps {
table: SupaTable
filterIdx: number
filter: Filter
onChange: (index: number, filter: Filter) => void
onDelete: (index: number) => void
}
const FilterRow: FC<Props> = ({ filter, filterIdx, onChange, onDelete }) => {
const state = useTrackedState()
const column = state.table?.columns.find((x) => x.name === filter.column)
const FilterRow = ({ table, filter, filterIdx, onChange, onDelete }: FilterRowProps) => {
const column = table.columns.find((x) => x.name === filter.column)
const columnOptions =
state.table?.columns?.map((x) => {
table.columns?.map((x) => {
return { value: x.name, label: x.name, postLabel: x.dataType }
}) || []
@@ -1,24 +1,33 @@
import React, { FC, useState, useMemo, useCallback } from 'react'
import React, { 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 { Sort, SupaTable } from 'components/grid/types'
import { formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
const SortPopover: FC = () => {
const [{ sort: sorts }]: any = useUrlState({ arrayKeys: ['sort'] })
export interface SortPopoverProps {
table: SupaTable
sorts: string[]
setParams: ReturnType<typeof useUrlState>[1]
}
const SortPopover = ({ table, sorts, setParams }: SortPopoverProps) => {
const btnText =
(sorts || []).length > 0
? `Sorted by ${sorts.length} rule${sorts.length > 1 ? 's' : ''}`
: 'Sort'
return (
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<SortOverlay />}>
<Popover
size="large"
align="start"
className="sb-grid-sort-popover"
overlay={<SortOverlay table={table} sorts={sorts} setParams={setParams} />}
>
<Button
as="span"
type={(sorts || []).length > 0 ? 'link' : 'text'}
@@ -33,19 +42,19 @@ const SortPopover: FC = () => {
</Popover>
)
}
export default SortPopover
const SortOverlay: FC = () => {
const state = useTrackedState()
export interface SortOverlayProps extends SortPopoverProps {}
const [{ sort: sortsFromUrl }, setParams] = useUrlState({ arrayKeys: ['sort'] })
const SortOverlay = ({ table, sorts: sortsFromUrl, setParams }: SortOverlayProps) => {
const initialSorts = useMemo(
() => formatSortURLParams((sortsFromUrl as string[]) ?? []),
[sortsFromUrl]
)
const [sorts, setSorts] = useState<Sort[]>(initialSorts)
const columns = state?.table?.columns!.filter((x) => {
const columns = table.columns!.filter((x) => {
const found = sorts.find((y) => y.column == x.name)
return !found
})
@@ -100,6 +109,7 @@ const SortOverlay: FC = () => {
{sorts.map((sort, index) => (
<SortRow
key={sort.column}
table={table}
index={index}
columnName={sort.column}
sort={sort}
@@ -1,12 +1,12 @@
import { FC, useRef, memo } from 'react'
import { useRef, memo } from 'react'
import { Button, IconMenu, Toggle, IconX } from 'ui'
import { XYCoord } from 'dnd-core'
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'
import { DragItem, Sort } from 'components/grid/types'
import { useTrackedState } from 'components/grid/store'
import { DragItem, Sort, SupaTable } from 'components/grid/types'
type SortRowProps = {
export interface SortRowProps {
table: SupaTable
index: number
columnName: string
sort: Sort
@@ -15,10 +15,8 @@ type SortRowProps = {
onDrag: (dragIndex: number, hoverIndex: number) => void
}
const SortRow: FC<SortRowProps> = ({ index, columnName, sort, onDelete, onToggle, onDrag }) => {
const state = useTrackedState()
const column = state?.table?.columns.find((x) => x.name === columnName)
const SortRow = ({ table, index, columnName, sort, onDelete, onToggle, onDrag }: SortRowProps) => {
const column = table.columns.find((x) => x.name === columnName)
if (!column) return null
const ref = useRef<HTMLDivElement>(null)
+8
View File
@@ -55,9 +55,17 @@ export type ColumnType =
| 'time'
| 'unknown'
export interface GridForeignKey {
targetTableSchema?: string | null
targetTableName?: string | null
targetColumnName?: string | null
deletionAction?: string
}
export interface ColumnHeaderProps<R> extends HeaderRendererProps<R> {
columnType: ColumnType
isPrimaryKey: boolean | undefined
isEncrypted: boolean | undefined
format: string
foreignKey?: GridForeignKey
}
+6
View File
@@ -1,3 +1,4 @@
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import React from 'react'
import { Dictionary } from './base'
import { SupaRow, SupaTable } from './table'
@@ -75,6 +76,11 @@ export interface SupabaseGridProps {
refreshDocs: () => void
onExpandJSONEditor: (column: string, row: SupaRow) => void
updateTableRow: (previousRow: any, updatedData: any) => void
onEditForeignKeyColumnValue: (args: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => void
}
export interface SupabaseGridRef {
+2 -4
View File
@@ -1,4 +1,4 @@
import { Dictionary } from './base'
import { Dictionary, GridForeignKey } from './base'
export interface SupaColumn {
readonly dataType: string
@@ -13,9 +13,7 @@ export interface SupaColumn {
readonly isNullable?: boolean
readonly isUpdatable?: boolean
readonly isEncrypted?: boolean
readonly targetTableSchema?: string | null
readonly targetTableName?: string | null
readonly targetColumnName?: string | null
readonly foreignKey?: GridForeignKey
position: number
}
+5 -6
View File
@@ -19,7 +19,6 @@ import {
DateTimeEditor,
DateTimeWithTimezoneEditor,
JsonEditor,
NullableBooleanEditor,
NumberEditor,
SelectEditor,
TextEditor,
@@ -34,7 +33,7 @@ import {
ForeignKeyFormatter,
} from 'components/grid/components/formatter'
const ESTIMATED_CHARACTER_PIXEL_WIDTH = 9
export const ESTIMATED_CHARACTER_PIXEL_WIDTH = 9
export function getGridColumns(
table: SupaTable,
@@ -47,7 +46,6 @@ export function getGridColumns(
): any[] {
const columns = table.columns.map((x, idx) => {
const columnType = getColumnType(x)
const columnDefaultWidth = getColumnDefaultWidth(x)
const columnWidthBasedOnName =
(x.name.length + x.format.length) * ESTIMATED_CHARACTER_PIXEL_WIDTH
@@ -75,6 +73,7 @@ export function getGridColumns(
isPrimaryKey={x.isPrimaryKey}
isEncrypted={x.isEncrypted}
format={x.format}
foreignKey={x.foreignKey}
/>
),
editor: options?.editable
@@ -105,7 +104,7 @@ function getColumnEditor(
switch (columnType) {
case 'boolean': {
return columnDefinition.isNullable ? NullableBooleanEditor : BooleanEditor
return (p: any) => <BooleanEditor {...p} isNullable={columnDefinition.isNullable} />
}
case 'date': {
return DateEditor
@@ -130,7 +129,7 @@ function getColumnEditor(
return NumberEditor
}
case 'text': {
return TextEditor
return (p: any) => <TextEditor {...p} isNullable={columnDefinition.isNullable} />
}
default: {
return undefined
@@ -180,7 +179,7 @@ function getColumnType(columnDef: SupaColumn): ColumnType {
} else return 'unknown'
}
function getColumnDefaultWidth(columnDef: SupaColumn): number {
export function getColumnDefaultWidth(columnDef: SupaColumn): number {
if (isNumericalColumn(columnDef.dataType)) {
return 120
} else if (
+1 -1
View File
@@ -65,6 +65,6 @@ export function isEnumColumn(type: string) {
}
export function isForeignKeyColumn(columnDef: SupaColumn) {
const { targetTableSchema, targetTableName, targetColumnName } = columnDef
const { targetTableSchema, targetTableName, targetColumnName } = columnDef?.foreignKey ?? {}
return !!targetTableSchema && !!targetTableName && !!targetColumnName
}
@@ -12,7 +12,7 @@ const baseDomainRegex =
// iOS deep linking scheme https://benoitpasquier.com/deep-linking-url-scheme-ios/
const appRegex =
/^[a-z0-9]+([.][a-z0-9]+)*:\/(\/[-a-z0-9._~!$&'()*+,;=:@%]+)+(?:\.[a-z]+)*(?::\d+)?(?![^<]*(?:<\/\w+>|\/?>))(.*)?\/?(.)*?$/i
/^[a-z0-9-]+([.][a-z0-9]+)*:\/(\/[-a-z0-9._~!$&'()*+,;=:@%]+)+(?:\.[a-z]+)*(?::\d+)?(?![^<]*(?:<\/\w+>|\/?>))(.*)?\/?(.)*?$/i
// Regex from https://stackoverflow.com/a/18696953/4807782
const localhostRegex = /^(?:^|\s)((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/i
@@ -1,4 +1,4 @@
import { FC, useRef, useState } from 'react'
import { FC, useState } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
@@ -9,7 +9,7 @@ import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice, validateSubscriptionUpdatePayload } from '../Billing.utils'
import PaymentTotal from './PaymentTotal'
import InformationBox from 'components/ui/InformationBox'
import { SubscriptionAddon } from '../AddOns/AddOns.types'
import { AddonPrice, SubscriptionAddon } from '../AddOns/AddOns.types'
import { getPITRDays } from './PaymentSummaryPanel.utils'
import ConfirmPaymentModal from './ConfirmPaymentModal'
import { StripeSubscription } from '../Subscription/Subscription.types'
@@ -88,6 +88,21 @@ const PaymentSummaryPanel: FC<Props> = ({
const [showConfirmModal, setShowConfirmModal] = useState(false)
const selectedPlanCost =
selectedPlan.prices.find(
(price: AddonPrice) => price.id === selectedPlan.metadata.default_price_id
)?.unit_amount ?? 0
const totalSelectedAddonCost = Object.keys(selectedAddons)
.map((productName) => {
const product = (selectedAddons as any)[productName]
const price = product.prices.find(
(price: AddonPrice) => price.id === product.metadata.default_price_id
)
return price
})
.reduce((a, b) => a + b.unit_amount, 0)
const totalMonthlyCost = selectedPlanCost + totalSelectedAddonCost
const currentPITRDays =
currentAddons.pitrDuration !== undefined ? getPITRDays(currentAddons.pitrDuration) : 0
const selectedPITRDays =
@@ -308,6 +323,7 @@ const PaymentSummaryPanel: FC<Props> = ({
<div className="w-full h-px bg-scale-600" />
<PaymentTotal
totalMonthlyCost={totalMonthlyCost / 100}
subscriptionPreview={subscriptionPreview}
isRefreshingPreview={isRefreshingPreview}
isSpendCapEnabled={isSpendCapEnabled}
@@ -5,18 +5,20 @@ import { SubscriptionPreview } from '../Billing.types'
import CostBreakdownModal from './CostBreakdownModal'
interface Props {
totalMonthlyCost: number
subscriptionPreview?: SubscriptionPreview
isRefreshingPreview: boolean
isSpendCapEnabled: boolean
}
const PaymentTotal: FC<Props> = ({
totalMonthlyCost,
subscriptionPreview,
isRefreshingPreview,
isSpendCapEnabled,
}) => {
const hasChanges = subscriptionPreview?.has_changes ?? false
const totalMonthlyCost = (subscriptionPreview?.base_amount_due_next_billing_cycle ?? 0) / 100
// const totalMonthlyCost = (subscriptionPreview?.base_amount_due_next_billing_cycle ?? 0) / 100
const amountDueImmediately = (subscriptionPreview?.amount_due_immediately ?? 0) / 100
const billingDate = new Date((subscriptionPreview?.bill_on ?? 0) * 1000)
const isBillingToday =
@@ -10,6 +10,7 @@ import { makeRandomString } from 'lib/helpers'
import ProjectCard from './ProjectCard'
import ShimmeringCard from './ShimmeringCard'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useOverdueInvoicesQuery } from 'data/invoices/invoices-query'
interface Props {
rewriteHref?: (projectRef: string) => string
@@ -20,6 +21,8 @@ const ProjectList: FC<Props> = ({ rewriteHref }) => {
const { organizations, projects } = app
const { isLoading: isLoadingProjects } = projects
const { data: allOverdueInvoices } = useOverdueInvoicesQuery()
const isLoadingPermissions = IS_PLATFORM ? (ui?.permissions ?? []).length === 0 : false
return (
@@ -32,9 +35,24 @@ const ProjectList: FC<Props> = ({ rewriteHref }) => {
const isEmpty = sortedProjects?.length == 0
const canReadProjects = checkPermissions(PermissionAction.READ, 'projects', undefined, id)
const overdueInvoices = (allOverdueInvoices || []).filter((it) => it.organization_id === id)
return (
<div className="space-y-3" key={makeRandomString(5)}>
<h4 className="text-lg">{name}</h4>
<div className="flex space-x-4 items-center">
<h4 className="text-lg">{name}</h4>
{!!overdueInvoices.length && (
<div>
<Link href={`/org/${slug}/invoices`}>
<a>
<Button type="danger">Outstanding Invoices</Button>
</a>
</Link>
</div>
)}
</div>
{isLoadingPermissions || isLoadingProjects ? (
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<ShimmeringCard />
@@ -45,7 +45,7 @@ const ForgotPasswordForm = () => {
ui.setNotification({
id: toastId,
category: 'success',
message: `Password reset email sent successfully! Please check your email`,
message: `If you registered using your email and password, you will receive a password reset email.`,
})
await router.push('/sign-in')
@@ -63,7 +63,7 @@ const ActionBar: FC<ActionBarProps> = ({
// Does not require applyFunction() callback
<Button
size="small"
disabled={disableApply}
disabled={loading || disableApply}
loading={loading}
htmlType="submit"
form={formId}
@@ -55,7 +55,7 @@ const ColumnDefaultValue: FC<Props> = ({
description="Can either be a literal or an expression (e.g uuid_generate_v4())"
placeholder={
typeof columnFields.defaultValue === 'string' && columnFields.defaultValue.length === 0
? 'Empty string'
? 'EMPTY'
: 'NULL'
}
value={columnFields?.defaultValue ?? ''}
@@ -1,12 +1,11 @@
import Link from 'next/link'
import { FC, useEffect, useState } from 'react'
import { isUndefined, isEmpty } from 'lodash'
import { isEmpty } from 'lodash'
import { Dictionary } from 'components/grid'
import { Checkbox, SidePanel, Input, Button, IconExternalLink, Toggle } from 'ui'
import type {
PostgresColumn,
PostgresExtension,
PostgresRelationship,
PostgresTable,
PostgresType,
} from '@supabase/postgres-meta'
@@ -27,19 +26,26 @@ import {
generateUpdateColumnPayload,
} from './ColumnEditor.utils'
import { TEXT_TYPES } from '../SidePanelEditor.constants'
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types'
import {
ColumnField,
CreateColumnPayload,
ExtendedPostgresRelationship,
UpdateColumnPayload,
} from '../SidePanelEditor.types'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
import { EncryptionKeySelector } from 'components/interfaces/Settings/Vault'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
interface Props {
column?: PostgresColumn
selectedTable: PostgresTable
tables: PostgresTable[]
visible: boolean
closePanel: () => void
saveChanges: (
payload: CreateColumnPayload | UpdateColumnPayload,
foreignKey: Partial<PostgresRelationship> | undefined,
foreignKey: ExtendedPostgresRelationship | undefined,
isNewRecord: boolean,
configuration: {
columnId: string | undefined
@@ -55,7 +61,6 @@ interface Props {
const ColumnEditor: FC<Props> = ({
column,
selectedTable,
tables = [],
visible = false,
closePanel = () => {},
saveChanges = () => {},
@@ -63,31 +68,41 @@ const ColumnEditor: FC<Props> = ({
}) => {
const { ref } = useParams()
const { meta, vault } = useStore()
const { project } = useProjectContext()
const isTCEEnabled = useFlag('transparentColumnEncryption')
const isNewRecord = isUndefined(column)
const originalForeignKey = column ? getColumnForeignKey(column, selectedTable) : undefined
const [errors, setErrors] = useState<Dictionary<any>>({})
const [columnFields, setColumnFields] = useState<ColumnField>()
const [isEditingRelation, setIsEditingRelation] = useState<boolean>(false)
const { data } = useForeignKeyConstraintsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedTable?.schema,
})
const foreignKeyMeta = data || []
const keys = vault.listKeys()
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
const [errors, setErrors] = useState<Dictionary<any>>({})
const [columnFields, setColumnFields] = useState<ColumnField>()
const [isEditingRelation, setIsEditingRelation] = useState<boolean>(false)
const [pgsodiumExtension] = meta.extensions.list(
(ext: PostgresExtension) => ext.name.toLowerCase() === 'pgsodium'
)
const isPgSodiumInstalled = pgsodiumExtension?.installed_version !== null
const isNewRecord = column === undefined
const originalForeignKey = column
? getColumnForeignKey(column, selectedTable, foreignKeyMeta)
: undefined
useEffect(() => {
if (visible) {
setErrors({})
const columnFields = isNewRecord
? { ...generateColumnField(), keyId: keys.length > 0 ? keys[0].id : 'create-new' }
: generateColumnFieldFromPostgresColumn(column!, selectedTable)
: generateColumnFieldFromPostgresColumn(column!, selectedTable, foreignKeyMeta)
setColumnFields(columnFields)
}
}, [visible])
@@ -111,23 +126,27 @@ const ColumnEditor: FC<Props> = ({
setErrors(updatedErrors)
}
const saveColumnForeignKey = (
foreignKeyConfiguration: { table: PostgresTable; column: PostgresColumn } | undefined
) => {
const saveColumnForeignKey = (foreignKeyConfiguration?: {
table: PostgresTable
column: PostgresColumn
deletionAction: string
}) => {
onUpdateField({
foreignKey: !isUndefined(foreignKeyConfiguration)
? {
id: 0,
constraint_name: '',
source_schema: selectedTable.schema,
source_table_name: selectedTable.name,
source_column_name: columnFields?.name || column?.name || '',
target_table_schema: foreignKeyConfiguration.table.schema,
target_table_name: foreignKeyConfiguration.table.name,
target_column_name: foreignKeyConfiguration.column.name,
}
: undefined,
...(!isUndefined(foreignKeyConfiguration) && {
foreignKey:
foreignKeyConfiguration !== undefined
? {
id: 0,
constraint_name: '',
source_schema: selectedTable.schema,
source_table_name: selectedTable.name,
source_column_name: columnFields?.name || column?.name || '',
target_table_schema: foreignKeyConfiguration.table.schema,
target_table_name: foreignKeyConfiguration.table.name,
target_column_name: foreignKeyConfiguration.column.name,
deletion_action: foreignKeyConfiguration.deletionAction,
}
: undefined,
...(foreignKeyConfiguration !== undefined && {
format: foreignKeyConfiguration.column.format,
defaultValue: null,
}),
@@ -180,9 +199,7 @@ const ColumnEditor: FC<Props> = ({
}
onInteractOutside={(event) => {
const isToast = (event.target as Element)?.closest('#toast')
if (isToast) {
event.preventDefault()
}
if (isToast) event.preventDefault()
}}
>
<FormSection header={<FormSectionLabel className="lg:!col-span-4">General</FormSectionLabel>}>
@@ -218,6 +235,7 @@ const ColumnEditor: FC<Props> = ({
originalForeignKey={originalForeignKey}
onSelectEditRelation={() => setIsEditingRelation(true)}
onSelectRemoveRelation={() => onUpdateField({ foreignKey: undefined })}
onSelectCancelRemoveRelation={() => onUpdateField({ foreignKey: originalForeignKey })}
/>
</div>
</FormSectionContent>
@@ -254,10 +272,10 @@ const ColumnEditor: FC<Props> = ({
layout="vertical"
enumTypes={enumTypes}
error={errors.format}
disabled={!isUndefined(columnFields?.foreignKey)}
disabled={columnFields?.foreignKey !== undefined}
onOptionSelect={(format: string) => onUpdateField({ format, defaultValue: null })}
/>
{isUndefined(columnFields.foreignKey) && (
{columnFields.foreignKey === undefined && (
<div className="space-y-4">
{columnFields.format.includes('int') && (
<div className="w-full">
@@ -8,7 +8,14 @@ import type {
} from '@supabase/postgres-meta'
import { uuidv4 } from 'lib/helpers'
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types'
import {
ColumnField,
CreateColumnPayload,
ExtendedPostgresRelationship,
UpdateColumnPayload,
} from '../SidePanelEditor.types'
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
const isSQLExpression = (input: string) => {
if (['CURRENT_DATE'].includes(input)) return true
@@ -49,12 +56,13 @@ export const generateColumnField = (field: any = {}): ColumnField => {
export const generateColumnFieldFromPostgresColumn = (
column: PostgresColumn,
table: PostgresTable
table: PostgresTable,
foreignKeys: ForeignKeyConstraint[]
): ColumnField => {
const { primary_keys } = table
// @ts-ignore
const primaryKeyColumns = primary_keys.map((key) => key.name)
const foreignKey = getColumnForeignKey(column, table)
const foreignKey = getColumnForeignKey(column, table, foreignKeys)
const isArray = column?.data_type === 'ARRAY'
return {
@@ -175,8 +183,8 @@ export const validateFields = (field: ColumnField) => {
}
export const getForeignKeyUIState = (
originalConfig: PostgresRelationship | undefined,
updatedConfig: PostgresRelationship | undefined
originalConfig: ExtendedPostgresRelationship | undefined,
updatedConfig: ExtendedPostgresRelationship | undefined
): 'Info' | 'Add' | 'Remove' | 'Update' => {
if (isUndefined(originalConfig) && !isUndefined(updatedConfig)) {
return 'Add'
@@ -189,7 +197,8 @@ export const getForeignKeyUIState = (
if (
!isEqual(originalConfig?.target_table_schema, updatedConfig?.target_table_schema) ||
!isEqual(originalConfig?.target_table_name, updatedConfig?.target_table_name) ||
!isEqual(originalConfig?.target_column_name, updatedConfig?.target_column_name)
!isEqual(originalConfig?.target_column_name, updatedConfig?.target_column_name) ||
originalConfig?.deletion_action !== updatedConfig?.deletion_action
) {
return 'Update'
}
@@ -197,15 +206,27 @@ export const getForeignKeyUIState = (
return 'Info'
}
export const getColumnForeignKey = (column: PostgresColumn, table: PostgresTable) => {
export const getColumnForeignKey = (
column: PostgresColumn,
table: PostgresTable,
foreignKeys: ForeignKeyConstraint[]
) => {
const { relationships } = table
return find(relationships, (relationship) => {
const foreignKey = find(relationships, (relationship) => {
return (
relationship.source_schema === column.schema &&
relationship.source_table_name === column.table &&
relationship.source_column_name === column.name
)
})
if (foreignKey === undefined) return foreignKey
else {
const foreignKeyMeta = foreignKeys.find((fk) => fk.id === foreignKey.id)
return {
...foreignKey,
deletion_action: foreignKeyMeta?.deletion_action ?? FOREIGN_KEY_DELETION_ACTION.NO_ACTION,
}
}
}
// Assumes arrayString is a stringified array (e.g "[1, 2, 3]")
@@ -213,3 +234,18 @@ const formatArrayToPostgresArray = (arrayString: string) => {
if (!arrayString) return null
return arrayString.replaceAll('[', '{').replaceAll(']', '}')
}
export const getForeignKeyDeletionAction = (deletionAction?: string) => {
switch (deletionAction) {
case FOREIGN_KEY_DELETION_ACTION.CASCADE:
return 'Cascade'
case FOREIGN_KEY_DELETION_ACTION.RESTRICT:
return 'Restrict'
case FOREIGN_KEY_DELETION_ACTION.SET_DEFAULT:
return 'Set default'
case FOREIGN_KEY_DELETION_ACTION.SET_NULL:
return 'Set NULL'
default:
return undefined
}
}
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more