Merge branch 'master' into feat/docs2.0
No files matched your search
@@ -35,8 +35,6 @@ We are in the process of migrating this repository to monorepo, using [Turborepo
|
||||
|
||||
Eventually, all the apps will be run using [Turborepo](https://turborepo.org/docs), which will significantly improve the developer workflow.
|
||||
|
||||
If you are working on the ([docs site](https://supabase.com/docs)), please refer to this [developers guide](https://github.com/supabase/supabase/tree/master/apps/reference/DEVELOPERS.md).
|
||||
|
||||
### Fork the repository
|
||||
|
||||
To contribute code to [Supabase](https://supabase.com), you must fork the [Supabase Repository](https://github.com/supabase/supabase).
|
||||
@@ -71,11 +69,11 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
|
||||
|
||||
Then visit, and edit, any of the following sites:
|
||||
|
||||
| Site | Directory | Scope name | Description | Local development server |
|
||||
| ---------------------------------------------- | -------------- | ---------- | ------------------------------------ | ------------------------ |
|
||||
| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 |
|
||||
| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001 |
|
||||
| Site | Directory | Scope name | Description | Local development server |
|
||||
| ---------------------------------------------- | ------------ | ---------- | ------------------------------------ | -------------------------- |
|
||||
| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 |
|
||||
| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001/docs |
|
||||
|
||||
#### Running sites individually
|
||||
|
||||
|
||||
@@ -245,7 +245,9 @@ export const menuItems: NavMenu = {
|
||||
label: 'Storage',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/storage', items: [] },
|
||||
{ name: 'CDN', url: '/guides/storage-cdn', items: [] },
|
||||
{ name: 'Quickstart', url: '/guides/storage/quickstart', items: [] },
|
||||
{ name: 'Access Control', url: '/guides/storage/access-control', items: [] },
|
||||
{ name: 'CDN', url: '/guides/storage/cdn', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -295,6 +295,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
|
||||
import type { Actions } from './$types'
|
||||
import { invalid, redirect } from '@sveltejs/kit'
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
|
||||
import { AuthApiError } from '@supabase/supabase-js'
|
||||
|
||||
export const actions: Actions = {
|
||||
signin: async (event) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const meta = {
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
The pg_net API is in beta. Functions signatures may change.
|
||||
The pg_net API is in alpha. Functions signatures may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -34,342 +34,6 @@ keep all video files in a "video" bucket, and profile pictures in an "avatar" bu
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Getting started
|
||||
|
||||
This is a quick guide that shows the basic functionality of Supabase Storage. Find a full
|
||||
[example application in GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management),
|
||||
which you can deploy yourself.
|
||||
|
||||
[](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">
|
||||
|
||||
File, Folder, and Bucket names **must follow** [AWS object key naming guidelines](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html) and avoid use of any other characters.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Create a bucket
|
||||
|
||||
You can create a bucket using the Supabase Dashboard.
|
||||
Since the storage is interoperable with your Postgres database, you can also use SQL or our
|
||||
client libraries. Here we create a bucket called "avatars":
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Click **New Bucket** and enter a name for the bucket.
|
||||
3. Click **Create Bucket**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Use Postgres to create a bucket.
|
||||
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
// Use the JS library to create a bucket.
|
||||
|
||||
const { data, error } = await supabase.storage.createBucket('avatars')
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-createbucket)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.createBucket('avatars');
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Upload a file
|
||||
|
||||
You can upload a file from the Dashboard, or within a browser using our JS libraries.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Select the bucket you want to upload the file to.
|
||||
3. Click **Upload File**.
|
||||
4. Select the file you want to upload.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
const avatarFile = event.target.files[0]
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload('public/avatar1.png', avatarFile)
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-from-upload)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Create file `example.txt` and upload it in `public` bucket
|
||||
final file = File('example.txt');
|
||||
file.writeAsStringSync('File content');
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.upload('example.txt', file);
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Download a file
|
||||
|
||||
You can download a file from the Dashboard, or within a browser using our JS libraries.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Select the bucket that contains the file.
|
||||
3. Select the file that you want to download.
|
||||
4. Click **Download**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
// Use the JS library to download a file.
|
||||
|
||||
const { data, error } = await supabase.storage.from('avatars').download('public/avatar1.png')
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-from-download)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.download('example.txt');
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/dart/storage-from-download)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Add security rules
|
||||
|
||||
To restrict access to your files you can use either the Dashboard or SQL.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Click **Policies** in the sidebar.
|
||||
3. Click **Add Policies** in the `OBJECTS` table to add policies for Files. You can also create policies for Buckets.
|
||||
4. Choose whether you want the policy to apply to downloads (SELECT), uploads (INSERT), updates (UPDATE), or deletes (DELETE).
|
||||
5. Give your policy a unique name.
|
||||
6. Write the policy using SQL.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Use SQL to create a policy.
|
||||
|
||||
create policy "Public Access"
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'public' );
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Public and Private Buckets
|
||||
|
||||
Storage buckets are private by default.
|
||||
|
||||
For private buckets, you can access objects via the [download](/docs/reference/javascript/storage-from-download) method. This corresponds to `/object/auth/` API endpoint.
|
||||
Alternatively, you can create a publicly shareable URL with an expiry date using the [createSignedUrl](/docs/reference/javascript/storage-from-createsignedurl) method
|
||||
which calls the `/object/sign/` API.
|
||||
|
||||
For public buckets, you can access the assets directly without a token or an Authorisation header. The [getPublicUrl](/docs/reference/javascript/storage-from-getpublicurl)
|
||||
helper method returns the full public URL for an asset. This calls the `/object/public/` API endpoint internally. While no authorization is required for accessing objects in a public bucket, proper [access control](#access-control) is required for other operations like uploading, deleting objects from public buckets as well.
|
||||
|
||||
Public buckets also tend to have [better performance](/docs/guides/storage-cdn#public-vs-private-buckets).
|
||||
|
||||
<details>
|
||||
<summary>Advanced: reverse proxy</summary>
|
||||
The URLs returned are proxied through the API Proxy. They are prefixed by <code>/storage/v1</code>.
|
||||
|
||||
For example, on the hosted Platform they will be
|
||||
|
||||
<code>https://[project_ref].supabase.co/storage/v1/object/public/[id]</code>
|
||||
|
||||
You can access the storage API directly with the same endpoint. See the <a href="https://supabase.github.io/storage-api">API docs</a> for a full list of operations available.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Access control
|
||||
|
||||
Supabase Storage is integrated with your [Postgres Database](/docs/guides/database).
|
||||
This means that you can use the same [Row Level Security Policies](/docs/guides/auth#policies)
|
||||
for managing access to your files. Supabase Storage stores metadata in the `objects` and `buckets` table in the storage schema. To allow read access to files, the RLS policy must allow users to `SELECT` the `objects` table and for uploading a new object, the RLS policy must grant users access to `INSERT` into the `objects` table and so on. The mapping between the different API calls and the database permissions required is documented in the [Reference docs](/docs/reference/javascript/storage-createbucket).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Access control for Storage is mapped to CRUD operations on the `buckets` and `objects` table via RLS policies.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/4ERX__Y908k"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Helpers
|
||||
|
||||
Supabase Storage provides SQL helper functions which you can use in your database queries and
|
||||
policies.
|
||||
|
||||
---
|
||||
|
||||
#### `storage.filename()`
|
||||
|
||||
Returns the name of a file.
|
||||
|
||||
```sql
|
||||
select storage.filename(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`'avatar.png'`
|
||||
|
||||
---
|
||||
|
||||
#### `storage.foldername()`
|
||||
|
||||
Returns an array path, with all of the subfolders that a file belongs to.
|
||||
|
||||
```sql
|
||||
select storage.foldername(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`[ 'public', 'subfolder' ]`
|
||||
|
||||
---
|
||||
|
||||
#### `storage.extension()`
|
||||
|
||||
Returns the extension of a file.
|
||||
|
||||
```sql
|
||||
select storage.extension(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`'png'`
|
||||
|
||||
---
|
||||
|
||||
## Policy examples
|
||||
|
||||
Here are some examples of storage policies.
|
||||
|
||||
### Allow public access to a bucket
|
||||
|
||||
```sql
|
||||
-- 1. Allow public access to any files in the "public" bucket
|
||||
create policy "Public Access"
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'public' );
|
||||
```
|
||||
|
||||
### Allow logged-in access to a bucket
|
||||
|
||||
```sql
|
||||
-- 1. Allow logged-in access to any files in the "restricted" bucket
|
||||
create policy "Restricted Access"
|
||||
on storage.objects for select
|
||||
using (
|
||||
bucket_id = 'restricted'
|
||||
and auth.role() = 'authenticated'
|
||||
);
|
||||
```
|
||||
|
||||
### Allow individual access to a file
|
||||
|
||||
```sql
|
||||
-- 1. Allow a user to access their own files
|
||||
create policy "Individual user Access"
|
||||
on storage.objects for select
|
||||
using ( auth.uid() = owner );
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- Read more about Supabase Storage in the [blog post](https://supabase.com/blog/supabase-storage)
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'storage-access-control',
|
||||
title: 'Access Control',
|
||||
description: 'Manage who can access your Supabase Storage files.',
|
||||
}
|
||||
|
||||
Supabase Storage is integrated with your [Postgres Database](/docs/guides/database).
|
||||
This means that you can use the same [Row Level Security Policies](/docs/guides/auth#policies)
|
||||
for managing access to your files. Supabase Storage stores metadata in the `objects` and `buckets` table in the storage schema.
|
||||
|
||||
To allow read access to files, the RLS policy must allow users to `SELECT` the `objects` table and for uploading a new object, the RLS policy must grant users access to `INSERT` into the `objects` table and so on. The mapping between the different API calls and the database permissions required is documented in the [Reference docs](/docs/reference/javascript/storage-createbucket).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Access control for Storage is mapped to CRUD operations on the `buckets` and `objects` table via RLS policies.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/4ERX__Y908k"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Public and private buckets
|
||||
|
||||
Storage buckets are private by default. For private buckets, you can access objects via the [download](/docs/reference/javascript/storage-from-download) method. This corresponds to `/object/auth/` API endpoint.
|
||||
Alternatively, you can create a publicly shareable URL with an expiry date using the [createSignedUrl](/docs/reference/javascript/storage-from-createsignedurl) method
|
||||
which calls the `/object/sign/` API.
|
||||
|
||||
For public buckets, you can access the assets directly without a token or an Authorisation header. The [getPublicUrl](/docs/reference/javascript/storage-from-getpublicurl)
|
||||
helper method returns the full public URL for an asset. This calls the `/object/public/` API endpoint internally. While no authorization is required for accessing objects in a public bucket, proper [access control](#access-control) is required for other operations like uploading, deleting objects from public buckets as well. Public buckets also tend to have [better performance](/docs/guides/storage-cdn#public-vs-private-buckets).
|
||||
|
||||
<details>
|
||||
<summary>Advanced: reverse proxy</summary>
|
||||
The URLs returned are proxied through the API Proxy. They are prefixed by <code>/storage/v1</code>.
|
||||
|
||||
For example, on the hosted Platform they will be
|
||||
|
||||
<code>https://[project_ref].supabase.co/storage/v1/object/public/[id]</code>
|
||||
|
||||
You can access the storage API directly with the same endpoint. See the <a href="https://supabase.github.io/storage-api">API docs</a> for a full list of operations available.
|
||||
|
||||
</details>
|
||||
|
||||
## Helper functions
|
||||
|
||||
Supabase Storage provides SQL helper functions which you can use in your database queries and
|
||||
policies.
|
||||
|
||||
---
|
||||
|
||||
#### `storage.filename()`
|
||||
|
||||
Returns the name of a file.
|
||||
|
||||
```sql
|
||||
select storage.filename(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`'avatar.png'`
|
||||
|
||||
---
|
||||
|
||||
#### `storage.foldername()`
|
||||
|
||||
Returns an array path, with all of the subfolders that a file belongs to.
|
||||
|
||||
```sql
|
||||
select storage.foldername(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`[ 'public', 'subfolder' ]`
|
||||
|
||||
---
|
||||
|
||||
#### `storage.extension()`
|
||||
|
||||
Returns the extension of a file.
|
||||
|
||||
```sql
|
||||
select storage.extension(name)
|
||||
from storage.objects;
|
||||
```
|
||||
|
||||
For example, if your file is stored in `public/subfolder/avatar.png` it would return:
|
||||
|
||||
`'png'`
|
||||
|
||||
---
|
||||
|
||||
## Policy examples
|
||||
|
||||
### Allow public access to a bucket
|
||||
|
||||
```sql
|
||||
-- 1. Allow public access to any files in the "public" bucket
|
||||
create policy "Public Access"
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'public' );
|
||||
```
|
||||
|
||||
### Allow logged-in access to a bucket
|
||||
|
||||
```sql
|
||||
-- 1. Allow logged-in access to any files in the "restricted" bucket
|
||||
create policy "Restricted Access"
|
||||
on storage.objects for select
|
||||
using (
|
||||
bucket_id = 'restricted'
|
||||
and auth.role() = 'authenticated'
|
||||
);
|
||||
```
|
||||
|
||||
### Allow individual access to a file
|
||||
|
||||
```sql
|
||||
-- 1. Allow a user to access their own files
|
||||
create policy "Individual user Access"
|
||||
on storage.objects for select
|
||||
using ( auth.uid() = owner );
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -3,27 +3,19 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'storage-cdn',
|
||||
title: 'Storage CDN',
|
||||
description: 'How Supabase Storage caches objects with a CDN',
|
||||
description: 'Learn how Supabase Storage caches objects with a CDN.',
|
||||
sidebar_label: 'CDN',
|
||||
}
|
||||
|
||||
## CDN basics
|
||||
All assets uploaded to Supabase Storage are cached on a Content Delivery Network (CDN) to improve the latency for users all around the world. CDNs are a geographically distributed set of servers or **nodes** which caches content from an **origin server**. For Supabase Storage, the origin is the storage server running in the [same region as your project](https://app.supabase.com/project/_/settings/general). Aside from performance, CDNs also help with security and availability by mitigating Distributed Denial of Service and other application attacks.
|
||||
|
||||
All assets uploaded to storage are cached on a Content Delivery Network (CDN).
|
||||
|
||||
To improve the latency for users all around the world, Supabase uses a CDN. CDNs are a geographically distributed set of servers or **nodes** which caches content from an **origin server**. For Supabase Storage, the origin is the storage server running in the [same region as your project](https://app.supabase.com/project/_/settings/general).
|
||||
## Example
|
||||
|
||||
Let’s walk through an example of how a CDN helps with performance. A new bucket is created for a Supabase project launched in Singapore. All requests to the Supabase Storage API first hit the CDN. A user from the United States requests an object and is routed to the U.S. CDN. At this point, that CDN node does not have the object in its cache and pings the origin server in Singapore. Another user, also in the United States, requests the same object and is served directly from the CDN cache in the United States instead of routing the request back to Singapore.
|
||||
|
||||
Aside from performance, CDNs also help with security and availability by mitigating Distributed Denial of Service and other application attacks.
|
||||
|
||||
## Cache duration
|
||||
|
||||
By default, assets are cached both in the CDN and in the user's browser for 1 hour. After this, the CDN nodes ping the storage server to see if an object has been updated.
|
||||
|
||||
You can modify this cache time when you are [uploading](/docs/reference/javascript/storage-from-upload) or [updating](/docs/reference/javascript/storage-from-update) an object by modifying the `cacheControl` parameter.
|
||||
|
||||
If you expect the object to not change at a given URL, setting a longer cache duration is preferable.
|
||||
By default, assets are cached both in the CDN and in the user's browser for 1 hour. After this, the CDN nodes ping the storage server to see if an object has been updated. You can modify this cache time when you are [uploading](/docs/reference/javascript/storage-from-upload) or [updating](/docs/reference/javascript/storage-from-update) an object by modifying the `cacheControl` parameter. If you expect the object to not change at a given URL, setting a longer cache duration is preferable.
|
||||
|
||||
If you need to update the version of the object stored in the CDN, there are various cache-busting techniques you can use. The most common way to do this is to add a version query parameter in the URL. For example, you can use a URL like `/storage/v1/object/sign/profile-pictures/cat.jpg?token=eyJh...&version=1` in your applications and set a long cache time of 1 year. When you want to update the cat picture, you can increment the version query parameter in the URL. The CDN will treat `/storage/v1/object/sign/profile-pictures/cat.jpg?token=eyJh...&version=2` as a new object and pings the origin for the updated version.
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'storage-quickstart',
|
||||
title: 'Storage Quickstart',
|
||||
description: 'Learn how to use Supabase to store and serve files.',
|
||||
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) you can deploy yourself.
|
||||
|
||||
[](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">
|
||||
|
||||
File, Folder, and Bucket names **must follow** [AWS object key naming guidelines](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html) and avoid use of any other characters.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Create a bucket
|
||||
|
||||
You can create a bucket using the Supabase Dashboard.
|
||||
Since the storage is interoperable with your Postgres database, you can also use SQL or our
|
||||
client libraries. Here we create a bucket called "avatars":
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Click **New Bucket** and enter a name for the bucket.
|
||||
3. Click **Create Bucket**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Use Postgres to create a bucket.
|
||||
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
// Use the JS library to create a bucket.
|
||||
|
||||
const { data, error } = await supabase.storage.createBucket('avatars')
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-createbucket)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.createBucket('avatars');
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Upload a file
|
||||
|
||||
You can upload a file from the Dashboard, or within a browser using our JS libraries.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Select the bucket you want to upload the file to.
|
||||
3. Click **Upload File**.
|
||||
4. Select the file you want to upload.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
const avatarFile = event.target.files[0]
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload('public/avatar1.png', avatarFile)
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-from-upload)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Create file `example.txt` and upload it in `public` bucket
|
||||
final file = File('example.txt');
|
||||
file.writeAsStringSync('File content');
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.upload('example.txt', file);
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](https://pub.dev/documentation/storage_client/latest/storage_client/SupabaseStorageClient/createBucket.html)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Download a file
|
||||
|
||||
You can download a file from the Dashboard, or within a browser using our JS libraries.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Select the bucket that contains the file.
|
||||
3. Select the file that you want to download.
|
||||
4. Click **Download**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="javascript" label="JavaScript">
|
||||
|
||||
```js
|
||||
// Use the JS library to download a file.
|
||||
|
||||
const { data, error } = await supabase.storage.from('avatars').download('public/avatar1.png')
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/javascript/storage-from-download)
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.download('example.txt');
|
||||
}
|
||||
```
|
||||
|
||||
[Reference.](/docs/reference/dart/storage-from-download)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Add security rules
|
||||
|
||||
To restrict access to your files you can use either the Dashboard or SQL.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Storage](https://app.supabase.com/project/_/storage/buckets) page in the Dashboard.
|
||||
2. Click **Policies** in the sidebar.
|
||||
3. Click **Add Policies** in the `OBJECTS` table to add policies for Files. You can also create policies for Buckets.
|
||||
4. Choose whether you want the policy to apply to downloads (SELECT), uploads (INSERT), updates (UPDATE), or deletes (DELETE).
|
||||
5. Give your policy a unique name.
|
||||
6. Write the policy using SQL.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Use SQL to create a policy.
|
||||
|
||||
create policy "Public Access"
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'public' );
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## See also
|
||||
|
||||
- [Supabase Storage on GitHub](https://github.com/supabase/storage-api)
|
||||
- [Swagger API documentation](https://supabase.github.io/storage-api)
|
||||
- Official [JavaScript](/docs/reference/javascript/storage-createbucket) and [Dart](/docs/reference/dart/storage-createbucket) documentation
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -29,6 +29,7 @@ Jon M
|
||||
Jonny Summers-Muir
|
||||
Joshen Lim
|
||||
Kang Ming Tay
|
||||
Karlo Ison
|
||||
Kevin Grüneberg
|
||||
Lakshmipathi G
|
||||
Laura C
|
||||
|
||||
@@ -153,7 +153,7 @@ Notice also how there is a `DEFAULT` value for the `key_id`. In a way, this give
|
||||
|
||||
### One Key ID per Row with Nonce Support
|
||||
|
||||
The default cryptographic algorithm for the above approach uses a _nonceless_ encryption algorithm called `[crypto_aead_det_xchacha20()](https://github.com/jedisct1/libsodium-xchacha20-siv)`. This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
|
||||
The default cryptographic algorithm for the above approach uses a _nonceless_ encryption algorithm called [`crypto_aead_det_xchacha20()`](https://github.com/jedisct1/libsodium-xchacha20-siv). This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
|
||||
|
||||
Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it _does_ have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes.
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: The Supabase Content Storm
|
||||
description: We worked with +30 content creators to drop a mountain of content simultaneously.
|
||||
author: ramiro
|
||||
image: 2022-12-06-content-storm/content-storm.jpg
|
||||
thumb: 2022-12-06-content-storm/content-storm.jpg
|
||||
tags:
|
||||
- announcements
|
||||
date: '2022-12-06'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
We're always trying to innovate at Supabase, so when we started planning [Launch Week 6](https://supabase.com/launch-week), we brainstormed ideas for a campaign to promote it that would involve the community. We took a bit of inspiration from things like Weezer’s YouTube takeover and came up with the concept of a Content Storm ⛈.
|
||||
|
||||
## What is the Content Storm?
|
||||
|
||||
The idea was to get many different external content creators as possible to create Supabase-related content and *publish everything on the same day*. We contacted some well known Youtubers, community members, makers building with Supabase, the SupaSquad, and generally anyone within the Supabase community. We wanted this to be an opportunity for them to build their personal brand or promote their products and startups.
|
||||
|
||||
## The aftermath of the storm
|
||||
|
||||
More than 30 creators participated in creating content in different formats and everything was published today (Tuesday 6th of December). Here is the full list of every piece of content and its author:
|
||||
|
||||
- [A quick look at Supabase](https://twitter.com/Rorstro/status/1600047388852293632) by Ania Kubów.
|
||||
- [Live streaming: Supabase and Sveltekit](https://www.youtube.com/@JamesQQuick/streams) by James Q Quick.
|
||||
- [Perform Database Migration Using Github Actions & Supabase CLI](https://www.youtube.com/watch?v=iCkdtXSeq7A) by Kunal Kushwaha.
|
||||
- [Supabase Authentication & Realtime Database with Flutter](https://www.youtube.com/watch?v=UTSSYPOd5A0) by Pooja Bhaumik.
|
||||
- [Why I Switched From AWS Cognito To Supabase The Week Before My Startup Launched](https://medium.com/@sleeplessfox/why-i-switched-from-aws-cognito-to-supabase-the-week-before-my-startup-launched-67fdc1eccb8c) by Danilo à Tellinghusen.
|
||||
- [Supabase Crash Course For Python Developers](https://www.youtube.com/watch?v=M6cfT2pqpSc) by Patrick Loeber.
|
||||
- [Hoppscotch with GraphQL](https://www.youtube.com/watch?v=HoBXQng3aK4) by Jamie Barton.
|
||||
- [Supabase with TypeScript: using tRPC and Prisma to achieve end-to-end typesafety](https://noahflk.com/blog/supabase-typescript-trpc) by Noah.
|
||||
- [How to Connect Browser Extensions to Supabase](https://akoskm.com/how-to-connect-browser-extensions-to-supabase) by Ákos Kőműves.
|
||||
- [A Chili Cookoff with Rust, Rocket, Render, and Supabase](bradcypert.com/chili-cookoff-with-rust-rocket-render-and-supabase/) by Brad Cypert.
|
||||
- [Authenticating users with Remix and Supabase](https://makerkit.dev/blog/tutorials/remix-supabase-auth) by Giancarlo Buomprisco.
|
||||
- [Automatic Spotify Playlist Creation with Pipedream and Supabase](https://www.ianwootten.co.uk/2022/12/06/automatic-spotify-playlist-creation-with-pipedream-and-supabase/) by Ian Wootten.
|
||||
- [Accept Payments For Your African-Based Business Using Supabase Edge Functions and Paystack](https://blog.hijabicoder.dev/accept-payments-for-your-african-based-business-using-supabase-edge-functions-and-paystack) by Fatuma Abdullahi.
|
||||
- [Creating AvatarAI.me using Supabase and Cerebrium](https://blog.cerebrium.ai/creating-avatarai-me-using-supabase-and-cerebrium-1bf69440c4a7) by Cerebrium.
|
||||
- [From Zero to Hero: Building Next.js Projects](https://www.youtube.com/watch?v=LR1i_8QjXyY) by Gui Bibeau.
|
||||
- [Set up a monorepo with Supabase and Turborepo](https://philipp.steinroetter.com/posts/supabase-turborepo) by Philipp Steinrötter.
|
||||
- [Twitter clone with Supabase](https://www.youtube.com/watch?v=JnUzV-PpopU) by Code with Guillaume.
|
||||
- [How Supabase Powers Mockury](https://blog.ikirill.com/how-supabase-powers-mockury) by Kirill Inoz.
|
||||
- [How to add SIGN-IN WITH APPLE using IONIC ANGULAR and SUPABASE](https://www.youtube.com/watch?v=xhzVyso-9OM) by [Simon Grimm].
|
||||
- [SvelteKit + Supabase v2 Auth with Magic Links, Providers, Email + Password, & more! 🔐🔴 LIVE](https://www.youtube.com/watch?v=XuyMr4pM4B8) by Johnny Magrippis
|
||||
- [How we use Supabase at Brick](https://dev.to/vinzent/how-we-use-supabase-at-brick-3a5d) by Vinzent.
|
||||
- [How Kuiq is using Supabase and serving around 100 APIs](https://kuiq.io/blog/how-kuiq-is-using-supabase-and-serving-around-100-apis) by Taishi
|
||||
- [How I created an interactive website with React, Chakra UI + Supabase](https://medium.com/@limjoshen/how-i-created-a-personal-website-with-react-chakra-ui-supabase-39661852746a) by Joshen Lim.
|
||||
- [5 benefits of using Supabase for your SaaS](https://supastarter.dev/blog/five-benefits-of-supabase) by Jonathan Wilke.
|
||||
- [Getting into the holiday spirit with Astro, React, and Supabase](https://www.aleksandra.codes/astro-supabase) by Aleksandra
|
||||
- [How I build register and login page with Supabase](https://surajondev.com/2022/12/06/how-i-build-register-and-login-page-using-supabase/) by Suraj Vishwakarma.
|
||||
- [What the hell is Supabase?](https://www.youtube.com/watch?v=syOnFdFC_5U) by Ayesha Sahar.
|
||||
- [Comment utiliser Supabase en No-Code avec Weweb ?](https://www.youtube.com/watch?v=g2TLd8vvs0E) by Thomas Buyle.
|
||||
- [How I used Supabase and n8n for my background tasks](https://mathias.rocks/blog/2022-12-06-how-I-used-supabase/) by Mathias Mitchel
|
||||
- [Build An Image Gallery With Supabase Storage and React (Upload Images with Supabase)](https://www.youtube.com/watch?v=8tfdY0Sf2rA) by Coopercodes
|
||||
- [Twitter thread showing how to build a web app](https://twitter.com/thisisisheanesu/status/1600257146792181766) by Isheanesu.
|
||||
- [This service will run your entire app](https://www.instagram.com/reel/Cl1KWbtDGMR/) by Simon Baker (@allthecode).
|
||||
|
||||
It's a lot! There are tons of great resources, going through everything would take a couple of days, so we recommend bookmarking this page! If you like the content, make sure to give feedback to the author and follow them on social.
|
||||
|
||||
And don't miss Launch Week 6, we've been working hard to make it our best Launch Week yet 🚀. Get your tickets [here](https://supabase.com/launch-week).
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Supabase Beta November 2022
|
||||
description: We are preparing everything for Launch Week 6, but we still had time to ship some goodies this month!
|
||||
author: ant_wilson
|
||||
image: 2022-12-07-november-beta-update/monthly-update-november-2022.jpg
|
||||
thumb: 2022-12-07-november-beta-update/monthly-update-november-2022.jpg
|
||||
tags:
|
||||
- release-notes
|
||||
date: '2022-12-07'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Launch Week 6 is just around the corner! We’re saving most of November’s updated as a surprise for Launch Week, but we still had time to ship some goodies this month. Read on!
|
||||
|
||||
## Launch Week 6 tickets
|
||||
|
||||

|
||||
|
||||
Next week, we go all out for LW6. It’s 5 days of shipping, including major features requested by the community. You don’t want to miss a thing, so make sure to claim your free ticket (and you might win some very special SupaSwag).
|
||||
|
||||
[Get your ticket](https://supabase.com/launch-week)
|
||||
|
||||
## Remix Auth Helpers
|
||||
|
||||

|
||||
|
||||
It’s here! The much-awaited Remix Auth Helpers make server-side auth even easier and with a better experience for server-side rendering. Up to date with supabase-js V2 and can be used with Typescript.
|
||||
|
||||
[Read the docs](https://supabase.com/docs/guides/auth/auth-helpers/remix)
|
||||
|
||||
## Edgy Edge Functions
|
||||
|
||||

|
||||
|
||||
We launched a new YouTube series, [Edgy Edge Functions](https://www.youtube.com/playlist?list=PL5S4mPUpp4OulD3olUW8Eq1IYKpUbk5Ob), where we take a deep look at a new function every week.
|
||||
|
||||
Plus, we published three new functions examples: [Generate OG Images](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/opengraph), [Build API servers using Oak](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server), and [Generate Screenshots using Puppeteer](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/puppeteer).
|
||||
|
||||
## Quick product updates
|
||||
|
||||
- Fixed two issues with the `supabase.auth.signOut` function: [cookies not clearing properly](https://github.com/supabase/gotrue/pull/830) and [session not removed](https://github.com/supabase/gotrue-js/pull/541).
|
||||
|
||||
- You can now customize magic links by providing access to the `HashedToken`. [PR](https://github.com/supabase/gotrue/pull/707)
|
||||
|
||||
- Now you can do redirects from your functions. [PR](https://github.com/supabase/supabase/issues/10325)
|
||||
|
||||
- Conda support (`conda install -c conda-forge supabase` and everything else in py)
|
||||
|
||||
## Content storm ⛈
|
||||
|
||||

|
||||
|
||||
As a prelude to Launch Week 6, we invited dozens of developers from around the world to create content in different formats and publish everything simultaneously!
|
||||
|
||||
[Check the full list](https://supabase.com/blog/the-supabase-content-storm)
|
||||
|
||||
## Fireship’s Supabase course
|
||||
|
||||

|
||||
|
||||
That guy from the [Supaship Youtube](https://www.youtube.com/@Supaship6000) channel made a Supabase Course with Fireship! Zack DeRose did an amazing job with this project-based course that builds a Reddit-inspired web app from scratch with Supabase, PostgreSQL, and React.
|
||||
|
||||
[Get it while it's hot](https://fireship.io/courses/supabase/).
|
||||
|
||||
## Made with Supabase
|
||||
|
||||

|
||||
|
||||
[postedbyAI](https://postedby.ai/) | Allows users to create personalized postcards using advanced AI technology. Built with Vercel, OpenAI, and Supabase.
|
||||
|
||||
Discover other great projects: [Made with Supabase](https://www.madewithsupabase.com/).
|
||||
|
||||
## Extended Community Highlights
|
||||
|
||||

|
||||
|
||||
- Supabase in 100 Seconds, by Fireship. [Video](https://www.youtube.com/watch)
|
||||
- NextAuth Supabase Adapter. [Docs](https://next-auth.js.org/adapters/supabase)
|
||||
- Flutter Authentication and Authorization with RLS. [Tutorial](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
|
||||
- The pros and cons of using PostgREST. [Podcast](https://postgres.fm/episodes/postgrest)
|
||||
- Querying Supabase from the edge with Netlify and PolyScale. [Tutorial](https://www.netlify.com/blog/querying-supabase-from-the-edge-with-polyscale/)
|
||||
- JWT Authentication with Supabase and Zuplo for your API. [Blog](https://zuplo.com/blog/2022/11/15/api-authentication-with-supabase-jwt/) and [Video](https://www.youtube.com/watch?v=UEeSZkV7o_Y&feature=youtu.be)
|
||||
- Shipping a public API backed by Supabase. [Blog](https://zuplo.com/blog/2022/11/18/shipping-a-public-api-backed-by-supabase) and [Video](https://www.youtube.com/watch?v=GJSkbxMnWxE)
|
||||
- A Guide to testing on Supabase using pgTAP. [Guide](https://usebasejump.com/blog/testing-on-supabase-with-pgtap)
|
||||
- [Using Bytebase to manage the database development lifecycle for Supabase databases](https://www.bytebase.com/docs/how-to/integrations/supabase)
|
||||
- The Best Backend as a Service for your React App. [Article](https://www.freecodecamp.org/news/best-backend-service-react/)
|
||||
- All You Need to Know About Serverless Functions and the Edge. [Article](https://dev.to/nefejames/all-you-need-to-know-about-serverless-functions-and-the-edge-2bl2)
|
||||
- #1 Hack for Building JavaScript Portfolio Projects. [Video](https://www.youtube.com/watch?v=3K9VUT7H5sw)
|
||||
- Livestreaming - Job Board App w/ SvelteKit, Supabase, & Prisma. [Youtube Playlist](https://www.youtube.com/playlist?list=PLq30BP0TIcqU8SlzDK2wX4IaXamQ26kpr)
|
||||
- How to connect to and insert data into Supabase with Pipedream. [Article](https://pipedream.com/blog/how-to-connect-to-and-insert-data-into-supabase-with-pipedream/) and [Video](https://www.youtube.com/watch?v=l9B8F1n3duk)
|
||||
- Google Sign-In using Supabase and React Native (Expo). [Tutorial](https://dev.to/fedorish/google-sign-in-using-supabase-and-react-native-expo-14jf)
|
||||
- Fast and Intuitive analytics for Supabase with DB Zaps. [Video](https://www.youtube.com/watch?v=QlIlqm9eG3w)
|
||||
- ReactJS + Supabase Fullstack CRUD Application Development. [Youtube Playlist](https://youtube.com/playlist?list=PLl6EcvA_AoxEFqwT_rNs1uZnnf7LSEoji)
|
||||
- How to connect a ESP8266 to a Streamlit App using Supabase database. [Video](https://www.youtube.com/watch?v=wI209WK3-VM)
|
||||
|
||||
|
||||
|
||||
## Meme Zone
|
||||
|
||||
This month’s meme comes courtesy of Fireship. [Follow us on Twitter](https://twitter.com/supabase) for more.
|
||||
|
||||

|
||||
|
||||
See you next month!
|
||||
@@ -30,7 +30,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'GitHub',
|
||||
stat: '40,000+',
|
||||
stat: '42,000+',
|
||||
statLabel: 'GitHub stars',
|
||||
img: 'github.png',
|
||||
invertImgDarkMode: true,
|
||||
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
stat: '44,000+',
|
||||
stat: '50,000+',
|
||||
statLabel: 'Followers',
|
||||
img: 'twitter.png',
|
||||
detail: () => (
|
||||
@@ -53,7 +53,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'Discord',
|
||||
stat: '8,000+',
|
||||
stat: '10,000+',
|
||||
statLabel: 'SupaTroopers',
|
||||
img: 'discord.png',
|
||||
detail: () => (
|
||||
|
||||
@@ -229,5 +229,12 @@
|
||||
"position": "Postgres Engineer",
|
||||
"author_url": "https://github.com/pashkinelfe",
|
||||
"author_image_url": "https://avatars.githubusercontent.com/u/63344111?v=4"
|
||||
},
|
||||
{
|
||||
"author_id": "ramiro",
|
||||
"author": "Ramiro Nunez Dosio",
|
||||
"position": "Growth Marketer",
|
||||
"author_url": "https://twitter.com/ramiro__nd",
|
||||
"author_image_url": "https://avatars.githubusercontent.com/u/20600542?v=4"
|
||||
}
|
||||
]
|
||||
@@ -30,20 +30,13 @@ export default function mdxComponents(type?: 'blog' | 'lp' | undefined) {
|
||||
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(' ')}
|
||||
>
|
||||
<span className={['next-image--dynamic-fill'].join(' ')}>
|
||||
<Image
|
||||
{...props}
|
||||
className={['next-image--dynamic-fill', type === 'blog' && 'rounded-md border'].join(
|
||||
' '
|
||||
)}
|
||||
className={[type === 'blog' ? 'rounded-md border' : ''].join(' ')}
|
||||
layout="fill"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <img {...props} />
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = [
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/storage-caching',
|
||||
destination: '/docs/guides/storage-cdn',
|
||||
destination: '/docs/guides/storage/cdn',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1560,6 +1560,11 @@ module.exports = [
|
||||
source: '/forgot-password',
|
||||
destination: 'https://app.supabase.com/forgot-password',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/storage-cdn',
|
||||
destination: '/docs/guides/storage/cdn',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/functions/examples',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextSeo } from 'next-seo'
|
||||
import _days from '~/components/LaunchWeek/days.json'
|
||||
import { WeekDayProps } from '~/components/LaunchWeek/types'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
|
||||
@@ -13,7 +12,20 @@ import classNames from 'classnames'
|
||||
import styleUtils from '~/components/LaunchWeek/Ticket/utils.module.css'
|
||||
import { SITE_ORIGIN } from '~/lib/constants'
|
||||
|
||||
const days = _days as WeekDayProps[]
|
||||
import { Badge, IconExternalLink } from '~/../../packages/ui'
|
||||
|
||||
const constellation = [
|
||||
[60, 8],
|
||||
[13, 20],
|
||||
[42, 24],
|
||||
[68, 27],
|
||||
[23, 42],
|
||||
[52, 52],
|
||||
[0, 55],
|
||||
[33, 65],
|
||||
[66, 70],
|
||||
[55, 82],
|
||||
]
|
||||
|
||||
export default function launchweek() {
|
||||
const { isDarkMode } = useTheme()
|
||||
@@ -25,6 +37,8 @@ export default function launchweek() {
|
||||
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
|
||||
)
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [creators, setCreators] = useState<any>([])
|
||||
const [activeCreator, setActiveCreator] = useState<any>(null)
|
||||
const { query } = useRouter()
|
||||
const ticketNumber = query.ticketNumber?.toString()
|
||||
const defaultUserData = {
|
||||
@@ -38,12 +52,36 @@ export default function launchweek() {
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
getCreators()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = isDarkMode ? 'dark bg-[#121212]' : 'light bg-[#fff]'
|
||||
}, [isDarkMode])
|
||||
|
||||
async function getCreators() {
|
||||
try {
|
||||
// setLoading(true)
|
||||
let supa = await supabase.from('lw6_creators').select()
|
||||
|
||||
let { data, error, status } = supa
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setCreators(data)
|
||||
}
|
||||
} catch (error) {
|
||||
// alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
// setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
@@ -60,7 +98,7 @@ export default function launchweek() {
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<SectionContainer className="flex flex-col !pb-24 items-center lg:pt-32 gap-32">
|
||||
<SectionContainer className="flex flex-col !pb-1 items-center lg:pt-32 gap-32">
|
||||
<div
|
||||
className={classNames(
|
||||
styleUtils.appear,
|
||||
@@ -116,6 +154,75 @@ export default function launchweek() {
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<SectionContainer
|
||||
className={classNames(
|
||||
'flex gap-6 min-h-[350px] !py-3 mb-11 flex-col-reverse md:flex-row',
|
||||
styleUtils.appear,
|
||||
styleUtils['appear-third']
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 bg-[url('/images/launchweek/orbit.svg')] bg-auto bg-no-repeat bg-bottom relative min-h-[360px]`} //grid grid-cols-5 grid-rows-5
|
||||
>
|
||||
{creators.map((creator: any, index: number) => {
|
||||
return (
|
||||
<div
|
||||
className={`justify-self-center absolute overflow-visible`}
|
||||
onMouseEnter={() => {
|
||||
setActiveCreator(index)
|
||||
}}
|
||||
style={{
|
||||
top: `${constellation[index][0]}%`,
|
||||
left: `${constellation[index][1]}%`,
|
||||
}}
|
||||
>
|
||||
<div className="relative group">
|
||||
<div className="absolute -inset-0.5 bg-brand-1100 rounded-full opacity-75 group-hover:opacity-100 group-hover:blur-sm transition duration-500"></div>
|
||||
<a href={creator.link} target="_blank" rel="noopener">
|
||||
<img
|
||||
className="relative rounded-full w-12 h-12 border border-brand-900 hover:shadow-md"
|
||||
src={creator.profile_picture}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Badge className="mb-6 bg-gradient-to-r from-[#0E3737C2] to-[#67947F] hover:to-[#39617D94] dark:hover:to-[#A6FFD899] text-whiteA-1200 dark:text-black font-normal !py-1 !px-4 dark:from-white dark:via-white dark:to-[#1a7a4c75] bg-slate-1200">
|
||||
Currently happening
|
||||
</Badge>
|
||||
<h2 className="text-4xl dark:text-white mb-2">The Supabase Content Storm</h2>
|
||||
<p className="text-slate-900 md:max-w-[80%] mb-16">
|
||||
We worked with +30 content creators from around the world to drop a mountain of
|
||||
content simultaneously!
|
||||
<a rel="noopener" target="_blank" href="/blog/the-supabase-content-storm">
|
||||
<div className="text-brand-900 flex items-center">
|
||||
See all the content
|
||||
<IconExternalLink size="small" className="inline-block ml-1" />
|
||||
</div>
|
||||
</a>
|
||||
</p>
|
||||
{activeCreator !== null && (
|
||||
<div className="lg:max-w-[50%] min-h-[120px]">
|
||||
<h3 className="dark:text-white">
|
||||
{activeCreator !== null
|
||||
? `${creators[activeCreator].first_name} ${creators[activeCreator].last_name}`
|
||||
: 'Title'}
|
||||
</h3>
|
||||
<p className="text-brand-900">
|
||||
<a rel="noopener" target="_blank" href={creators[activeCreator].link}>
|
||||
<span>
|
||||
{creators[activeCreator].link_title}
|
||||
<IconExternalLink size="small" className="inline-block ml-2" />
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.mask {
|
||||
mask-image: radial-gradient(black, transparent);
|
||||
}
|
||||
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 147 KiB |
@@ -0,0 +1,39 @@
|
||||
<svg width="709" height="355" viewBox="0 0 709 355" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_867_58310)">
|
||||
<circle cx="355" cy="360" r="101" stroke="url(#paint0_linear_867_58310)" stroke-width="0.685121"/>
|
||||
<circle cx="355" cy="360" r="157" stroke="url(#paint1_linear_867_58310)" stroke-width="0.685121"/>
|
||||
<circle cx="355" cy="360" r="225" stroke="url(#paint2_linear_867_58310)" stroke-width="0.685121"/>
|
||||
<circle cx="355" cy="360" r="285" stroke="url(#paint3_linear_867_58310)" stroke-width="0.685121"/>
|
||||
<circle cx="355" cy="360" r="360" stroke="url(#paint4_linear_867_58310)" stroke-width="0.685121"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_867_58310" x1="254" y1="259" x2="456" y2="259" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.135417" stop-color="#121212" stop-opacity="0"/>
|
||||
<stop offset="0.510996" stop-color="#4F4F4F"/>
|
||||
<stop offset="0.864583" stop-color="#121212" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_867_58310" x1="198" y1="203" x2="512" y2="203" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.135417" stop-color="#121212" stop-opacity="0"/>
|
||||
<stop offset="0.510996" stop-color="#4F4F4F"/>
|
||||
<stop offset="0.864583" stop-color="#121212" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_867_58310" x1="130" y1="135" x2="580" y2="135" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.135417" stop-color="#121212" stop-opacity="0"/>
|
||||
<stop offset="0.510996" stop-color="#4F4F4F"/>
|
||||
<stop offset="0.864583" stop-color="#121212" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_867_58310" x1="70" y1="75" x2="640" y2="75" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.135417" stop-color="#121212" stop-opacity="0"/>
|
||||
<stop offset="0.510996" stop-color="#4F4F4F"/>
|
||||
<stop offset="0.864583" stop-color="#121212" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_867_58310" x1="-5" y1="0" x2="715" y2="-2.73533e-06" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.135417" stop-color="#121212" stop-opacity="0"/>
|
||||
<stop offset="0.510996" stop-color="#4F4F4F"/>
|
||||
<stop offset="0.864583" stop-color="#121212" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_867_58310">
|
||||
<rect width="709" height="355" fill="white" transform="matrix(-1 -8.74228e-08 -8.74228e-08 1 709 0)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -5,9 +5,23 @@
|
||||
<link>https://supabase.com</link>
|
||||
<description>Latest news from Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Wed, 30 Nov 2022 16:04:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Tue, 06 Dec 2022 16:04:00 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-november-2022</guid>
|
||||
<title>Supabase Beta November 2022</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-november-2022</link>
|
||||
<description>We are preparing everything for Launch Week 6, but we still had time to ship some goodies this month!</description>
|
||||
<pubDate>Tue, 06 Dec 2022 16:04:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/the-supabase-content-storm</guid>
|
||||
<title>The Supabase Content Storm</title>
|
||||
<link>https://supabase.com/blog/the-supabase-content-storm</link>
|
||||
<description>We worked with +30 content creators to drop a mountain of content simultaneously.</description>
|
||||
<pubDate>Mon, 05 Dec 2022 16:04:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/transparent-column-encryption-with-postgres</guid>
|
||||
<title>Transparent Column Encryption with Postgres</title>
|
||||
<link>https://supabase.com/blog/transparent-column-encryption-with-postgres</link>
|
||||
|
||||
@@ -426,7 +426,7 @@ launch week
|
||||
}
|
||||
|
||||
.gradient-container {
|
||||
min-height: 480px;
|
||||
min-height: 360px;
|
||||
position: relative;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Upload files to Storage
|
||||
|
||||
This example shows how to process `multipart/form-data` in an Edge Function. Then, upload files in the request to Supabase Storage and insert other fields as a record to a database table.
|
||||
@@ -0,0 +1,66 @@
|
||||
// This example shows how to use Edge Functions to read incoming multipart/form-data request,
|
||||
// and write files to Supabase Storage and other fields to a database table.
|
||||
|
||||
import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const MB = 1024 * 1024
|
||||
|
||||
const app = new Application()
|
||||
|
||||
app.use(async (ctx) => {
|
||||
const body = ctx.request.body({ type: 'form-data' })
|
||||
const formData = await body.value.read({
|
||||
// Need to set the maxSize so files will be stored in memory.
|
||||
// This is necessary as Edge Functions don't have disk write access.
|
||||
// We are setting the max size as 10MB (an Edge Function has a max memory limit of 150MB)
|
||||
// For more config options, check: https://deno.land/x/oak@v11.1.0/mod.ts?s=FormDataReadOptions
|
||||
maxSize: 10 * MB,
|
||||
})
|
||||
if (!formData.files || !formData.files.length) {
|
||||
ctx.response.status = 400
|
||||
ctx.response.body = 'missing file'
|
||||
return
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API ANON KEY - env var exported by default.
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
|
||||
)
|
||||
|
||||
//upload image to Storage
|
||||
const file = formData.files[0]
|
||||
const timestamp = +new Date()
|
||||
const uploadName = `${file.name}-${timestamp}`
|
||||
const { data: upload, error: uploadError } = await supabaseClient.storage
|
||||
.from('images')
|
||||
.upload(uploadName, file.content!.buffer, {
|
||||
contentType: file.contentType,
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (uploadError) {
|
||||
console.error(uploadError)
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'Failed to upload the file'
|
||||
return
|
||||
}
|
||||
|
||||
// insert record to messages table
|
||||
const { error } = await supabaseClient
|
||||
.from('comments')
|
||||
.insert({ message: formData.fields!.message || '', image_path: upload.path })
|
||||
if (error) {
|
||||
console.error(error)
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'Fail to add the record'
|
||||
return
|
||||
}
|
||||
|
||||
ctx.response.status = 201
|
||||
ctx.response.body = 'Success!'
|
||||
})
|
||||
|
||||
await app.listen({ port: 8000 })
|
||||
@@ -27,11 +27,14 @@
|
||||
|
||||
Para documentação completa, visite [supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
Para ver como Contribuir, visite [Primeiros Passos](./DEVELOPERS.md)
|
||||
|
||||
## Comunidade & Suporte
|
||||
|
||||
- [Fórum da comunidade](https://github.com/supabase/supabase/discussions). Melhor para: ajuda com a construção, discussões sobre melhores práticas em banco de dados.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Melhor para: problemas ou erros que você encontrar usando o Supabase.
|
||||
- [Suporte por E-mail](https://supabase.com/docs/support#business-support). Melhor para: problemas com o seu banco de dados ou infraestrutura.
|
||||
- [Discord](https://discord.supabase.com). Melhor para: compartilhar seus aplicativos e se divertir com a comunidade.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -53,6 +56,7 @@ Supabase é uma combinação de ferramentas de código aberto. Nós estamos cons
|
||||
**Arquitetura atual**
|
||||
|
||||
Supabase é uma [plataforma hospedada](https://app.supabase.com). Você pode se cadastrar e começar a usar Supabase sem instalar nada. Nós ainda estamos criando a experiência de trabalho local - esse é o nosso foco principal, junto com a estabilidade da plataforma.
|
||||
Voce tambem pode [auto-hospedar](https://supabase.com/docs/guides/hosting/overview) e [develop locally](https://supabase.com/docs/guides/local-development).
|
||||
|
||||

|
||||
|
||||
@@ -66,7 +70,7 @@ Supabase é uma [plataforma hospedada](https://app.supabase.com). Você pode se
|
||||
|
||||
#### Bibliotecas de cliente
|
||||
|
||||
Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementação independente para cada sistema externo. Esse é uma das formas de apoiar as ferramentas existentes.
|
||||
Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementação independente para cada sistema externo. Essa é uma das formas de apoiar as ferramentas existentes.
|
||||
|
||||
<table style="table-layout:fixed; white-space: nowrap;">
|
||||
<tr>
|
||||
@@ -81,6 +85,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">GoTrue</a></th>
|
||||
<th><a href="https://github.com/supabase/realtime" target="_blank" rel="noopener noreferrer">Realtime</a></th>
|
||||
<th><a href="https://github.com/supabase/storage-api" target="_blank" rel="noopener noreferrer">Storage</a></th>
|
||||
<th>Funções</th>
|
||||
</tr>
|
||||
<!-- TEMPLATE FOR NEW ROW -->
|
||||
<!-- START ROW
|
||||
@@ -93,7 +98,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
|
||||
</tr>
|
||||
END ROW -->
|
||||
<th colspan="6">⚡️ Oficial ⚡️</th>
|
||||
<th colspan="7">⚡️ Oficial ⚡️</th>
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
@@ -101,8 +106,18 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
</tr>
|
||||
<th colspan="6">💚 Comunidade 💚</th>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<th colspan="7">💚 Comunidade 💚</th>
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
@@ -110,22 +125,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">gotrue-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">realtime-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-csharp" target="_blank" rel="noopener noreferrer">storage-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-csharp" target="_blank" rel="noopener noreferrer">functions-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">postgrest-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-go" target="_blank" rel="noopener noreferrer">gotrue-go</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
@@ -134,14 +143,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kotlin</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt" target="_blank" rel="noopener noreferrer">supabase-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-kt" target="_blank" rel="noopener noreferrer">postgrest-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">gotrue-kt</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Realtime" target="_blank" rel="noopener noreferrer">realtime-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python</td>
|
||||
@@ -149,7 +160,8 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ruby</td>
|
||||
@@ -158,6 +170,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rust</td>
|
||||
@@ -166,6 +179,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
@@ -174,6 +188,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const RowHeader: FC<RowHeaderProps> = ({ sorts, filters }) => {
|
||||
} else {
|
||||
const rowIdxs = Array.from(selectedRows) as number[]
|
||||
const rows = allRows.filter((x) => rowIdxs.includes(x.idx))
|
||||
const { error } = state.rowService!.delete(rows)
|
||||
const { error } = await state.rowService!.delete(rows)
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
|
||||
@@ -17,13 +17,13 @@ const RowContextMenu: React.FC<RowContextMenuProps> = ({}) => {
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: 'Are you sure you want to delete this row? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
onAsyncConfirm: async () => {
|
||||
const { props } = p
|
||||
const { rowIdx } = props
|
||||
const row = state.rows[rowIdx]
|
||||
if (!row) return
|
||||
|
||||
const { error } = state.rowService!.delete([row])
|
||||
const { error } = await state.rowService!.delete([row])
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
|
||||
@@ -37,11 +37,7 @@ export class SqlRowService implements IRowService {
|
||||
}
|
||||
}
|
||||
|
||||
async create(row: SupaRow) {
|
||||
return { error: { message: 'not implemented' } }
|
||||
}
|
||||
|
||||
delete(rows: SupaRow[]) {
|
||||
async delete(rows: SupaRow[]) {
|
||||
const { primaryKeys, error } = this.getPrimaryKeys()
|
||||
if (error) return { error }
|
||||
|
||||
@@ -53,14 +49,7 @@ export class SqlRowService implements IRowService {
|
||||
})
|
||||
|
||||
const query = queryChains.toSql()
|
||||
SupabaseGridQueue.add(async () => {
|
||||
const { error } = await this.onSqlQuery(query)
|
||||
if (error) throw error
|
||||
}).catch((error) => {
|
||||
this.onError(error)
|
||||
})
|
||||
|
||||
return {}
|
||||
return await this.onSqlQuery(query)
|
||||
}
|
||||
|
||||
// For deleting all rows based on a given filter
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Filter, ServiceError, Sort, SupaRow } from '../../types'
|
||||
export interface IRowService {
|
||||
count: (filters: Filter[]) => Promise<{ data?: number; error?: ServiceError }>
|
||||
|
||||
create: (row: SupaRow) => Promise<{ data?: SupaRow; error?: ServiceError }>
|
||||
|
||||
fetchPage: (
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
@@ -25,7 +23,7 @@ export interface IRowService {
|
||||
* Right now, we remove rows immediately, so it's confused when delete operation fails
|
||||
* and rows are already removed from the grid
|
||||
*/
|
||||
delete: (rows: SupaRow[]) => { error?: ServiceError }
|
||||
delete: (rows: SupaRow[]) => Promise<{ error?: ServiceError }>
|
||||
deleteAll: (filters: Filter[]) => Promise<{ error?: ServiceError }>
|
||||
|
||||
update: (
|
||||
|
||||
@@ -32,14 +32,16 @@ const PortalToast = () => (
|
||||
{({ icon, message }) => (
|
||||
<>
|
||||
{icon}
|
||||
{message}
|
||||
{t.type !== 'loading' && (
|
||||
<div className="ml-4">
|
||||
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<div className="toast-message w-full">{message}</div>
|
||||
{t.type !== 'loading' && (
|
||||
<div className="ml-4">
|
||||
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ToastBar>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { DatabaseAddon } from './AddOns.types'
|
||||
|
||||
// e.g supabase-postgres-14.1.0.88 (There's 4 segments instead, so we can't use semver)
|
||||
export const getSemanticVersion = (version: string) => {
|
||||
if (!version) return 0
|
||||
|
||||
const segments = version.split('-')
|
||||
// e.g supabase-postgres-14.1.0.88
|
||||
// There's 4 segments instead so we can't use the semver package
|
||||
const segments = version.split('supabase-postgres-')
|
||||
const semver = segments[segments.length - 1]
|
||||
return Number(semver.split('.').join(''))
|
||||
|
||||
// e.g supabase-postgres-14.1.0.99-vault-rc1
|
||||
const formattedSemver = semver.split('-')[0]
|
||||
|
||||
return Number(formattedSemver.split('.').join(''))
|
||||
}
|
||||
|
||||
export const formatComputeSizes = (addons: DatabaseAddon[]) => {
|
||||
|
||||
@@ -71,12 +71,11 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const projectRegion = ui.selectedProject?.region
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canUpdatePaymentMethods = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.payment_methods')
|
||||
: isOwner
|
||||
const canUpdatePaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
|
||||
@@ -311,9 +310,8 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
{enablePermissions
|
||||
? 'You need additional permissions to add new payment methods to this organization'
|
||||
: 'Only organization owners can add new payment methods'}
|
||||
You need additional permissions to add new payment methods to this
|
||||
organization
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
|
||||
@@ -36,13 +36,13 @@ const Subscription: FC<Props> = ({
|
||||
const router = useRouter()
|
||||
|
||||
const { ref } = router.query
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
|
||||
const canReadSubscription = checkPermissions(PermissionAction.READ, 'subscriptions')
|
||||
const canUpdateSubscription = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateSubscription = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
|
||||
const { usage, isLoading: loadingUsage } = useProjectUsage(ref as string)
|
||||
|
||||
@@ -112,10 +112,8 @@ const Subscription: FC<Props> = ({
|
||||
<br />
|
||||
Our engineers are working on a fix.
|
||||
</>
|
||||
) : !canUpdateSubscription && enablePermissions ? (
|
||||
) : !canUpdateSubscription ? (
|
||||
'You need additional permissions to amend subscriptions'
|
||||
) : !canUpdateSubscription && !enablePermissions ? (
|
||||
'Only the organization owner can amend subscriptions'
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
@@ -86,8 +86,8 @@ const PITRSelection = ({}) => {
|
||||
|
||||
// Start: Variables specifically for date picker component
|
||||
// Required as it only works with vanilla Date object which is not timezone localized
|
||||
const earliestAvailableBackupFormatted = new Date(earliestAvailableBackup.format('DD-MMM-YYYY'))
|
||||
const latestAvailableBackupFormatted = new Date(latestAvailableBackup.format('DD-MMM-YYYY'))
|
||||
const earliestAvailableBackupFormatted = new Date(earliestAvailableBackup.format('YYYY-MM-DD'))
|
||||
const latestAvailableBackupFormatted = new Date(latestAvailableBackup.format('YYYY-MM-DD'))
|
||||
const isSelectedOnEarliest = checkMatchingDates(selectedDate, earliestAvailableBackupFormatted)
|
||||
const isSelectedOnLatest = checkMatchingDates(selectedDate, latestAvailableBackupFormatted)
|
||||
const availableDates = getDatesBetweenRange(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Dictionary } from 'components/grid'
|
||||
import { Form, Input, Button, Listbox } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useFlag } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { COUNTRIES } from './BillingAddress.constants'
|
||||
@@ -22,11 +22,11 @@ const BillingAddress: FC<Props> = ({ loading, address, onAddressUpdated }) => {
|
||||
const orgSlug = ui.selectedOrganization?.slug ?? ''
|
||||
const { city, country, line1, line2, postal_code, state } = address
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canReadBillingAddress = checkPermissions(PermissionAction.BILLING_READ, 'stripe.customer')
|
||||
const canUpdateBillingAddress = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.customer')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateBillingAddress = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.customer'
|
||||
)
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const initialValues = {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { useContext, useEffect } from 'react'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
|
||||
import {
|
||||
FormActions,
|
||||
FormPanel,
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms'
|
||||
|
||||
import { PageContext } from 'pages/org/[slug]/settings'
|
||||
|
||||
@@ -24,12 +18,7 @@ const BillingEmail = () => {
|
||||
billing_email: PageState.organization?.billing_email ?? '',
|
||||
}
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
const canReadBillingEmail = checkPermissions(PermissionAction.READ, 'organizations')
|
||||
|
||||
const onUpdateOrganization = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
@@ -9,7 +9,6 @@ import CreditBalance from './CreditBalance'
|
||||
import PaymentMethods from './PaymentMethods'
|
||||
import BillingAddress from './BillingAddress/BillingAddress'
|
||||
import TaxID from './TaxID/TaxID'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import BillingEmail from './BillingEmail'
|
||||
|
||||
interface Props {
|
||||
@@ -39,14 +38,6 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
? customerBalance.toString().replace('-', '')
|
||||
: customerBalance
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canReadBillingEmail = checkPermissions(PermissionAction.READ, 'organizations')
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerProfile()
|
||||
getPaymentMethods()
|
||||
|
||||
@@ -46,15 +46,14 @@ const PaymentMethods: FC<Props> = ({
|
||||
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false)
|
||||
const [isUpdatingPaymentMethod, setIsUpdatingPaymentMethod] = useState(false)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canReadPaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_READ,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
const canUpdatePaymentMethods = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.payment_methods')
|
||||
: isOwner
|
||||
const canUpdatePaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
|
||||
const onConfirmMakeDefaultPaymentMethod = async () => {
|
||||
try {
|
||||
@@ -120,9 +119,7 @@ const PaymentMethods: FC<Props> = ({
|
||||
<div className="flex w-full justify-between">
|
||||
{!canUpdatePaymentMethods ? (
|
||||
<p className="text-sm text-scale-1000">
|
||||
{enablePermissions
|
||||
? "You need additional permissions to manage this organization's payment methods"
|
||||
: 'Only organization owners can update payment methods'}
|
||||
You need additional permissions to manage this organization's payment methods
|
||||
</p>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@@ -50,12 +50,8 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
|
||||
}, [taxIds])
|
||||
|
||||
const hasChanges = !isEqual(taxIdValues, formattedTaxIds)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canReadTaxIds = checkPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids')
|
||||
const canUpdateTaxIds = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateTaxIds = checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids')
|
||||
|
||||
const onUpdateTaxId = (id: string, key: string, value: string) => {
|
||||
const updatedTaxIds = taxIdValues.map((taxId: any) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
|
||||
@@ -24,16 +24,8 @@ const GeneralSettings = observer(() => {
|
||||
|
||||
const formId = 'org-general-settings'
|
||||
const initialValues = { name: PageState.organization.name }
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canDeleteOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
const canDeleteOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
|
||||
const onUpdateOrganization = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
if (!canUpdateOrganization) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { IconAlertCircle } from 'ui'
|
||||
|
||||
import { useFlag } from 'hooks'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
// [Joshen] This can just use NoPermission component i think
|
||||
const NotOrganizationOwnerWarning = () => {
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<InformationBox
|
||||
@@ -17,9 +13,7 @@ const NotOrganizationOwnerWarning = () => {
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
{enablePermissions
|
||||
? 'Contact your organization owner or adminstrator to create a new project.'
|
||||
: 'Only the organization owner can create new projects. Contact your organization owner to create a new project for this organization.'}
|
||||
Contact your organization owner or adminstrator to create a new project.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Form, IconMail, Input, Modal, Select } from 'ui'
|
||||
|
||||
import { Member, User, Role } from 'types'
|
||||
import { checkPermissions, useFlag, useOrganizationDetail, useStore } from 'hooks'
|
||||
import { checkPermissions, useOrganizationDetail, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
@@ -23,8 +23,6 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
const router = useRouter()
|
||||
const { slug } = router.query
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { mutateOrgMembers } = useOrganizationDetail((slug as string) || '')
|
||||
|
||||
@@ -57,16 +55,14 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
}
|
||||
}
|
||||
|
||||
const roleId = enablePermissions
|
||||
? Number(values.role)
|
||||
: roles.find((role) => role.name === 'Developer')?.id ?? roles[0].id
|
||||
const roleId = Number(values.role)
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: values.email.toLowerCase(),
|
||||
owner_id: user.id,
|
||||
...(enablePermissions ? { role_id: roleId } : {}),
|
||||
role_id: roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
@@ -141,9 +137,7 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
}, [roles])
|
||||
|
||||
const selectedRole = roles.find((role) => role.id === Number(values.role))
|
||||
const invalidRoleSelected = enablePermissions
|
||||
? values.role && !rolesAddable.includes(Number(values.role))
|
||||
: false
|
||||
const invalidRoleSelected = values.role && !rolesAddable.includes(Number(values.role))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -151,7 +145,7 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
<div className="w-full py-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{roles && enablePermissions && (
|
||||
{roles && (
|
||||
<Select
|
||||
name="role"
|
||||
label="Member role"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Button, Dropdown, IconTrash, IconMoreHorizontal } from 'ui'
|
||||
|
||||
import { Member, Role } from 'types'
|
||||
import { useStore, useOrganizationDetail, useFlag, checkPermissions } from 'hooks'
|
||||
import { useStore, useOrganizationDetail, checkPermissions } from 'hooks'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
@@ -27,7 +27,6 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const { rolesRemovable } = getRolesManagementPermissions(roles)
|
||||
|
||||
const { app, ui } = useStore()
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '')
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -37,9 +36,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const isPendingInviteAcceptance = member.invited_id
|
||||
|
||||
const roleId = member.role_ids?.[0] ?? -1
|
||||
const canRemoveMember = enablePermissions
|
||||
? rolesRemovable.includes((member?.role_ids ?? [-1])[0])
|
||||
: true
|
||||
const canRemoveMember = rolesRemovable.includes((member?.role_ids ?? [-1])[0])
|
||||
const canResendInvite = checkPermissions(PermissionAction.CREATE, 'user_invites', {
|
||||
resource: { role_id: roleId },
|
||||
})
|
||||
@@ -54,11 +51,9 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
onAsyncConfirm: async () => {
|
||||
setLoading(true)
|
||||
|
||||
const response = enablePermissions
|
||||
? await delete_(`${API_URL}/organizations/${slug}/members/${member.gotrue_id}`)
|
||||
: await delete_(`${API_URL}/organizations/${slug}/members/remove`, {
|
||||
member_id: member.id,
|
||||
})
|
||||
const response = await delete_(
|
||||
`${API_URL}/organizations/${slug}/members/${member.gotrue_id}`
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
@@ -67,9 +62,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
})
|
||||
setLoading(false)
|
||||
} else {
|
||||
const updatedMembers = enablePermissions
|
||||
? members.filter((m) => m.gotrue_id !== member.gotrue_id)
|
||||
: members.filter((m) => m.id !== member.id)
|
||||
const updatedMembers = members.filter((m) => m.gotrue_id !== member.gotrue_id)
|
||||
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({
|
||||
@@ -117,7 +110,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: member.primary_email,
|
||||
owner_id: member.invited_id,
|
||||
...(enablePermissions ? { role_id: roleId } : {}),
|
||||
role_id: roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
@@ -189,7 +182,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
align="end"
|
||||
overlay={
|
||||
<>
|
||||
{!enablePermissions && !isPendingInviteAcceptance && (
|
||||
{!isPendingInviteAcceptance && (
|
||||
<>
|
||||
<Dropdown.Item onClick={() => setOwnerTransferIsVisible(!ownerTransferIsVisible)}>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Button, Loading, Listbox, IconUser, Modal } from 'ui'
|
||||
|
||||
import { Member, Role } from 'types'
|
||||
import { useStore, useFlag, useOrganizationDetail } from 'hooks'
|
||||
import { useStore, useOrganizationDetail } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
|
||||
@@ -28,17 +28,13 @@ const MembersView = () => {
|
||||
|
||||
const { ui } = useStore()
|
||||
const slug = ui.selectedOrganization?.slug || ''
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { mutateOrgMembers } = useOrganizationDetail(slug)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
|
||||
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
|
||||
|
||||
const hasAccessToMemberActions = enablePermissions ? true : isOwner
|
||||
|
||||
const getRoleNameById = (id: number | undefined) => {
|
||||
if (!roles) return id
|
||||
return roles.find((x: any) => x.id === id)?.name
|
||||
@@ -87,7 +83,7 @@ const MembersView = () => {
|
||||
<Table.th key="header-status"></Table.th>,
|
||||
<Table.th key="header-role" className="flex items-center space-x-2">
|
||||
<span>Role</span>
|
||||
{enablePermissions && <RolesHelperModal />}
|
||||
<RolesHelperModal />
|
||||
</Table.th>,
|
||||
<Table.th key="header-action"></Table.th>,
|
||||
]}
|
||||
@@ -157,71 +153,64 @@ const MembersView = () => {
|
||||
</Table.td>
|
||||
|
||||
<Table.td>
|
||||
{!role && !enablePermissions && <p>{x.is_owner ? 'Owner' : 'Developer'}</p>}
|
||||
{!role && <p>{x.is_owner ? 'Owner' : 'Developer'}</p>}
|
||||
{role && (
|
||||
<>
|
||||
{!enablePermissions ? (
|
||||
<p>{role?.name ?? 'Developer'}</p>
|
||||
) : (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Listbox
|
||||
className={disableRoleEdit ? 'pointer-events-none' : ''}
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Listbox
|
||||
className={disableRoleEdit ? 'pointer-events-none' : ''}
|
||||
disabled={disableRoleEdit}
|
||||
value={role.id}
|
||||
onChange={validateSelectedRoleToChange}
|
||||
>
|
||||
{roles.map((r: any) => (
|
||||
<Listbox.Option
|
||||
key={r.id}
|
||||
value={r.id}
|
||||
label={r.name}
|
||||
disabled={disableRoleEdit}
|
||||
value={role.id}
|
||||
onChange={validateSelectedRoleToChange}
|
||||
>
|
||||
{roles.map((r: any) => (
|
||||
<Listbox.Option
|
||||
key={r.id}
|
||||
value={r.id}
|
||||
label={r.name}
|
||||
disabled={disableRoleEdit}
|
||||
>
|
||||
{r.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</Tooltip.Trigger>
|
||||
{memberIsPendingInvite ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Role can only be changed after the user has accepted the
|
||||
invite
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : !memberIsUser && !canRemoveRole ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage this team member
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
{r.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</Tooltip.Trigger>
|
||||
{memberIsPendingInvite ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Role can only be changed after the user has accepted the invite
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : !memberIsUser && !canRemoveRole ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage this team member
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{hasAccessToMemberActions && !memberIsUser && (
|
||||
{!memberIsUser && (
|
||||
<MemberActions members={PageState.members} member={x} roles={roles} />
|
||||
)}
|
||||
</Table.td>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useContext } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Input, IconSearch } from 'ui'
|
||||
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import InviteMemberButton from './InviteMemberButton'
|
||||
@@ -21,10 +21,8 @@ const TeamSettings = observer(() => {
|
||||
const slug = ui.selectedOrganization?.slug ?? ''
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
|
||||
const canAddMembers = enablePermissions ? rolesAddable.length > 0 : isOwner
|
||||
const canAddMembers = rolesAddable.length > 0
|
||||
|
||||
function onFilterMemberChange(e: any) {
|
||||
PageState.membersFilterString = e.target.value
|
||||
|
||||
@@ -35,7 +35,7 @@ const CustomDomainConfig = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<FormHeader title="Custom Domains" description="" />
|
||||
<FormHeader title="Custom Domains" description="Present a branded experience to your users" />
|
||||
{data?.status === '0_no_hostname_configured' ? (
|
||||
<CustomDomainsConfigureHostname projectRef={ref} />
|
||||
) : (
|
||||
|
||||
@@ -83,9 +83,29 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain
|
||||
: 'Please note that it may take up to 24 hours for the DNS records to propagate.'
|
||||
}
|
||||
description={
|
||||
isNotVerifiedYet
|
||||
? 'Do check again in a bit as it may take up to 24 hours for changes in DNS records to propagate.'
|
||||
: undefined
|
||||
isNotVerifiedYet ? (
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
Do check again in a bit as it may take up to 24 hours for changes in DNS
|
||||
records to propagate.
|
||||
</p>
|
||||
<p>
|
||||
You may also visit{' '}
|
||||
<Link href={`https://whatsmydns.net/#TXT/${customDomain.hostname}`}>
|
||||
<a className="text-brand-900">here</a>
|
||||
</Link>{' '}
|
||||
to check if your DNS has been propagated successfully before clicking verify.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
You may also visit{' '}
|
||||
<Link href={`https://whatsmydns.net/#TXT/${customDomain.hostname}`}>
|
||||
<a className="text-brand-900">here</a>
|
||||
</Link>{' '}
|
||||
to check if your DNS has been propagated successfully before clicking verify.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ const CustomDomainsConfigureHostname = ({ projectRef }: CustomDomainsConfigureHo
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Add a Custom Domain</FormSectionLabel>}>
|
||||
<FormSection header={<FormSectionLabel>Add a custom domain</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input
|
||||
id="domain"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Input } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, checkPermissions, useFlag } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
@@ -17,13 +17,10 @@ interface Props {
|
||||
const DeleteProjectButton: FC<Props> = ({ type = 'danger' }) => {
|
||||
const router = useRouter()
|
||||
const { app, ui } = useStore()
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const project = ui.selectedProject
|
||||
const projectRef = project?.ref
|
||||
const projectTier = project?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
const isFree = projectTier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -38,9 +35,7 @@ const DeleteProjectButton: FC<Props> = ({ type = 'danger' }) => {
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const canDeleteProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: isOwner
|
||||
const canDeleteProject = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const toggle = () => {
|
||||
if (loading) return
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC } from 'react'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, checkPermissions, useFlag } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import {
|
||||
@@ -20,16 +20,11 @@ const General: FC<Props> = ({}) => {
|
||||
const { app, ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const formId = 'project-general-settings'
|
||||
const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' }
|
||||
const canUpdateProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: isOwner
|
||||
const canUpdateProject = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
const onSubmit = async (values: any, { resetForm }: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, { name: values.name })
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconPause } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useFlag } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
@@ -21,15 +21,13 @@ const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const canPauseProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.pause')
|
||||
: isOwner
|
||||
const canPauseProject = checkPermissions(
|
||||
PermissionAction.INFRA_EXECUTE,
|
||||
'queue_jobs.projects.pause'
|
||||
)
|
||||
|
||||
const requestPauseProject = async () => {
|
||||
if (!canPauseProject) {
|
||||
|
||||
@@ -23,7 +23,7 @@ interface Props {
|
||||
sidePanelKey?: 'row' | 'column' | 'table'
|
||||
/** Toggles if we're duplicating a table */
|
||||
isDuplicating: boolean
|
||||
/** Selected entities if we're editting a row, column or table */
|
||||
/** Selected entities if we're editing a row, column or table */
|
||||
selectedRowToEdit?: Dictionary<any>
|
||||
selectedColumnToEdit?: PostgresColumn
|
||||
selectedTableToEdit?: PostgresTable
|
||||
|
||||
@@ -16,21 +16,19 @@ interface Props {
|
||||
|
||||
const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
const { ui, app } = useStore()
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
const orgSlug = ui.selectedOrganization?.slug
|
||||
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug)
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
|
||||
const [showConfirmRestore, setShowConfirmRestore] = useState(false)
|
||||
const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false)
|
||||
|
||||
const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug)
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
|
||||
const canResumeProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume')
|
||||
: isOwner
|
||||
const canResumeProject = checkPermissions(
|
||||
PermissionAction.INFRA_EXECUTE,
|
||||
'queue_jobs.projects.initialize_or_resume'
|
||||
)
|
||||
|
||||
const onSelectRestore = () => {
|
||||
if (!canResumeProject) {
|
||||
|
||||
@@ -43,10 +43,15 @@ function ConfirmModal({
|
||||
loading={loading}
|
||||
customFooter={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" onClick={() => onCancelClick()}>
|
||||
<Button type="default" disabled={loading} onClick={() => onCancelClick()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => onConfirmClick()} loading={loading}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={loading}
|
||||
onClick={() => onConfirmClick()}
|
||||
loading={loading}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
{icon && <div className="mt-1">{icon}</div>}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between space-x-32">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">{primaryText}</p>
|
||||
<div>
|
||||
|
||||
@@ -3,25 +3,13 @@ import useSWR, { mutate } from 'swr'
|
||||
import { Member } from 'types'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
export function useOrganizationDetail(slug: string) {
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
// Get org members
|
||||
const url = enablePermissions
|
||||
? `${API_URL}/organizations/${slug}/members?member_roles`
|
||||
: `${API_URL}/props/org/${slug}`
|
||||
const url = `${API_URL}/organizations/${slug}/members?member_roles`
|
||||
|
||||
const { data, error } = useSWR<any>(slug ? url : null, get)
|
||||
let members = enablePermissions
|
||||
? data
|
||||
: (data?.members ?? []).map((m: any) => ({
|
||||
id: m.id,
|
||||
is_owner: m.is_owner,
|
||||
username: m.profile.username,
|
||||
primary_email: m.profile.primary_email,
|
||||
})) ?? []
|
||||
let members = data
|
||||
|
||||
// Get pending invite users
|
||||
const pendingInviteUrl = `${API_URL}/organizations/${slug}/members/invite`
|
||||
@@ -37,7 +25,7 @@ export function useOrganizationDetail(slug: string) {
|
||||
username: x.invited_email.slice(0, 1),
|
||||
primary_email: x.invited_email,
|
||||
}
|
||||
return enablePermissions ? { ...member, role_ids: [x.role_id] } : member
|
||||
return { ...member, role_ids: [x.role_id] }
|
||||
})
|
||||
|
||||
members = [...members, ...invitedMembers]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import jsonLogic from 'json-logic-js'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import useSWR from 'swr'
|
||||
@@ -40,9 +40,6 @@ export function checkPermissions(
|
||||
) {
|
||||
if (!IS_PLATFORM) return true
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
if (!enablePermissions) return true
|
||||
|
||||
const { ui } = useStore()
|
||||
const orgid = organizationId ?? ui?.selectedOrganization?.id
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const { slug } = router.query
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
@@ -66,13 +65,8 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const currentOrg = organizations.find((o: any) => o.slug === slug)
|
||||
const stripeCustomerId = currentOrg?.stripe_customer_id
|
||||
|
||||
const isOrganizationOwner = ui.selectedOrganization?.is_owner
|
||||
const availableRegions = getAvailableRegions()
|
||||
|
||||
const isAdmin = enablePermissions
|
||||
? checkPermissions(PermissionAction.CREATE, 'projects')
|
||||
: isOrganizationOwner
|
||||
|
||||
const isAdmin = checkPermissions(PermissionAction.CREATE, 'projects')
|
||||
const isInvalidSlug = isUndefined(currentOrg)
|
||||
const isEmptyOrganizations = organizations.length <= 0 && app.organizations.isInitialized
|
||||
const isEmptyPaymentMethod = paymentMethods ? !paymentMethods.length : false
|
||||
|
||||
@@ -42,10 +42,7 @@ const ResetDbPassword: FC<any> = () => {
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canResetDbPassword = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
|
||||
const [isUpdatingPassword, setIsUpdatingPassword] = useState<boolean>(false)
|
||||
|
||||
@@ -19,6 +19,8 @@ export const CONFIRMATION: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -48,6 +50,8 @@ export const INVITE: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -77,6 +81,8 @@ export const MAGIC_LINK: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -106,6 +112,8 @@ export const EMAIL_CHANGE: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the email change
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -135,6 +143,8 @@ export const RECOVERY: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the password reset
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -552,4 +552,12 @@
|
||||
animation-name: Toastify__slideOutDown;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=ReactToastify.css.map */
|
||||
.toast-message {
|
||||
max-width: 258px;
|
||||
div[role='status'] {
|
||||
@apply justify-start w-full break-words;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=ReactToastify.css.map */
|
||||