Bring in changes from master and resolve conflicts
31
apps/docs/components/MDX/auth_helpers_faq.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
### Fetch requests to API endpoints aren't showing the session
|
||||
|
||||
You must pass along the cookie header with the fetch request in order for your API endpoint to get access to the cookie from this request.
|
||||
|
||||
```ts
|
||||
const res = await fetch('http://localhost/contact', {
|
||||
headers: {
|
||||
cookie: headers().get('cookie') as string,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Performing administration tasks on the server side with the `service_role` `secret`
|
||||
|
||||
By default, the auth-helpers do not permit the use of the `service_role` `secret`. This restriction is in place to prevent the accidental exposure of your `service_role` `secret` to the public. Since the auth-helpers function on both the server and client side, it becomes challenging to separate the key specifically for client-side usage.
|
||||
|
||||
However, there is a solution. You can create a separate Supabase client using the `createClient` method from `@supabase/supabase-js` and provide it with the `service_role` `secret`. In a server environment, you will also need to disable certain properties to ensure proper functionality.
|
||||
|
||||
By implementing this approach, you can safely utilize the `service_role` `secret` without compromising security or exposing sensitive information to the public.
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(supabaseUrl, serviceRoleSecret, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -22,6 +22,7 @@ import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
import AuthHelpersFAQ from './MDX/auth_helpers_faq.mdx'
|
||||
import { CH } from '@code-hike/mdx/components'
|
||||
import RefHeaderSection from './reference/RefHeaderSection'
|
||||
|
||||
@@ -74,6 +75,7 @@ const components = {
|
||||
SocialProviderSettingsSupabase,
|
||||
StepHikeCompact,
|
||||
StorageManagement,
|
||||
AuthHelpersFAQ,
|
||||
Mermaid,
|
||||
Extensions,
|
||||
Alert: (props: any) => (
|
||||
|
||||
@@ -79,7 +79,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
|
||||
When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary.
|
||||
|
||||
In Next.js Server Components, you can read a cookie, but you can't write back to it. Middleware on the other hand, allow you to both read a write to cookies.
|
||||
Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies.
|
||||
|
||||
Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. We'll use Middleware to refresh the user's session before loading Server Component routes.
|
||||
|
||||
@@ -528,7 +528,7 @@ This allows for the Supabase client to be easily instantiated in the correct con
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/f4yAnVgcJqI"
|
||||
src="https://www.youtube-nocookie.com/embed/6Sb8R1PYhTY"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
@@ -784,7 +784,7 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/3kK-40z0DHI"
|
||||
src="https://www.youtube-nocookie.com/embed/r6q7ypXbPFI"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
@@ -854,6 +854,24 @@ See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-
|
||||
- [Protected Routes](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/[id]/page.tsx)
|
||||
- [Conditional Rendering in Client Components with SSR](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login-form.tsx)
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
<AuthHelpersFAQ />
|
||||
|
||||
### OAuth sign in isn't redirecting on the server side
|
||||
|
||||
The reason behind this limitation is that the auth helpers library lacks a direct mechanism for performing server-side redirects, as each framework handles redirects differently. However, the library does offer a URL through the data property it returns, which should be utilized for the purpose of redirection.
|
||||
|
||||
```ts
|
||||
import { NextResponse } from "next/server";
|
||||
...
|
||||
const { data } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
})
|
||||
|
||||
return NextResponse.redirect(data.url)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating to v0.7.X
|
||||
|
||||
@@ -745,6 +745,24 @@ export default function Index() {
|
||||
|
||||
> Ensure you have [enabled replication](https://supabase.com/dashboard/project/_/database/replication) on the table you are subscribing to.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
<AuthHelpersFAQ />
|
||||
|
||||
### OAuth sign in isn't redirecting on the server side
|
||||
|
||||
The reason behind this limitation is that the auth helpers library lacks a direct mechanism for performing server-side redirects, as each framework handles redirects differently. However, the library does offer a URL through the data property it returns, which should be utilized for the purpose of redirection.
|
||||
|
||||
```ts
|
||||
import { redirect } from "@remix-run/node"; // or cloudflare/deno
|
||||
...
|
||||
const { data } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
})
|
||||
|
||||
return redirect(data.url)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating to v0.2.0
|
||||
|
||||
@@ -749,6 +749,24 @@ export const actions = {
|
||||
}
|
||||
```
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
<AuthHelpersFAQ />
|
||||
|
||||
### OAuth sign in isn't redirecting on the server side
|
||||
|
||||
The reason behind this limitation is that the auth helpers library lacks a direct mechanism for performing server-side redirects, as each framework handles redirects differently. However, the library does offer a URL through the data property it returns, which should be utilized for the purpose of redirection.
|
||||
|
||||
```ts
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
...
|
||||
const { data } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
})
|
||||
|
||||
throw redirect(303, data.url)
|
||||
```
|
||||
|
||||
## Migration Guide [#migration]
|
||||
|
||||
### Migrate to 0.10
|
||||
|
||||
@@ -24,17 +24,17 @@ From the perspective of Postgres, config overrides will show up in the global co
|
||||
|
||||
The following parameters are available for overrides:
|
||||
|
||||
1. `effective_cache_size`
|
||||
1. `maintenance_work_mem`
|
||||
1. `max_connections`
|
||||
1. `max_parallel_maintenance_workers`
|
||||
1. `max_parallel_workers_per_gather`
|
||||
1. `max_parallel_workers`
|
||||
1. `max_worker_processes`
|
||||
1. `session_replication_role`
|
||||
1. `shared_buffers`
|
||||
1. `statement_timeout`
|
||||
1. `work_mem`
|
||||
1. [effective_cache_size](https://postgresqlco.nf/doc/en/param/effective_cache_size/)
|
||||
1. [maintenance_work_mem](https://postgresqlco.nf/doc/en/param/maintenance_work_mem/)
|
||||
1. [max_connections](https://postgresqlco.nf/doc/en/param/max_connections/)
|
||||
1. [max_parallel_maintenance_workers](https://postgresqlco.nf/doc/en/param/max_parallel_maintenance_workers/)
|
||||
1. [max_parallel_workers_per_gather](https://postgresqlco.nf/doc/en/param/max_parallel_workers_per_gather/)
|
||||
1. [max_parallel_workers](https://postgresqlco.nf/doc/en/param/max_parallel_workers/)
|
||||
1. [max_worker_processes](https://postgresqlco.nf/doc/en/param/max_worker_processes/)
|
||||
1. [session_replication_role](https://postgresqlco.nf/doc/en/param/session_replication_role/)
|
||||
1. [shared_buffers](https://postgresqlco.nf/doc/en/param/shared_buffers/)
|
||||
1. [statement_timeout](https://postgresqlco.nf/doc/en/param/statement_timeout/)
|
||||
1. [work_mem](https://postgresqlco.nf/doc/en/param/work_mem/)
|
||||
|
||||
### Setting config using the CLI
|
||||
|
||||
|
||||
@@ -87,14 +87,25 @@ In read-only mode, clients will encounter errors such as `cannot execute INSERT
|
||||
|
||||
### Disabling read-only mode
|
||||
|
||||
You can manually override read-only mode to reduce disk size. To do this, run the following in the [SQL Editor](https://supabase.com/dashboard/project/_/sql):
|
||||
You manually override read-only mode to reduce disk size. To do this, run the following in the [SQL Editor](https://supabase.com/dashboard/project/_/sql):
|
||||
|
||||
First, change the [transaction access mode](https://www.postgresql.org/docs/current/sql-set-transaction.html):
|
||||
|
||||
```sql
|
||||
SET
|
||||
default_transaction_read_only = 'off';
|
||||
set session characteristics as transaction read write;
|
||||
```
|
||||
|
||||
This allows you to delete data from within the session. After deleting data, you should run a vacuum to reclaim as much space as possible.
|
||||
This allows you to delete data from within the session. After deleting data, you should run a vacuum to reclaim as much space as possible. After deleting data, consider running a vacuum to reclaim as much space as possible:
|
||||
|
||||
```sql
|
||||
vacuum;
|
||||
```
|
||||
|
||||
Once you have reclaimed space, you can run the following to disable [read-only](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-READ-ONLY) mode:
|
||||
|
||||
```SQL
|
||||
set default_transaction_read_only = 'off';
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -60,16 +60,16 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints:
|
||||
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------- |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. Is customizable with custom SMTP set up. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. As of 14th July 2023, this has been updated to 4 emails per hour. Is customizable with custom SMTP set up. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
|
||||
### Realtime Quotas
|
||||
|
||||
|
||||
@@ -48,10 +48,14 @@ We can use your JWT Secret to generate new `anon` and `service` API keys using t
|
||||
|
||||
### Update API Keys
|
||||
|
||||
Replace the values the `.env` file:
|
||||
Replace the values in these files:
|
||||
|
||||
- `.env`:
|
||||
- `ANON_KEY` - replace with an `anon` key
|
||||
- `SERVICE_ROLE_KEY` - replace with a `service` key
|
||||
- `volumes/api/kong.yml`
|
||||
- `anon` - replace with an `anon` key
|
||||
- `service_role` - replace with a `service` key
|
||||
|
||||
### Update Secrets
|
||||
|
||||
|
||||
311
apps/www/_blog/2023-07-13-pgvector-performance.mdx
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: 'pgvector 0.4.0 performance'
|
||||
description: There's been lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.
|
||||
tags:
|
||||
- AI
|
||||
- performance
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2023-07-13'
|
||||
toc_depth: 2
|
||||
author: egor_romanov,pavel
|
||||
image: 2023-07-13-pgvector-performance/vector-benchmarks-og.jpeg
|
||||
thumb: 2023-07-13-pgvector-performance/vector-benchmarks-thumb.jpeg
|
||||
---
|
||||
|
||||
There are a few pgvector benchmarks floating around the internet, most recently a [pgvector vs Qdrant](https://nirantk.com/writing/pgvector-vs-qdrant/) comparison by NirantK. We wanted to reproduce (or improve!) the results.
|
||||
|
||||
There is an obvious bias here: we're a Postgres company. It's not our goal to prove that pgvector is better than Qdrant for running vector workloads. From everything we hear about Qdrant, it's fantastic.
|
||||
|
||||
Our goals in this article are:
|
||||
|
||||
1. To show the strengths and limitations of the _current version_ of pgvector.
|
||||
2. Highlight some improvements that are coming to pgvector.
|
||||
3. Prove to you that it's completely viable for production workloads and give you some tips on using it at scale. We'll show you how to run 1 million Open AI embeddings at ~1800 requests per second with 91% precision, or 670 requests per second with 98% precision.
|
||||
|
||||
## Benchmark Methodology
|
||||
|
||||
We've used the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology, a standard for benchmarking vector databases.
|
||||
|
||||
The key elements are:
|
||||
|
||||
- **Helper scripts:** a Python test runner which is responsible for data upload, index creation, and query execution. This uses [qdrant's vector-db-benchmark](https://github.com/qdrant/vector-db-benchmark) repo. The “engine” in this repo uses [Vecs](https://github.com/supabase/vecs), a Python client for pgvector.
|
||||
- **Runtime:** Each test runs for at least 30-40 minutes and included a series of experiments executed at various concurrency levels. This process allowed us to gauge the engine's performance under different load types. Subsequently, we averaged the results.
|
||||
- **Pre-warming RAM:** We executed 10,000 to 50,000 “warm-up” queries before each benchmark, matching the number of probes as the benchmark. Additionally, we executed about 1,000 queries with probes ranging from three to ten times the benchmark's probes. Both of these help with RAM utilization.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/vecs-bench--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/vecs-bench--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Hardware
|
||||
|
||||
All compute add-ons available on Supabase were used to run our benchmarks. Each add-on variant has a different allocation of RAM and CPU cores, the details of which are available in our docs. Each Supabase compute add-on comes with a specific set of optimizations ([version 2023-07](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08)).
|
||||
|
||||
| Instance | CPU | Memory |
|
||||
| -------- | ----------------------- | ------ |
|
||||
| 2XL | 8-core ARM (dedicated) | 32 GB |
|
||||
| 4XL | 16-core ARM (dedicated) | 64 GB |
|
||||
| 8XL | 32-core ARM (dedicated) | 128 GB |
|
||||
| 12XL | 48-core ARM (dedicated) | 192 GB |
|
||||
| 16XL | 64-core ARM (dedicated) | 256 GB |
|
||||
|
||||
## Dataset
|
||||
|
||||
We tested using the same dataset as the Qdrant comparison: [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M). This dataset includes 1M embeddings with 1536 dimensions (created using OpenAI). The embeddings are created by Wikipedia articles. It's a great dataset!
|
||||
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||
We also have some useful [benchmarks in our docs](https://supabase.com/docs/guides/ai/choosing-compute-addon#results) for [gist-960-angular](http://corpus-texmex.irisa.fr/) (1M image embeddings, 960 dimensions) and [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) (1.6M text embeddings, 512 dimensions).
|
||||
|
||||
</div>
|
||||
|
||||
## Baseline
|
||||
|
||||
Let's start with NirantK's results as a baseline:
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/nirantk--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/nirantk--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
They aren't very flattering! Repeating our statements above, these benchmarks are using the defaults for both engines. Our goal now is to replicate the results, and then see what improvements need to be made as developers scale up their workload.
|
||||
|
||||
## Results
|
||||
|
||||
Our tests mirrored NirantK's: but incorporated slight variations:
|
||||
|
||||
Same:
|
||||
|
||||
- We used the same dataset
|
||||
- We used the same hardware: a 2XL instance on Supabase, which offers the same core and RAM count as NirantK's machine - 8 cores and 32 GB of RAM
|
||||
|
||||
Changed:
|
||||
|
||||
- We used the pre-warming technique described earlier.
|
||||
- We used the `inner-product` distance function.
|
||||
- We set `lists` constant for an index equal to 2000 instead of 1000.
|
||||
- We adjusted the [Probes](https://github.com/pgvector/pgvector#query-options) in various runs to benchmark the difference (NirantK's tests used `probes = 1`).
|
||||
|
||||
The resulting figures were significantly different after these changes.
|
||||
|
||||
### PROBES = 10
|
||||
|
||||
With the changes above and probes set to 10, pgvector was faster and more accurate:
|
||||
|
||||
- precision@10 of 0.91
|
||||
- RPS (requests per second) of 380
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-optimized--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-optimized--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
### PROBES = 40
|
||||
|
||||
If we increase the probes from 10 to 40, pgvector was not just substantially faster but also boasted almost the same precision as Qdrant:
|
||||
|
||||
- precision@10 of 0.98
|
||||
- RPS of 140
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-qdrant--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-qdrant--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
### Scaling the database
|
||||
|
||||
Another key takeaway is that the performance scales predictably with the size of the database. For instance, a 4XL instance achieves precision@10 of 0.98 and RPS of 270 with probes set to 40. Moreover, an 8XL compute add-on analogously obtains precision@10 of 0.98 and an RPS of 470, surpassing the results of Qdrant.
|
||||
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||
The Qdrant benchmark uses “default” configuration and is in not indicative of its capabilities after modifying the configuration.
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-8xl--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/pgvector-8xl--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Although more compute is required to match Qdrant's precision and RPS levels concurrently, this is still a satisfying outcome. It means that it's not a _necessity_ to use another vector database. You can put everything in Postgres to lower your operational complexity.
|
||||
|
||||
### Final results: pgvector performance
|
||||
|
||||
Putting it all together, we find that we can predictably scale our database to match the performance we need.
|
||||
|
||||
With a 64-core, 256 GB server we achieve ~1800 RPS and 0.91 precision. This is for pgvector 0.4.0, and we've heard that the latest version (0.4.4) already has significant improvements. We'll release those benchmarks as soon as we have them.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/size-to-rps--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/size-to-rps--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Other performance factors
|
||||
|
||||
It's been about 5 months since we [added](https://supabase.com/blog/openai-embeddings-postgres-vector) pgvector to the platform. Since then we've discovered a few other important things to keep in mind.
|
||||
|
||||
### Increasing lists improves performance
|
||||
|
||||
Another way to improve performance without throwing more compute would be to increase `lists`.
|
||||
|
||||
We ran a test to measure the impact of list size: we uploaded 90,000 vectors from the Wikipedia dataset and then queried 10,000 vectors from the same dataset. The documentation recommends to use `lists` constant of `number of vectors / 1000`. In this case, it would be 90.
|
||||
|
||||
But as our experiment shows, we can improve RPS if we increase `lists` (i.e. with more lists in the index we need to get less index data to get the same precision). So for 95% precision, we can take any of:
|
||||
|
||||
- 3% of index data = 270 lists
|
||||
- 6% of index data = 90 lists
|
||||
- 13% of index data = 30 lists
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/lists-count--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/lists-count--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
This also has an important caveat: building the index takes longer with more lists. Here we measure the index build time for a dataset containing 900,000 vectors:
|
||||
|
||||
So if you can afford an index build time of 1 hour or more, you can go with `lists=5000` (`number of vectors / 200`) or more!
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/lists-for-1m--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/lists-for-1m--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||
You may need to increase `maintenance_work_mem` to be able to create an index with high values for `lists`. For example:
|
||||
|
||||
```sql
|
||||
SET maintenance_work_mem TO '7168 MB';
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Keeping in mind that the overall index size is almost the same, and only index build time increases, we can say that it's better to use more lists for better select queries speed.
|
||||
|
||||
### Real data has higher precision than random data
|
||||
|
||||
Embeddings created from “real” data are more likely to be clustered together, whereas random embeddings are more likely to be scattered. In other words, real embeddings are very far from being randomly distributed. This might seem obvious, but it's an important call-out for benchmarks.
|
||||
|
||||
Embeddings generated for similarity search using “real world data” will be more correlated, so the precision will be higher as well. You can see the difference in this chart using 10,000 Wikipedia embeddings, vs 10,000 randomly-generated embeddings:
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/random-data--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/images/blog/2023-07-13-pgvector-performance/random-data--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Optimizing pgvector performance
|
||||
|
||||
Armed with all this information, we can safely give you a few tips and strategies for optimizing your pgvector workloads.
|
||||
|
||||
### Tips
|
||||
|
||||
First, a few generic tips which you can pick and choose from:
|
||||
|
||||
1. **Adjust your Postgres config.** It should be aligned with RAM & CPU cores. [Find more details here](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08).
|
||||
2. Prefer `inner-product` to `L2` or `Cosine` distances if your vectors are normalized (like `text-embedding-ada-002`). If embeddings are not normalized, `Cosine` distance should give the best results with an index.
|
||||
3. **Pre-warm your database.** Implement the warm-up technique we described earlier before transitioning to production.
|
||||
4. **Establish your workload.** Increasing the lists constant for the pgvector index can accelerate your queries (at the expense of a slower build). For instance, for benchmarks with OpenAI embeddings, we employed a `lists` constant of 2000 (`number of vectors / 500`) as opposed to the suggested 1000 (`number of vectors / 1000`).
|
||||
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the `probes` constant, balancing precision with RPS.
|
||||
|
||||
### Going into production
|
||||
|
||||
Before running your pgvector workload in production, here are a few steps you can take to maximize performance.
|
||||
|
||||
1. Over-provision RAM during preparation. You can scale down in step `5`, but it's better to start with a larger size to get the best results for RAM requirements. (We'd recommend at least 8XL if you're using Supabase.)
|
||||
2. Upload your data to the database. If you use [`vecs`](https://supabase.com/docs/guides/ai/python/api) library, it will automatically generate an index with default parameters.
|
||||
3. Run a benchmark using randomly generated queries and see the results. Again, you can use `vecs` library with the `ann-benchmarks` tool. Do it with probes set to 10 (default) and then with probes set to 100 or more, so RPS will be lower than 10.
|
||||
4. Take a look at the RAM usage, and save it as a note for yourself. You would likely want to use compute add-on in the future that would have the same amount of RAM as used at the moment (both actual RAM usage and RAM used for cache and buffers).
|
||||
5. Scale down your compute add-on to the one that would have the same amount of RAM as used at the moment.
|
||||
6. Repeat step 3. to load the data into RAM. You should see that RPS is increased on subsequent runs, and stop when it no longer increases. Then repeat the benchmark with probes set to a higher value as well if you didn't do it before on that compute add-on size.
|
||||
7. Run a benchmark using real queries and see the results. You can use `vecs` library for that as well with `ann-benchmarks` tool. Do it with probes set to 10 (default) and then gradually increase/decrease probes value until you see that both precision and RPS match your requirements.
|
||||
8. If you want higher RPS and you don't expect to have frequent inserts and reindexing, you can increase `lists` constantly. You have to rebuild the index with higher lists value and repeat steps 6-7 to find the best combination of `lists` and `probes` constants to achieve the best RPS and precision values. Higher `lists` mean that index will build slower, but you can achieve better RPS and precision. Higher probes mean that select queries will be slower, but you can achieve better precision.
|
||||
|
||||
## The pgvector roadmap
|
||||
|
||||
pgvector is still early in development. As with any open source tool, it needs time and resources to make it better. Supabase plans to continue supporting Andrew with his development of pgvector.
|
||||
|
||||
What's next on the roadmap? Andrew has an impressive list of features [planned for v0.5.0](https://github.com/pgvector/pgvector/issues/27):
|
||||
|
||||
- Adding HNSW: an index with better speed & precision than IVFFlat (at a higher memory cost)
|
||||
- Product quantization: better storage for IVFFLAT, improving speed and recall
|
||||
- Parallel index builds: building your IVFFLAT indexes will be much faster
|
||||
|
||||
## More AI resources
|
||||
|
||||
- [How to build ChatGPT Plugin from scratch with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
|
||||
- [Docs pgvector: Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
|
||||
- [pgvector vs Qdrant](https://nirantk.com/writing/pgvector-vs-qdrant)
|
||||
- [Choosing Compute Add-on for AI workloads](https://supabase.com/docs/guides/ai/choosing-compute-addon)
|
||||
@@ -1,5 +1,6 @@
|
||||
export const APP_NAME = 'Supabase'
|
||||
export const DESCRIPTION = 'The Open Source Alternative to Firebase.'
|
||||
export const DEFAULT_META_DESCRIPTION =
|
||||
'Build production-grade applications with a Postgres database, Authentication, instant APIs, Realtime, Functions, Storage and Vector embeddings. Start for free.'
|
||||
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
|
||||
export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
|
||||
@@ -3,7 +3,7 @@ import '../../../packages/ui/build/css/themes/dark.css'
|
||||
|
||||
import '../styles/index.css'
|
||||
|
||||
import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants'
|
||||
import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from 'lib/constants'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import { AppProps } from 'next/app'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -52,7 +52,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
}
|
||||
}, [router.isReady])
|
||||
|
||||
const site_title = `The Open Source Firebase Alternative | ${APP_NAME}`
|
||||
const site_title = `${APP_NAME} | The Open Source Firebase Alternative`
|
||||
const { basePath } = useRouter()
|
||||
|
||||
return (
|
||||
@@ -63,7 +63,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
<Meta />
|
||||
<DefaultSeo
|
||||
title={site_title}
|
||||
description={DESCRIPTION}
|
||||
description={DEFAULT_META_DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
url: 'https://supabase.com/',
|
||||
|
||||
@@ -4,7 +4,7 @@ import CTABanner from 'components/CTABanner/index'
|
||||
import FlyOut from 'components/UI/FlyOut'
|
||||
import { AlphaNumbers, IntroductionSegments, PerformanceComparisonData } from 'data/BetaPage'
|
||||
import authors from 'lib/authors.json'
|
||||
import { APP_NAME, DESCRIPTION } from 'lib/constants'
|
||||
import { APP_NAME, DEFAULT_META_DESCRIPTION } from 'lib/constants'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
@@ -975,7 +975,7 @@ const Beta = (props: Props) => {
|
||||
title={site_title}
|
||||
openGraph={{
|
||||
title: site_title,
|
||||
description: DESCRIPTION,
|
||||
description: DEFAULT_META_DESCRIPTION,
|
||||
url: `https://supabase.com/beta`,
|
||||
type: 'article',
|
||||
article: {
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 200 KiB |
@@ -40,24 +40,20 @@ services:
|
||||
container_name: supabase-kong
|
||||
image: kong:2.8.1
|
||||
restart: unless-stopped
|
||||
# https://unix.stackexchange.com/a/294837
|
||||
entrypoint: bash -c 'eval "echo \"$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
|
||||
KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
|
||||
# https://github.com/supabase/cli/issues/14
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
volumes:
|
||||
# https://github.com/supabase/supabase/issues/12661
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro
|
||||
- ./volumes/api/kong.yml:/var/lib/kong/kong.yml:ro
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
|
||||
@@ -6,10 +6,10 @@ _format_version: '1.1'
|
||||
consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY
|
||||
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY
|
||||
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
|
||||
@@ -55,12 +55,12 @@ Supabase è una combinazione di strumenti open source. Stiamo costruendo le funz
|
||||
|
||||
**Architettura**
|
||||
|
||||
Supabase è una [piattaforma ospitata](https://supabase.com/dashboard). È possibile registrarsi e iniziare a usare Supabase senza installare nulla.
|
||||
È anche possibile [auto-ospitare](https://supabase.com/docs/guides/hosting/overview) e [sviluppare localmente](https://supabase.com/docs/guides/local-development).
|
||||
Supabase è una [piattaforma hosted](https://supabase.com/dashboard). È possibile registrarsi e iniziare a usare Supabase senza installare nulla.
|
||||
È anche possibile fare [self-hosting](https://supabase.com/docs/guides/hosting/overview) e [sviluppare localmente](https://supabase.com/docs/guides/local-development).
|
||||
|
||||

|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/) è un sistema di database relazionale a oggetti con oltre 30 anni di sviluppo attivo che gli ha fatto guadagnare una solida reputazione in termini di affidabilità, robustezza e prestazioni.
|
||||
- [PostgreSQL](https://www.postgresql.org/) è un sistema di database relazionale a oggetti con oltre 30 anni di sviluppo attivo con una solida reputazione in termini di affidabilità, robustezza e prestazioni.
|
||||
- [Realtime](https://github.com/supabase/realtime) è un server Elixir che consente di ascoltare gli inserimenti, gli aggiornamenti e le cancellazioni di PostgreSQL tramite websocket. Realtime controlla la funzionalità di replica integrata di Postgres per le modifiche al database, converte le modifiche in JSON e trasmette il JSON tramite websocket ai client autorizzati.
|
||||
- [PostgREST](http://postgrest.org/) è un server web che trasforma il database PostgreSQL direttamente in un'API REST
|
||||
- [pg_graphql](http://github.com/supabase/pg_graphql/) un'estensione di PostgreSQL che espone un'API GraphQL
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
PITRSidePanel,
|
||||
} from 'components/interfaces/Settings/Addons'
|
||||
import ProjectUpdateDisabledTooltip from '../../ProjectUpdateDisabledTooltip'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
|
||||
export interface AddOnsProps {}
|
||||
|
||||
@@ -25,6 +27,9 @@ const AddOns = ({}: AddOnsProps) => {
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { isDarkMode } = useTheme()
|
||||
|
||||
const { project: selectedProject } = useProjectContext()
|
||||
const cpuArchitecture = getCloudProviderArchitecture(selectedProject?.cloud_provider)
|
||||
|
||||
// [Joshen] We could possibly look into reducing the interval to be more "realtime"
|
||||
// I tried setting the interval to 1m but no data was returned, may need to experiment
|
||||
const startDate = useMemo(() => dayjs().subtract(15, 'minutes').millisecond(0).toISOString(), [])
|
||||
@@ -214,7 +219,7 @@ const AddOns = ({}: AddOnsProps) => {
|
||||
</a>
|
||||
</Link>
|
||||
<p className="text-sm">
|
||||
{computeInstance?.variant?.meta?.cpu_cores ?? 2}-core ARM{' '}
|
||||
{computeInstance?.variant?.meta?.cpu_cores ?? 2}-core {cpuArchitecture}{' '}
|
||||
{computeInstance?.variant?.meta?.cpu_dedicated ? '(Dedicated)' : '(Shared)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -14,11 +14,13 @@ import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2'
|
||||
export interface PaymentMethodSelectionProps {
|
||||
selectedPaymentMethod?: string
|
||||
onSelectPaymentMethod: (id: string) => void
|
||||
layout?: 'vertical' | 'horizontal'
|
||||
}
|
||||
|
||||
const PaymentMethodSelection = ({
|
||||
selectedPaymentMethod,
|
||||
onSelectPaymentMethod,
|
||||
layout = 'vertical',
|
||||
}: PaymentMethodSelectionProps) => {
|
||||
const { ui } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -70,8 +72,7 @@ const PaymentMethodSelection = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Select payment method</p>
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center px-4 py-2 space-x-4 border rounded-md border-scale-700 bg-scale-400">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
@@ -91,6 +92,7 @@ const PaymentMethodSelection = ({
|
||||
disabled={!canUpdatePaymentMethods}
|
||||
icon={<IconCreditCard />}
|
||||
onClick={() => setShowAddNewPaymentMethodModal(true)}
|
||||
htmlType='button'
|
||||
>
|
||||
Add new
|
||||
</Button>
|
||||
@@ -116,7 +118,13 @@ const PaymentMethodSelection = ({
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
) : (
|
||||
<Listbox value={selectedPaymentMethod} onChange={onSelectPaymentMethod}>
|
||||
<Listbox
|
||||
layout={layout}
|
||||
label="Payment method"
|
||||
value={selectedPaymentMethod}
|
||||
onChange={onSelectPaymentMethod}
|
||||
className="flex items-center"
|
||||
>
|
||||
{paymentMethods.map((method: any) => {
|
||||
const label = `•••• •••• •••• ${method.card.last4}`
|
||||
return (
|
||||
|
||||
@@ -10,17 +10,18 @@ import { useOrganizationBillingMigrationMutation } from 'data/organizations/orga
|
||||
import { useOrganizationBillingMigrationPreview } from 'data/organizations/organization-migrate-billing-preview-query'
|
||||
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { PRICING_TIER_LABELS_ORG } from 'lib/constants'
|
||||
import PaymentMethodSelection from '../BillingSettingsV2/Subscription/PaymentMethodSelection'
|
||||
|
||||
const MigrateOrganizationBillingButton = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [tier, setTier] = useState('')
|
||||
const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false)
|
||||
const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(true)
|
||||
const [paymentMethodId, setPaymentMethodId] = useState('')
|
||||
|
||||
const dbTier = useMemo(() => {
|
||||
if (tier === '') return ''
|
||||
@@ -80,23 +81,15 @@ const MigrateOrganizationBillingButton = observer(() => {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
const onValidate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (!values.tier) {
|
||||
errors.tier = 'Please select a plan.'
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
const onConfirmMigrate = async (values: any) => {
|
||||
const onConfirmMigrate = async () => {
|
||||
if (!tier) return
|
||||
if (!canMigrateOrganization) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'You do not have the required permissions to migrate this organization',
|
||||
})
|
||||
}
|
||||
|
||||
migrateBilling({ organizationSlug: organization?.slug, tier: dbTier })
|
||||
migrateBilling({ organizationSlug: organization?.slug, tier: dbTier, paymentMethodId })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -118,167 +111,174 @@ const MigrateOrganizationBillingButton = observer(() => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
validateOnBlur
|
||||
initialValues={{ tier: '', isSpendCapEnabled: true }}
|
||||
onSubmit={onConfirmMigrate}
|
||||
validate={onValidate}
|
||||
>
|
||||
{() => (
|
||||
<div className="space-y-4 py-3">
|
||||
<Modal.Content>
|
||||
<div className="space-y-2">
|
||||
<Alert title="About the migration" withIcon variant="info">
|
||||
<div className="text-sm space-y-2">
|
||||
<p>
|
||||
Migrating to new organization-level billing combines subscriptions for all
|
||||
projects in the organization into a single subscription. This cannot be
|
||||
reversed.{' '}
|
||||
</p>
|
||||
<div className="space-y-4 py-3">
|
||||
<Modal.Content>
|
||||
<div className="space-y-2">
|
||||
<Alert title="About the migration" withIcon variant="info">
|
||||
<div className="text-sm space-y-2">
|
||||
<p>
|
||||
Migrating to new organization-level billing combines subscriptions for all
|
||||
projects in the organization into a single subscription. This cannot be
|
||||
reversed.{' '}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For a detailed breakdown of changes, see{' '}
|
||||
<Link href="https://www.notion.so/supabase/Organization-Level-Billing-9c159d69375b4af095f0b67881276582?pvs=4">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
Billing Migration Docs
|
||||
</a>
|
||||
</Link>
|
||||
. To transfer projects to a different organization, visit{' '}
|
||||
<Link href="/projects/_/settings/general">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
General settings
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Alert>
|
||||
<p>
|
||||
For a detailed breakdown of changes, see{' '}
|
||||
<Link href="https://www.notion.so/supabase/Organization-Level-Billing-9c159d69375b4af095f0b67881276582?pvs=4">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
Billing Migration Docs
|
||||
</a>
|
||||
</Link>
|
||||
. To transfer projects to a different organization, visit{' '}
|
||||
<Link href="/projects/_/settings/general">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
General settings
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content>
|
||||
<Listbox
|
||||
id="tier"
|
||||
label="Organization Pricing Plan"
|
||||
layout="horizontal"
|
||||
value={tier}
|
||||
onChange={setTier}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Listbox.Option label="Select plan" value="" disabled className="hidden">
|
||||
Select Plan
|
||||
</Listbox.Option>
|
||||
{Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => {
|
||||
return (
|
||||
<Listbox.Option key={k} label={v} value={k}>
|
||||
{v}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
|
||||
<p className="text-sm text-scale-1000 mt-4">
|
||||
The pricing plan, along with included usage limits will apply to your entire
|
||||
organization. See{' '}
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/pricing"
|
||||
>
|
||||
Pricing
|
||||
</a>{' '}
|
||||
for more details. Please contact support if you are an Enterprise customer.
|
||||
</p>
|
||||
|
||||
{tier !== '' && tier !== 'FREE' && (
|
||||
<div className="my-2 space-y-1 pb-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Paid plans come with one compute instance included. Additional projects will at
|
||||
least cost the compute instance hours used (min $7/month). See{' '}
|
||||
<Link href="https://www.notion.so/supabase/Organization-Level-Billing-9c159d69375b4af095f0b67881276582?pvs=4">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
Compute Instance Usage Billing
|
||||
</a>
|
||||
</Link>{' '}
|
||||
for more details.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Content>
|
||||
|
||||
<Modal.Separator />
|
||||
|
||||
{tier === 'PRO' && (
|
||||
<>
|
||||
<Modal.Content>
|
||||
<div className="mt-4 grid grid-cols-8 gap-x-8 gap-y-2">
|
||||
<div className="space-y-2 col-span-4">
|
||||
<p className="text-sm flex items-center gap-4">
|
||||
Enable spend cap{' '}
|
||||
<IconHelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="transition opacity-50 cursor-pointer hover:opacity-100"
|
||||
onClick={() => setShowSpendCapHelperModal(true)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-8">
|
||||
<Toggle
|
||||
id="isSpendCapEnabled"
|
||||
layout="vertical"
|
||||
checked={isSpendCapEnabled}
|
||||
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<p className="text-sm text-scale-1000">
|
||||
When enabled, usage is limited to the plan's quota, with restrictions when
|
||||
limits are exceeded. To scale beyond Pro limits without restrictions, disable
|
||||
the spend cap and pay for over-usage beyond the quota.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SpendCapModal
|
||||
visible={showSpendCapHelperModal}
|
||||
onHide={() => setShowSpendCapHelperModal(false)}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content>
|
||||
<Listbox
|
||||
id="tier"
|
||||
label="Organization Pricing Plan"
|
||||
layout="horizontal"
|
||||
value={tier}
|
||||
onChange={setTier}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Listbox.Option label="Select plan" value="" disabled className="hidden">
|
||||
Select Plan
|
||||
</Listbox.Option>
|
||||
{Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => {
|
||||
return (
|
||||
<Listbox.Option key={k} label={v} value={k}>
|
||||
{v}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
|
||||
<p className="text-sm text-scale-1000 mt-4">
|
||||
The pricing plan, along with included usage limits will apply to your entire
|
||||
organization. See{' '}
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/pricing"
|
||||
>
|
||||
Pricing
|
||||
</a>{' '}
|
||||
for more details. Please contact support if you are an Enterprise customer.
|
||||
</p>
|
||||
|
||||
{tier !== '' && tier !== 'FREE' && (
|
||||
<div className="my-2 space-y-1 pb-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Paid plans come with one compute instance included. Additional projects will
|
||||
at least cost the compute instance hours used (min $7/month). See{' '}
|
||||
<Link href="https://www.notion.so/supabase/Organization-Level-Billing-9c159d69375b4af095f0b67881276582?pvs=4">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
Compute Instance Usage Billing
|
||||
</a>
|
||||
</Link>{' '}
|
||||
for more details.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Modal.Separator />
|
||||
{tier === 'PRO' && (
|
||||
<div className="mt-4 grid grid-cols-8 gap-8 ">
|
||||
<div className="space-y-2 col-span-5">
|
||||
<h4 className="flex items-center gap-4">
|
||||
Spend Cap{' '}
|
||||
<IconHelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="transition opacity-50 cursor-pointer hover:opacity-100"
|
||||
onClick={() => setShowSpendCapHelperModal(true)}
|
||||
/>
|
||||
</h4>
|
||||
<p className="text-sm text-scale-1000">
|
||||
When enabled, usage is limited to the plan's quota, with restrictions when
|
||||
limits are exceeded.{' '}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
To scale beyond Pro limits without restrictions, disable the spend cap and
|
||||
pay for over-usage beyond the quota.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3">
|
||||
<Toggle
|
||||
id="isSpendCapEnabled"
|
||||
layout="vertical"
|
||||
label={
|
||||
<div className="flex space-x-4">
|
||||
<span>Enable Spend Cap</span>
|
||||
</div>
|
||||
}
|
||||
checked={isSpendCapEnabled}
|
||||
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SpendCapModal
|
||||
visible={showSpendCapHelperModal}
|
||||
onHide={() => setShowSpendCapHelperModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Content>
|
||||
<Loading active={tier !== '' && migrationPreviewIsLoading}>
|
||||
{migrationPreviewError && (
|
||||
<Alert title="Organization cannot be migrated" variant="danger">
|
||||
<span>{migrationPreviewError.message}</span>
|
||||
</Alert>
|
||||
)}
|
||||
</Loading>
|
||||
|
||||
{migrationError && (
|
||||
<Alert title="Organization cannot be migrated" variant="danger">
|
||||
<span>{migrationError.message}</span>
|
||||
</Alert>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Content>
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isMigrating}
|
||||
disabled={migrationPreviewData === undefined || isMigrating}
|
||||
>
|
||||
I understand, migrate this organization
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{tier && tier !== 'FREE' && (
|
||||
<>
|
||||
<Modal.Content>
|
||||
<PaymentMethodSelection
|
||||
layout="horizontal"
|
||||
onSelectPaymentMethod={(pm) => setPaymentMethodId(pm)}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Modal.Content>
|
||||
<Loading active={tier !== '' && migrationPreviewIsLoading}>
|
||||
{migrationPreviewError && (
|
||||
<Alert title="Organization cannot be migrated" variant="danger">
|
||||
<span>{migrationPreviewError.message}</span>
|
||||
</Alert>
|
||||
)}
|
||||
</Loading>
|
||||
|
||||
{migrationError && (
|
||||
<Alert title="Organization cannot be migrated" variant="danger">
|
||||
<span>{migrationError.message}</span>
|
||||
</Alert>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Content>
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isMigrating}
|
||||
disabled={migrationPreviewData === undefined || isMigrating || !tier}
|
||||
onClick={() => onConfirmMigrate()}
|
||||
>
|
||||
I understand, migrate this organization
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo } from 'react'
|
||||
import { useParams, useTheme } from 'common'
|
||||
import { getAddons } from 'components/interfaces/BillingV2/Subscription/Subscription.utils'
|
||||
import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDivider,
|
||||
@@ -17,6 +18,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useFlag } from 'hooks'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
import { Alert, Button, IconChevronRight, IconExternalLink } from 'ui'
|
||||
@@ -28,6 +30,9 @@ const Addons = () => {
|
||||
const snap = useSubscriptionPageStateSnapshot()
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
|
||||
const { project: selectedProject } = useProjectContext()
|
||||
const cpuArchitecture = getCloudProviderArchitecture(selectedProject?.cloud_provider)
|
||||
|
||||
// [Joshen] We could possibly look into reducing the interval to be more "realtime"
|
||||
// I tried setting the interval to 1m but no data was returned, may need to experiment
|
||||
const startDate = useMemo(() => dayjs().subtract(15, 'minutes').millisecond(0).toISOString(), [])
|
||||
@@ -222,7 +227,7 @@ const Addons = () => {
|
||||
</a>
|
||||
</Link>
|
||||
<p className="text-sm">
|
||||
{computeInstance?.variant?.meta?.cpu_cores ?? 2}-core ARM{' '}
|
||||
{computeInstance?.variant?.meta?.cpu_cores ?? 2}-core {cpuArchitecture}{' '}
|
||||
{computeInstance?.variant?.meta?.cpu_dedicated ? '(Dedicated)' : '(Shared)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscr
|
||||
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
import { Alert, Button, IconExternalLink, IconInfo, Modal, Radio, SidePanel } from 'ui'
|
||||
|
||||
@@ -106,6 +107,7 @@ const ComputeInstanceSidePanel = () => {
|
||||
const isSubmitting = isUpdating || isRemoving
|
||||
|
||||
const projectId = selectedProject?.id
|
||||
const cpuArchitecture = getCloudProviderArchitecture(selectedProject?.cloud_provider)
|
||||
const selectedAddons = addons?.selected_addons ?? []
|
||||
const availableAddons = addons?.available_addons ?? []
|
||||
|
||||
@@ -299,7 +301,7 @@ const ComputeInstanceSidePanel = () => {
|
||||
<div className="px-4 py-2">
|
||||
<p className="text-scale-1000">{option.meta?.memory_gb ?? 0} GB memory</p>
|
||||
<p className="text-scale-1000">
|
||||
{option.meta?.cpu_cores ?? 0}-core ARM CPU (
|
||||
{option.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU (
|
||||
{option.meta?.cpu_dedicated ? 'Dedicated' : 'Shared'})
|
||||
</p>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
@@ -354,7 +356,7 @@ const ComputeInstanceSidePanel = () => {
|
||||
|
||||
{selectedCategory === 'micro' && (
|
||||
<p className="text-sm text-scale-1100">
|
||||
Your database will use the standard Micro size instance of 2-core ARM CPU (Shared)
|
||||
Your database will use the standard Micro size instance of 2-core {cpuArchitecture} CPU (Shared)
|
||||
with 1GB of memory.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -125,7 +125,7 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
|
||||
const [updates, setUpdates] = useState<any>({
|
||||
pool_mode: bouncerInfo.pool_mode || 'transaction',
|
||||
default_pool_size: bouncerInfo.default_pool_size || '',
|
||||
default_pool_size: bouncerInfo.default_pool_size || undefined,
|
||||
ignore_startup_parameters: bouncerInfo.ignore_startup_parameters || '',
|
||||
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
|
||||
})
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
import { useParams } from 'common'
|
||||
import Divider from 'components/ui/Divider'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import Connecting from 'components/ui/Loading'
|
||||
import MultiSelect from 'components/ui/MultiSelect'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
@@ -51,19 +51,48 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
const [uploadedDataUrls, setUploadedDataUrls] = useState<string[]>([])
|
||||
const [selectedServices, setSelectedServices] = useState<string[]>([])
|
||||
|
||||
const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
|
||||
const {
|
||||
data: organizations,
|
||||
isLoading: isLoadingOrganizations,
|
||||
isError: isErrorOrganizations,
|
||||
isSuccess: isSuccessOrganizations,
|
||||
} = useOrganizationsQuery()
|
||||
// for use in useEffect
|
||||
const organizationsRef = useLatest(organizations)
|
||||
|
||||
const { data: allProjects, isSuccess: isProjectsSuccess } = useProjectsQuery()
|
||||
const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({
|
||||
projectRef: ref,
|
||||
})
|
||||
const {
|
||||
data: allProjects,
|
||||
isLoading: isLoadingProjects,
|
||||
isError: isErrorProjects,
|
||||
isSuccess: isSuccessProjects,
|
||||
} = useProjectsQuery()
|
||||
|
||||
const isInitialized = isOrganizationsSuccess && isProjectsSuccess
|
||||
const projectDefaults: Partial<Project>[] = [{ ref: 'no-project', name: 'No specific project' }]
|
||||
|
||||
const projects = [...(allProjects ?? []), ...projectDefaults]
|
||||
const selectedProjectFromUrl = projects.find((project) => project.ref === ref)
|
||||
const selectedCategoryFromUrl = CATEGORY_OPTIONS.find((option) => {
|
||||
if (option.value.toLowerCase() === ((category as string) ?? '').toLowerCase()) return option
|
||||
})
|
||||
|
||||
const selectedProjectRef =
|
||||
selectedProjectFromUrl !== undefined
|
||||
? selectedProjectFromUrl.ref
|
||||
: projects.length > 0
|
||||
? projects[0].ref
|
||||
: 'no-project'
|
||||
const selectedOrganizationSlug =
|
||||
selectedProjectRef !== 'no-project'
|
||||
? organizations?.find((org) => {
|
||||
const project = projects.find((project) => project.ref === selectedProjectRef)
|
||||
return org.id === project?.organization_id
|
||||
})?.slug
|
||||
: organizations?.[0]?.slug
|
||||
|
||||
const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: selectedProjectRef },
|
||||
{ enabled: selectedProjectRef !== 'no-project' }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!uploadedFiles) return
|
||||
@@ -78,37 +107,14 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
const { profile } = useProfile()
|
||||
const respondToEmail = profile?.primary_email ?? 'your email'
|
||||
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className="w-[622px] py-48">
|
||||
<Connecting />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const selectedProject = projects.find((project) => project.ref === ref)
|
||||
const selectedCategory = CATEGORY_OPTIONS.find((option) => {
|
||||
if (option.value.toLowerCase() === ((category as string) ?? '').toLowerCase()) return option
|
||||
})
|
||||
|
||||
const initialProjectRef =
|
||||
selectedProject !== undefined
|
||||
? selectedProject.ref
|
||||
: projects.length > 0
|
||||
? projects[0].ref
|
||||
: 'no-project'
|
||||
const initialOrganizationSlug =
|
||||
initialProjectRef !== 'no-project'
|
||||
? organizations?.find((org) => {
|
||||
const project = projects.find((project) => project.ref === initialProjectRef)
|
||||
return org.id === project?.organization_id
|
||||
})?.slug
|
||||
: organizations?.[0]?.slug
|
||||
const initialValues = {
|
||||
category: selectedCategory !== undefined ? selectedCategory.value : CATEGORY_OPTIONS[0].value,
|
||||
category:
|
||||
selectedCategoryFromUrl !== undefined
|
||||
? selectedCategoryFromUrl.value
|
||||
: CATEGORY_OPTIONS[0].value,
|
||||
severity: 'Low',
|
||||
projectRef: initialProjectRef,
|
||||
organizationSlug: initialOrganizationSlug,
|
||||
projectRef: selectedProjectRef,
|
||||
organizationSlug: selectedOrganizationSlug,
|
||||
library: 'no-library',
|
||||
subject: subject ?? '',
|
||||
message: message || '',
|
||||
@@ -235,6 +241,23 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
}
|
||||
}, [values.projectRef])
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (
|
||||
isSuccessProjects &&
|
||||
isSuccessOrganizations &&
|
||||
allProjects.length > 0 &&
|
||||
organizations.length > 0
|
||||
) {
|
||||
const updatedValues = {
|
||||
...values,
|
||||
projectRef: selectedProjectRef,
|
||||
organizationSlug: selectedOrganizationSlug,
|
||||
}
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
}
|
||||
}, [isSuccessProjects, isSuccessOrganizations])
|
||||
|
||||
return (
|
||||
<div className="space-y-8 w-[620px]">
|
||||
<div className="px-6">
|
||||
@@ -264,21 +287,40 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
|
||||
<div className="px-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Listbox id="projectRef" layout="vertical" label="Which project is affected?">
|
||||
{projects.map((option) => {
|
||||
const organization = organizations?.find((x) => x.id === option.organization_id)
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={`option-${option.ref}`}
|
||||
label={option.name || ''}
|
||||
value={option.ref}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
<span className="block text-xs opacity-50">{organization?.name}</span>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
{isLoadingProjects && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm prose">Which project is affected?</p>
|
||||
<ShimmeringLoader className="!py-[19px]" />
|
||||
</div>
|
||||
)}
|
||||
{isErrorProjects && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm prose">Which project is affected?</p>
|
||||
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
|
||||
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
|
||||
<p className="text-sm prose">Failed to retrieve projects</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isSuccessProjects && (
|
||||
<Listbox id="projectRef" layout="vertical" label="Which project is affected?">
|
||||
{projects.map((option) => {
|
||||
const organization = organizations?.find(
|
||||
(x) => x.id === option.organization_id
|
||||
)
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={`option-${option.ref}`}
|
||||
label={option.name || ''}
|
||||
value={option.ref}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
<span className="block text-xs opacity-50">{organization?.name}</span>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
)}
|
||||
<Listbox id="severity" layout="vertical" label="Severity">
|
||||
{SEVERITY_OPTIONS.map((option: any) => {
|
||||
return (
|
||||
@@ -295,7 +337,8 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
})}
|
||||
</Listbox>
|
||||
</div>
|
||||
{subscription ? (
|
||||
|
||||
{values.projectRef !== 'no-project' && subscription && isSuccessProjects ? (
|
||||
<p className="text-sm text-scale-1000 mt-2">
|
||||
This project is on the{' '}
|
||||
<span className="text-scale-1100">{subscription.plan.name} plan</span>
|
||||
@@ -310,25 +353,42 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{values.projectRef === 'no-project' && (organizations?.length ?? 0) > 0 && (
|
||||
{isSuccessProjects && values.projectRef === 'no-project' && (
|
||||
<div className="px-6">
|
||||
<Listbox
|
||||
id="organizationSlug"
|
||||
layout="vertical"
|
||||
label="Which organization is affected?"
|
||||
>
|
||||
{organizations?.map((option) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={`option-${option.slug}`}
|
||||
label={option.name || ''}
|
||||
value={option.slug}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
{isLoadingOrganizations && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm prose">Which organization is affected?</p>
|
||||
<ShimmeringLoader className="!py-[19px]" />
|
||||
</div>
|
||||
)}
|
||||
{isErrorOrganizations && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm prose">Which organization is affected?</p>
|
||||
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
|
||||
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
|
||||
<p className="text-sm prose">Failed to retrieve organizations</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isSuccessOrganizations && (
|
||||
<Listbox
|
||||
id="organizationSlug"
|
||||
layout="vertical"
|
||||
label="Which organization is affected?"
|
||||
>
|
||||
{organizations?.map((option) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={`option-${option.slug}`}
|
||||
label={option.name || ''}
|
||||
value={option.slug}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -369,13 +429,6 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
|
||||
|
||||
<Divider light />
|
||||
|
||||
{/* {values.category === 'Problem' && (
|
||||
<>
|
||||
<ClientLibrariesGuidance />
|
||||
<Divider light />
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{!isDisabled ? (
|
||||
<>
|
||||
{['Performance'].includes(values.category) && isFreeProject ? (
|
||||
|
||||
@@ -11,7 +11,7 @@ import { generateDocsMenu } from './DocsLayout.utils'
|
||||
|
||||
function DocsLayout({ title, children }: { title: string; children: ReactElement }) {
|
||||
const router = useRouter()
|
||||
const { meta } = useStore()
|
||||
const { ui, meta } = useStore()
|
||||
const { data, isLoading, error } = meta.openApi
|
||||
const selectedProject = useSelectedProject()
|
||||
|
||||
@@ -26,10 +26,10 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject?.ref && !isPaused) {
|
||||
if (ui.selectedProjectRef && !isPaused) {
|
||||
meta.openApi.load()
|
||||
}
|
||||
}, [selectedProject?.ref])
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -34,10 +34,10 @@ const SettingsLayout: FC<Props> = ({ title, children }) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (project?.ref) {
|
||||
if (ui.selectedProjectRef) {
|
||||
meta.extensions.load()
|
||||
}
|
||||
}, [project?.ref])
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
|
||||
@@ -128,6 +128,17 @@ export const generateSettingsMenu = (
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isVaultEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Vault',
|
||||
key: 'vault',
|
||||
url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/vault/secrets`,
|
||||
items: [],
|
||||
label: 'BETA',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
...(IS_PLATFORM
|
||||
|
||||
@@ -9,17 +9,20 @@ import { organizationKeys } from './keys'
|
||||
export type OrganizationBillingMigrationVariables = {
|
||||
organizationSlug?: string
|
||||
tier?: string
|
||||
paymentMethodId?: string
|
||||
}
|
||||
|
||||
export async function migrateBilling({
|
||||
organizationSlug,
|
||||
tier,
|
||||
paymentMethodId,
|
||||
}: OrganizationBillingMigrationVariables) {
|
||||
if (!organizationSlug) throw new Error('organizationSlug is required')
|
||||
if (!tier) throw new Error('tier is required')
|
||||
|
||||
const payload: { tier: string } = {
|
||||
const payload: { tier: string; payment_method_id?: string } = {
|
||||
tier,
|
||||
payment_method_id: paymentMethodId,
|
||||
}
|
||||
|
||||
const response = await post(
|
||||
|
||||
12
studio/lib/cloudprovider-utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { PROVIDERS } from "./constants";
|
||||
|
||||
export function getCloudProviderArchitecture(cloudProvider: string | undefined) {
|
||||
switch (cloudProvider) {
|
||||
case PROVIDERS.AWS.id:
|
||||
return 'ARM';
|
||||
case PROVIDERS.FLY.id:
|
||||
return 'x86 64-bit';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,8 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
|
||||
|
||||
const freePlanWithExceedingLimits =
|
||||
(isSelectFreeTier || orgSubscription?.plan?.id === 'free') && hasMembersExceedingFreeTierLimit
|
||||
((isSelectFreeTier && !billedViaOrg) || orgSubscription?.plan?.id === 'free') &&
|
||||
hasMembersExceedingFreeTierLimit
|
||||
|
||||
const canCreateProject = isAdmin && !freePlanWithExceedingLimits
|
||||
|
||||
@@ -559,7 +560,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
</Panel.Content>
|
||||
)}
|
||||
|
||||
{!isSelectFreeTier && (
|
||||
{!billedViaOrg && !isSelectFreeTier && (
|
||||
<>
|
||||
<Panel.Content className="border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
|
||||
<Toggle
|
||||
|
||||
@@ -34,8 +34,8 @@ const HooksPage: NextPageWithLayout = () => {
|
||||
const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
|
||||
|
||||
useEffect(() => {
|
||||
if (project?.ref) meta.hooks.load()
|
||||
}, [project?.ref])
|
||||
if (ui.selectedProjectRef) meta.hooks.load()
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
const enableHooksForProject = async () => {
|
||||
const res = await post(`${API_URL}/database/${ref}/hook-enable`, {})
|
||||
|
||||