Bring changes from 14058 and resolve conflicts

This commit is contained in:
Joshen Lim committed 2023-09-13 17:43:12 +08:00
commit ecdea366eb
132 files changed
+3076 -1481

No files matched your search

+3
View File
@@ -1,3 +1,6 @@
/packages/shared-data/pricing.ts @roryw10
/packages/shared-data/plans.ts @roryw10
/studio/ @supabase/Dashboard
/apps/www/ @supabase/Website
/apps/docs/ @supabase/docs
@@ -964,34 +964,9 @@ export const ai = {
url: '/guides/ai/structured-unstructured',
},
{
name: 'Quickstarts',
name: 'Learn',
url: undefined,
items: [
{ name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' },
{ name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' },
{
name: 'Generate Embeddings',
url: '/guides/ai/quickstarts/generate-text-embeddings',
},
{ name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
],
},
{
name: 'Python Client',
url: undefined,
items: [
{ name: 'API', url: '/guides/ai/python/api' },
{ name: 'Collections', url: '/guides/ai/python/collections' },
{ name: 'Indexes', url: '/guides/ai/python/indexes' },
{ name: 'Metadata', url: '/guides/ai/python/metadata' },
],
},
{
name: 'Guides',
url: undefined,
items: [
{ name: 'Managing collections', url: '/guides/ai/managing-collections' },
{ name: 'Vector columns', url: '/guides/ai/vector-columns' },
{ name: 'Vector indexes', url: '/guides/ai/vector-indexes', items: vectorIndexItems },
{ name: 'Engineering for scale', url: '/guides/ai/engineering-for-scale' },
@@ -1000,25 +975,23 @@ export const ai = {
],
},
{
name: 'Examples',
name: 'JavaScript Examples',
url: undefined,
items: [
{
name: 'OpenAI completions using Edge Functions',
url: '/guides/ai/examples/openai',
},
{
name: 'Image search with OpenAI CLIP',
url: '/guides/ai/examples/image-search-openai-clip',
},
{
name: 'Generate image captions using Hugging Face',
url: '/guides/ai/examples/huggingface-image-captioning',
},
{
name: 'Building ChatGPT Plugins',
url: '/guides/ai/examples/building-chatgpt-plugins',
name: 'Generate Embeddings',
url: '/guides/ai/quickstarts/generate-text-embeddings',
},
{
name: 'Adding generative Q&A to your documentation',
url: '/guides/ai/examples/headless-vector-search',
@@ -1029,6 +1002,36 @@ export const ai = {
},
],
},
{
name: 'Python Client',
url: undefined,
items: [
{ name: 'Choosing a Client', url: '/guides/ai/python-clients' },
{ name: 'API', url: '/guides/ai/python/api' },
{ name: 'Collections', url: '/guides/ai/python/collections' },
{ name: 'Indexes', url: '/guides/ai/python/indexes' },
{ name: 'Metadata', url: '/guides/ai/python/metadata' },
],
},
{
name: 'Python Examples',
url: undefined,
items: [
{ name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' },
{ name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' },
{ name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
{
name: 'Image search with OpenAI CLIP',
url: '/guides/ai/examples/image-search-openai-clip',
},
{
name: 'Building ChatGPT Plugins',
url: '/guides/ai/examples/building-chatgpt-plugins',
},
],
},
{
name: 'Third-Party Tools',
url: undefined,
-4
View File
@@ -8,7 +8,6 @@ import Head from 'next/head'
import { PropsWithChildren, memo } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
import { Announcement, LW8CountdownBanner } from 'ui'
const levelsData = {
home: {
@@ -327,9 +326,6 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
<title>Supabase Docs</title>
</Head>
<main>
<Announcement>
<LW8CountdownBanner />
</Announcement>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
@@ -17,7 +17,122 @@ You have two options for scaling your vector workload:
## Dimensionality
The number of dimensions in your embeddings is the most important factor in choosing the right Compute Add-on. In general, the lower the dimensionality the better the performance. We've provided guidance for some of the more common embedding dimensions below. For each benchmark, we used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create an `inner-product` index for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:
The number of dimensions in your embeddings is the most important factor in choosing the right Compute Add-on. In general, the lower the dimensionality the better the performance. We've provided guidance for some of the more common embedding dimensions below. For each benchmark, we used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create both the `IVFFlat` and `HNSW` indexes for `inner-product` distance measure for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:
## HNSW
### 1536 Dimensions
This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. And 224,482 embeddings from [Wikipedia articles](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings) for compute add-ons `large` and below. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="openai1536"
>
<TabPanel id="openai1536" label="OpenAI-1536">
| Plan | Vectors | m | ef_construction | ef_search | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 15,000 | 16 | 40 | 40 | 480 | 0.011 sec | 0.016 sec | 1 GB + 200 Mb Swap | 1 GB |
| Small | 50,000 | 32 | 64 | 100 | 175 | 0.031 sec | 0.051 sec | 2 GB + 200 Mb Swap | 2 GB |
| Medium | 100,000 | 32 | 64 | 100 | 240 | 0.083 sec | 0.126 sec | 4 GB | 4 GB |
| Large | 224,482 | 32 | 64 | 100 | 280 | 0.017 sec | 0.028 sec | 8 GB | 8 GB |
| XL | 500,000 | 24 | 56 | 100 | 360 | 0.055 sec | 0.135 sec | 13 GB | 16 GB |
| 2XL | 1,000,000 | 24 | 56 | 250 | 560 | 0.036 sec | 0.058 sec | 32 GB | 32 GB |
| 4XL | 1,000,000 | 24 | 56 | 250 | 950 | 0.021 sec | 0.033 sec | 39 GB | 64 GB |
| 8XL | 1,000,000 | 24 | 56 | 250 | 1650 | 0.016 sec | 0.023 sec | 40 GB | 128 GB |
| 12XL | 1,000,000 | 24 | 56 | 250 | 1900 | 0.015 sec | 0.021 sec | 38 GB | 192 GB |
| 16XL | 1,000,000 | 24 | 56 | 250 | 2200 | 0.015 sec | 0.020 sec | 40 GB | 256 GB |
Accuracy was 0.99 for benchmarks.
QPS can also be improved by increasing [`m` and `ef_construction`](/docs/guides/ai/going-to-prod#hnsw-understanding-efconstruction--efsearch--and-m). This will allow you to use a smaller value for `ef_search` and increase QPS. For example, increasing `m` to 32 and `ef_construction` to 80 for 4XL will increase QPS to 1280.
</TabPanel>
</Tabs>
<Admonition type="note">
It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.
</Admonition>
## IVFFlat
### 512 Dimensions
This benchmark uses the [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) dataset, which contains 1,623,397 embeddings of text. Each embedding is 512 dimensions. Random vectors were generated for queries.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="glove512"
>
<TabPanel id="glove512" label="GloVe-512, probes = 10">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 100,000 | 100 | 250 | 0.395 sec | 0.432 sec | 1 GB + 300 Mb Swap | 1 GB |
| Small | 250,000 | 250 | 440 | 0.223 sec | 0.250 sec | 2 GB + 200 Mb Swap | 2 GB |
| Medium | 500,000 | 500 | 425 | 0.116 sec | 0.143 sec | 3.7 GB | 4 GB |
| Large | 1,000,000 | 1000 | 515 | 0.096 sec | 0.116 sec | 7.5 GB | 8 GB |
| XL | 1,623,397 | 1275 | 465 | 0.212 sec | 0.272 sec | 14 GB | 16 GB |
| 2XL | 1,623,397 | 1275 | 1400 | 0.061 sec | 0.075 sec | 22 GB | 32 GB |
| 4XL | 1,623,397 | 1275 | 1800 | 0.027 sec | 0.043 sec | 20 GB | 64 GB |
| 8XL | 1,623,397 | 1275 | 2850 | 0.032 sec | 0.049 sec | 21 GB | 128 GB |
| 12XL | 1,623,397 | 1275 | 3700 | 0.020 sec | 0.036 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 3700 | 0.025 sec | 0.042 sec | 29 GB | 256 GB |
</TabPanel>
<TabPanel id="glove512_60" label="GloVe-512, probes = 60">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Free | 100,000 | 100 | - | - | - | - | 1 GB |
| Small | 250,000 | 250 | - | - | - | - | 2 GB |
| Medium | 500,000 | 500 | 75 | 0.656 sec | 0.750 sec | 3.7 GB | 4 GB |
| Large | 1,000,000 | 1000 | 102 | 0.488 sec | 0.580 sec | 7.5 GB | 8 GB |
| XL | 1,000,000 | 1000 | 188 | 0.525 sec | 0.596 sec | 14 GB | 16 GB |
| XL | 1,623,397 | 1275 | 75 | 0.679 sec | 0.798 sec | 14 GB | 16 GB |
| 2XL | 1,623,397 | 1275 | 160 | 0.314 sec | 0.384 sec | 22 GB | 32 GB |
| 4XL | 1,623,397 | 1275 | 300 | 0.083 sec | 0.113 sec | 20 GB | 64 GB |
| 8XL | 1,623,397 | 1275 | 565 | 0.105 sec | 0.141 sec | 21 GB | 128 GB |
| 12XL | 1,623,397 | 1275 | 840 | 0.093 sec | 0.124 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 940 | 0.084 sec | 0.108 sec | 29 GB | 256 GB |
</TabPanel>
</Tabs>
### 960 Dimensions
This benchmark uses the [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gist960"
>
<TabPanel id="gist960" label="gist-960, probes = 10">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 30,000 | 30 | 75 | 0.065 sec | 0.088 sec | 1 GB + 100 Mb Swap | 1 GB |
| Small | 100,000 | 100 | 78 | 0.064 sec | 0.092 sec | 1.8 GB | 2 GB |
| Medium | 250,000 | 250 | 58 | 0.085 sec | 0.129 sec | 3.2 GB | 4 GB |
| Large | 500,000 | 500 | 55 | 0.088 sec | 0.140 sec | 5 GB | 8 GB |
| XL | 1,000,000 | 1000 | 110 | 0.046 sec | 0.070 sec | 14 GB | 16 GB |
| 2XL | 1,000,000 | 1000 | 235 | 0.083 sec | 0.136 sec | 10 GB | 32 GB |
| 4XL | 1,000,000 | 1000 | 420 | 0.071 sec | 0.106 sec | 11 GB | 64 GB |
| 8XL | 1,000,000 | 1000 | 815 | 0.072 sec | 0.106 sec | 13 GB | 128 GB |
| 12XL | 1,000,000 | 1000 | 1150 | 0.052 sec | 0.078 sec | 15.5 GB | 192 GB |
| 16XL | 1,000,000 | 1000 | 1345 | 0.072 sec | 0.106 sec | 17.5 GB | 256 GB |
</TabPanel>
</Tabs>
### 1536 Dimensions
@@ -31,7 +146,7 @@ This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/data
>
<TabPanel id="dbpedia1536" label="OpenAI-1536, probes = 10">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 20,000 | 40 | 135 | 0.372 sec | 0.412 sec | 1 GB + 200 Mb Swap | 1 GB |
| Small | 50,000 | 100 | 140 | 0.357 sec | 0.398 sec | 1.8 GB | 2 GB |
@@ -44,12 +159,12 @@ This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/data
| 12XL | 1,000,000 | 2000 | 1600 | 0.030 sec | 0.052 sec | 41 GB | 192 GB |
| 16XL | 1,000,000 | 2000 | 1790 | 0.029 sec | 0.051 sec | 45 GB | 256 GB |
For 1,000,000 vectors 10 probes results to precision of 0.91. And for 500,000 vectors and below 10 probes results to precision in the range of 0.95 - 0.99. To increase precision, you need to increase the number of probes.
For 1,000,000 vectors 10 probes results to accuracy of 0.91. And for 500,000 vectors and below 10 probes results to accuracy in the range of 0.95 - 0.99. To increase accuracy, you need to increase the number of probes.
</TabPanel>
<TabPanel id="dbpedia1536_40" label="OpenAI-1536, probes = 40">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Free | 20,000 | 40 | - | - | - | - | 1 GB |
| Small | 50,000 | 100 | - | - | - | - | 2 GB |
@@ -62,7 +177,7 @@ For 1,000,000 vectors 10 probes results to precision of 0.91. And for 500,000 ve
| 12XL | 1,000,000 | 2000 | 600 | 0.085 sec | 0.132 sec | 41 GB | 192 GB |
| 16XL | 1,000,000 | 2000 | 670 | 0.081 sec | 0.129 sec | 45 GB | 256 GB |
For 1,000,000 vectors 40 probes results to precision of 0.98. Note that exact values may vary depending on the dataset and queries, we recommend to run benchmarks with your own data to get precise results. Use this table as a reference.
For 1,000,000 vectors 40 probes results to accuracy of 0.98. Note that exact values may vary depending on the dataset and queries, we recommend to run benchmarks with your own data to get precise results. Use this table as a reference.
</TabPanel>
</Tabs>
@@ -80,82 +195,9 @@ For 1,000,000 vectors 40 probes results to precision of 0.98. Note that exact va
/>
</div>
### 960 Dimensions
This benchmark uses the [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gist960"
>
<TabPanel id="gist960" label="gist-960, probes = 10">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 30,000 | 30 | 75 | 0.065 sec | 0.088 sec | 1 GB + 100 Mb Swap | 1 GB |
| Small | 100,000 | 100 | 78 | 0.064 sec | 0.092 sec | 1.8 GB | 2 GB |
| Medium | 250,000 | 250 | 58 | 0.085 sec | 0.129 sec | 3.2 GB | 4 GB |
| Large | 500,000 | 500 | 55 | 0.088 sec | 0.140 sec | 5 GB | 8 GB |
| XL | 1,000,000 | 1000 | 110 | 0.046 sec | 0.070 sec | 14 GB | 16 GB |
| 2XL | 1,000,000 | 1000 | 235 | 0.083 sec | 0.136 sec | 10 GB | 32 GB |
| 4XL | 1,000,000 | 1000 | 420 | 0.071 sec | 0.106 sec | 11 GB | 64 GB |
| 8XL | 1,000,000 | 1000 | 815 | 0.072 sec | 0.106 sec | 13 GB | 128 GB |
| 12XL | 1,000,000 | 1000 | 1150 | 0.052 sec | 0.078 sec | 15.5 GB | 192 GB |
| 16XL | 1,000,000 | 1000 | 1345 | 0.072 sec | 0.106 sec | 17.5 GB | 256 GB |
</TabPanel>
</Tabs>
### 512 Dimensions
This benchmark uses the [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) dataset, which contains 1,623,397 embeddings of text. Each embedding is 512 dimensions. Random vectors were generated for queries.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="glove512"
>
<TabPanel id="glove512" label="GloVe-512, probes = 10">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 100,000 | 100 | 250 | 0.395 sec | 0.432 sec | 1 GB + 300 Mb Swap | 1 GB |
| Small | 250,000 | 250 | 440 | 0.223 sec | 0.250 sec | 2 GB + 200 Mb Swap | 2 GB |
| Medium | 500,000 | 500 | 425 | 0.116 sec | 0.143 sec | 3.7 GB | 4 GB |
| Large | 1,000,000 | 1000 | 515 | 0.096 sec | 0.116 sec | 7.5 GB | 8 GB |
| XL | 1,623,397 | 1275 | 465 | 0.212 sec | 0.272 sec | 14 GB | 16 GB |
| 2XL | 1,623,397 | 1275 | 1400 | 0.061 sec | 0.075 sec | 22 GB | 32 GB |
| 4XL | 1,623,397 | 1275 | 1800 | 0.027 sec | 0.043 sec | 20 GB | 64 GB |
| 8XL | 1,623,397 | 1275 | 2850 | 0.032 sec | 0.049 sec | 21 GB | 128 GB |
| 12XL | 1,623,397 | 1275 | 3700 | 0.020 sec | 0.036 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 3700 | 0.025 sec | 0.042 sec | 29 GB | 256 GB |
</TabPanel>
<TabPanel id="glove512_60" label="GloVe-512, probes = 60">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Free | 100,000 | 100 | - | - | - | - | 1 GB |
| Small | 250,000 | 250 | - | - | - | - | 2 GB |
| Medium | 500,000 | 500 | 75 | 0.656 sec | 0.750 sec | 3.7 GB | 4 GB |
| Large | 1,000,000 | 1000 | 102 | 0.488 sec | 0.580 sec | 7.5 GB | 8 GB |
| XL | 1,000,000 | 1000 | 188 | 0.525 sec | 0.596 sec | 14 GB | 16 GB |
| XL | 1,623,397 | 1275 | 75 | 0.679 sec | 0.798 sec | 14 GB | 16 GB |
| 2XL | 1,623,397 | 1275 | 160 | 0.314 sec | 0.384 sec | 22 GB | 32 GB |
| 4XL | 1,623,397 | 1275 | 300 | 0.083 sec | 0.113 sec | 20 GB | 64 GB |
| 8XL | 1,623,397 | 1275 | 565 | 0.105 sec | 0.141 sec | 21 GB | 128 GB |
| 12XL | 1,623,397 | 1275 | 840 | 0.093 sec | 0.124 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 940 | 0.084 sec | 0.108 sec | 29 GB | 256 GB |
</TabPanel>
</Tabs>
<Admonition type="note">
It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: RPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.
It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.
</Admonition>
+61 -18
View File
@@ -8,35 +8,77 @@ export const meta = {
sidebar_label: 'Going to Production',
}
This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal precision for your use case.
This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
See our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide for more information about engineering at scale.
## Do you need indexes?
Sequential scans will result in significantly higher latencies and lower throughput, guaranteeing 100% precision and not being RAM bound.
Sequential scans will result in significantly higher latencies and lower throughput, guaranteeing 100% accuracy and not being RAM bound.
There are a couple of cases where you might not need indexes:
- You have a small dataset and don't need to scale it.
- You are not expecting high amounts of vector search queries per second.
- You need to guarantee 100% precision.
- You need to guarantee 100% accuracy.
You don't have to create indexes in these cases and can use sequential scans instead. This type of workload will not be RAM bound and will not require any additional resources but will result in higher latencies and lower throughput. Extra CPU cores may help to improve queries per second, but it will not help to improve latency.
On the other hand, if you need to scale your application, you will need to create indexes. This will result in lower latencies and higher throughput, but will require additional RAM to make use of Postgres Caching. Also, using indexes will result in lower precision, since you are replacing exact (KNN) search with approximate (ANN) search.
On the other hand, if you need to scale your application, you will need to [create indexes](/docs/guides/ai/vector-indexes). This will result in lower latencies and higher throughput, but will require additional RAM to make use of Postgres Caching. Also, using indexes will result in lower accuracy, since you are replacing exact (KNN) search with approximate (ANN) search.
## Understanding `probes` and `lists`
## HNSW vs IVFFlat indexes
`pgvector` supports two types of indexes: HNSW and IVFFlat. We recommend using [HNSW](/docs/guides/ai/vector-indexes/hnsw-indexes) because of its [performance](https://supabase.com/blog/increase-performance-pgvector-hnsw#hnsw-performance-1536-dimensions) and [robustness against changing data](/docs/guides/ai/vector-indexes/hnsw-indexes#when-should-you-create-hnsw-indexes).
<div>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (light)"
className="dark:hidden"
src="/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--light.png"
/>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (dark)"
className="hidden dark:block"
src="/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--dark.png"
/>
</div>
## HNSW, understanding `ef_construction`, `ef_search`, and `m`
Index build parameters:
- `m` is the number of bi-directional links created for every new element during construction. Higher `m` is suitable for datasets with high dimensionality and/or high accuracy requirements. Reasonable values for `m` are between 2 and 100. Range 12-48 is a good starting point for most use cases (16 is the default value).
- `ef_construction` is the size of the dynamic list for the nearest neighbors (used during the construction algorithm). Higher `ef_construction` will result in better index quality and higher accuracy, but it will also increase the time required to build the index. `ef_construction` has to be at least 2 \* `m` (64 is the default value). At some point, increasing `ef_construction` does not improve the quality of the index. You can measure accuracy when `ef_search`=`ef_construction`: if accuracy is lower than 0.9, then there is room for improvement.
Search parameters:
- `ef_search` is the size of the dynamic list for the nearest neighbors (used during the search). Increasing `ef_search` will result in better accuracy, but it will also increase the time required to execute a query (40 is the default value).
<div>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using different build parameters (light)"
className="dark:hidden"
src="/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png"
/>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using different build parameters (dark)"
className="hidden dark:block"
src="/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png"
/>
</div>
## IVFFlat, understanding `probes` and `lists`
Indexes used for approximate vector similarity search in pgvector divides a dataset into partitions. The number of these partitions is defined by the `lists` constant. The `probes` controls how many lists are going to be searched during a query.
The values of lists and probes directly affect precision and requests per second (RPS).
The values of lists and probes directly affect accuracy and queries per second (QPS).
- Higher `lists` means an index will be built slower, but you can achieve better RPS and precision.
- Higher `probes` means that select queries will be slower, but you can achieve better precision.
- `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same precision.
- Higher `lists` means an index will be built slower, but you can achieve better QPS and accuracy.
- Higher `probes` means that select queries will be slower, but you can achieve better accuracy.
- `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same accuracy.
You can find more examples of how `lists` and `probes` constants affect precision and RPS in [pgvector 0.4.0 performance](https://supabase.com/blog/pgvector-performance) blogpost.
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](https://supabase.com/blog/pgvector-performance) blogpost.
<div>
<img
@@ -58,21 +100,22 @@ First, a few generic tips which you can pick and choose from:
1. The Supabase managed platform will automatically optimize Postgres configs for you based on your compute addon. But if you self-host, consider **adjusting your Postgres config** based on RAM & CPU cores. See [example optimizations](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08) for more details.
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 before transitioning to production or running benchmarks.
- Execute 10,000 to 50,000 "warm-up" queries before each benchmark, matching the number of `probes` you are going to use in production. Additionally, you can execute about 1,000 queries with probes ranging from three to ten times the prod's probes. Both of these help to increase RAM utilization.
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 1,000,000 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 requests per second (RPS).
- Use [pg_prewarm](https://www.postgresql.org/docs/current/pgprewarm.html) to load the index into RAM `select pg_prewarm('vecs.docs_vec_idx');`. This will help to avoid cold cache issues.
- Execute 10,000 to 50,000 "warm-up" queries before each benchmark/prod. This will help to utilize cache and buffers more efficiently.
4. **Establish your workload.** Finetune `m` and `ef_construction` or `lists` constants for the pgvector index to accelerate your queries (at the expense of a slower build times). For instance, for benchmarks with 1,000,000 OpenAI embeddings, we set `m` and `ef_construction` to 32 and 80, and it resulted in 35% higher QPS than 24 and 56 values respectively.
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the index build parameters, balancing accuracy with queries per second (QPS).
## Going into production
1. Decide if you are going to use indexes or not. You can skip the rest of this guide if you do not use indexes.
2. 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.)
3. Upload your data to the database. If you use the [`vecs`](/docs/guides/ai/python/api) library, it will automatically generate an index with default parameters.
4. Run a benchmark using randomly generated queries and observe the results. Again, you can use the `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. Run a benchmark using randomly generated queries and observe the results. Again, you can use the `vecs` library with the `ann-benchmarks` tool. Do it with default values for index build parameters, you can later adjust them to get the best results.
5. Monitor the RAM usage, and save it as a note for yourself. You would likely want to use a compute add-on in the future that has the same amount of RAM that was used at the moment (both actual RAM usage and RAM used for cache and buffers).
6. Scale down your compute add-on to the one that would have the same amount of RAM used at the moment.
7. Repeat step 3 to load the data into RAM. You should see RPS increase on subsequent runs, and stop when it no longer increases. Then repeat the benchmark with probes set to a higher value if you haven't already performed it for that compute add-on size.
8. Run a benchmark using real queries and observe the results. You can use the `vecs` library for that as well with `ann-benchmarks` tool. Set probes to 10 (default) and then gradually increase/decrease probes until you see that both precision and RPS match your requirements.
9. 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 a 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.
7. Repeat step 3 to load the data into RAM. You should see QPS increase on subsequent runs, and stop when it no longer increases.
8. Run a benchmark using real queries and observe the results. You can use the `vecs` library for that as well with `ann-benchmarks` tool. Tweak `ef_search` for HNSW or `probes` for IVFFlat until you see that both accuracy and QPS match your requirements.
9. If you want higher QPS you can increase `m` and `ef_construction` for HNSW or `lists` for IVFFlat parameters (consider switching from IVF to HNSW). You have to rebuild the index with a higher `m` and `ef_construction` values and repeat steps 6-7 to find the best combination of `m`, `ef_construction` and `ef_search` constants to achieve the best QPS and accuracy values. Higher `m`, `ef_construction` mean that index will build slower, but you can achieve better QPS and accuracy. Higher `ef_search` mean that select queries will be slower, but you can achieve better accuracy.
## Useful links
@@ -80,7 +123,7 @@ Don't forget to check out the general [Production Checklist](/docs/guides/platfo
You can look at our [Choosing Compute Add-on](/docs/guides/ai/choosing-compute-addon) guide to get a basic understanding of how much compute you might need for your workload.
Or take a look at our [pgvector 0.4.0 performance](https://supabase.com/blog/pgvector-performance) blog post to see what pgvector is capable of and how the above technique can be used to achieve the best results.
Or take a look at our [pgvector 0.5.0 performance](https://supabase.com/blog/increase-performance-pgvector-hnsw) and [pgvector 0.4.0 performance](https://supabase.com/blog/pgvector-performance) blog posts to see what pgvector is capable of and how the above technique can be used to achieve the best results.
<div>
<img
@@ -1,156 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-collections',
title: 'Managing collections',
description: 'Learn how to manage groups of vector records using the vecs Python library',
sidebar_label: 'Managing collections',
}
A collection is a group of vector records managed by the `vecs` Python library. Records can be added to or updated in a collection. Collections can be queried at any time, but should be indexed for scalable query performance.
Supabase provides a [Python client](/docs/guides/ai/vecs-python-client) called `vecs` for managing unstructured vector stores in Postgres. If you come from a data science background, this unstructured data approach will feel familiar. If you are more interested in a structured data approach, see [Vector columns](/docs/guides/ai/vector-columns) or read our guide on [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured).
Under the hood `vecs` will manage the necessary Postgres tables and columns to store and query your collections.
## API
Find the full API in the [official API docs](https://supabase.github.io/vecs/api).
### Connecting
Before you can interact with vecs, create the client to communicate with Postgres.
```python
import vecs
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
vx = vecs.create_client(DB_CONNECTION)
```
### Create collection
You can create a collection to store vectors specifying the collection's name and the number of dimensions in the vectors you intend to store.
```python
docs = vx.create_collection(name="docs", dimension=3)
```
If another collection exists with the same name,
### Get an existing collection
To access a previously created collection, use `get_collection` to retrieve it by name
```python
docs = vx.get_collection(name="docs")
```
### Upserting vectors
`vecs` combines the concepts of "insert" and "update" into "upsert". Upserting records adds them to the collection if the `id` is not present, or updates the existing record if the `id` does exist.
```python
# add records to the collection
docs.upsert(
vectors=[
(
"vec0", # the vector's identifier
[0.1, 0.2, 0.3], # the vector. list or np.array
{"year": 1973} # associated metadata
),
(
"vec1",
[0.7, 0.8, 0.9],
{"year": 2012}
)
]
)
```
### Create an index
Collections can be queried immediately after being created.
However, for good performance, the collection should be indexed after records have been upserted.
Indexes should be created **after** the collection has been populated with records. Building an index on an empty collection will significantly reduce recall. Once the index has been created you can still upsert new documents into the collection but you should rebuild the index if the size of the collection more than doubles.
Only one index may exist per-collection. By default, creating an index will replace any existing index.
To create an index:
```python
##
# INSERT RECORDS HERE
##
# index the collection to be queried by cosine distance
docs.create_index(measure=vecs.IndexMeasure.cosine_distance)
```
Available options for query `measure` are:
- `vecs.IndexMeasure.cosine_distance`
- `vecs.IndexMeasure.l2_distance`
- `vecs.IndexMeasure.max_inner_product`
which correspond to different methods for comparing query vectors to the vectors in the database.
If you aren't sure which to use, stick with the default (cosine_distance) by omitting the parameter i.e.: `docs.create_index()`.
<Admonition type="note">
The time required to create an index grows with the number of records and size of vectors. For a few thousand records expect sub-minute a response in under a minute. It may take a few minutes for larger collections.
</Admonition>
For an in-depth guide on vector indexes, see [Vector indexes](/docs/guides/ai/vector-indexes).
### Query
Be aware that indexes are essential for good performance. If you do not create an index, every query will return a warning that includes the `IndexMeasure` you should index.
#### Basic
The simplest form of search is to provide a query vector.
```python
docs.query(
query_vector=[0.4,0.5,0.6], # required
limit=5, # number of records to return
filters={}, # metadata filters
measure="cosine_distance", # distance measure to use
include_value=False, # should distance measure values be returned?
include_metadata=False, # should record metadata be returned?
)
```
Which returns a list of vector record `ids`.
#### Metadata Filtering
The metadata that is associated with each record can also be filtered during a query.
As an example, `{"year": {"$eq": 2005}}` filters a `year` metadata key to be equal to 2005
In context:
```python
docs.query(
query_vector=[0.4,0.5,0.6],
filters={"year": {"$eq": 2012}}, # metadata filters
)
```
For a complete reference, see the [metadata guide](https://supabase.github.io/vecs/concepts_metadata/).
## Resources
- Official Vecs Documentation: https://supabase.github.io/vecs/api
- Source Code: https://github.com/supabase/vecs
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,18 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-python-clients',
title: 'Choosing a Client',
description: 'Learn how to manage vectors using Python',
sidebar_label: 'Choosing a Client',
}
As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured), AI workloads come in many forms.
For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata.
For production python applications with version controlled migrations, we recommend adding first class vector support to your toolchain by [registering the vector type with your ORM](https://github.com/pgvector/pgvector-python). pgvector provides bindings for the most commonly used SQL drivers/libraries including Django, SQLAlchemy, SQLModel, psycopg, asyncpg and Peewee.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -79,7 +79,7 @@ docs.query(
## Deep Dive
For a more in-depth guide on `vecs` collections, see [Managing collections](/docs/guides/ai/managing-collections).
For a more in-depth guide on `vecs` collections, see [API](/docs/guides/ai/python/api).
## Resources
+1 -1
View File
@@ -7,7 +7,7 @@ export const meta = {
sidebar_label: 'Vector columns',
}
Supabase offers a number of different ways to store and query vectors within Postgres. If you prefer to use Python to store and query your vectors using collections, see [Managing collections](/docs/guides/ai/managing-collections). If you want more control over vectors within your own Postgres tables or would like to interact with them using a different language like JavaScript, keep reading.
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a PostgreSQL extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings).
@@ -30,8 +30,6 @@ Indexes can be used to improve performance of nearest neighbor search using vari
Currently vectors with up to 2,000 dimensions can be indexed.
If you are using the `vecs` Python library, follow the instructions in [Managing collections](/docs/guides/ai/managing-collections#create-an-index) to create indexes.
## Resources
Read more about indexing on `pgvector`'s [GitHub page](https://github.com/pgvector/pgvector#indexing).
@@ -41,8 +41,6 @@ create index on items using hnsw (column_name vector_cosine_ops);
Currently vectors with up to 2,000 dimensions can be indexed.
If you are using the `vecs` Python library, follow the instructions in [Managing collections](/docs/guides/ai/managing-collections#create-an-index) to create indexes.
## How does HNSW work?
HNSW uses proximity graphs (graphs connecting nodes based on distance between them) to approximate nearest-neighbor search. To understand HNSW, we can break it down into 2 parts:
@@ -50,8 +50,6 @@ create index on items using ivfflat (column_name vector_cosine_ops) with (lists
Currently vectors with up to 2,000 dimensions can be indexed.
If you are using the `vecs` Python library, follow the instructions in [Managing collections](/docs/guides/ai/managing-collections#create-an-index) to create indexes.
## How does IVFFlat work?
IVF stands for 'inverted file indexes'. It works by clustering your vectors in order to reduce the similarity search scope. Rather than comparing a vector to every other vector, the vector is only compared against vectors within the same cell cluster (or nearby clusters, depending on your configuration).
@@ -1868,8 +1868,6 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
- [SvelteKit example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit)
- [SvelteKit Email/Password example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit-email-password)
- [SvelteKit Magiclink example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit-magic-link)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -111,7 +111,7 @@ curl -X POST 'https://cvwawazfelidkloqmbma.supabase.co/auth/v1/signup' \
The user will now receive an SMS with a 6-digit pin that you will need to receive from them within 60-seconds before they can login to their account.
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`:
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`:
<Tabs
scrollable
@@ -122,7 +122,7 @@ You should present a form to the user so they can input the 6 digit pin, then se
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '+13334445555',
token: '123456',
})
@@ -237,7 +237,7 @@ The second step is the same as the previous section, you need to collect the 6-d
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '+13334445555',
token: '123456',
})
@@ -157,7 +157,7 @@ curl -X POST 'https://cvwawazfelidkloqmbma.supabase.co/auth/v1/signup' \
The user will now receive an SMS with a 6-digit pin that you will need to receive from them within 60-seconds before they can login to their account.
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`:
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`:
<Tabs
scrollable
@@ -104,7 +104,7 @@ curl -X POST 'https://xxx.supabase.co/auth/v1/signup' \
The user will now receive an SMS with a 6-digit pin that you will need to receive from them within 60-seconds before they can login to their account.
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`:
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`:
<Tabs
scrollable
@@ -115,7 +115,7 @@ You should present a form to the user so they can input the 6 digit pin, then se
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '491512223334444',
token: '123456',
})
@@ -230,7 +230,7 @@ The second step is the same as the previous section, you need to collect the 6-d
<TabPanel id="js" label="JavaScript">
```js
let { session, error } = await supabase.auth.verifyOTP({
let { session, error } = await supabase.auth.verifyOtp({
phone: '491512223334444',
token: '123456',
})
@@ -3,7 +3,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'organization-based-billing',
title: 'How billing works',
description: 'Learn how organzation-based billing works in Supabase.',
description: 'Learn how organization-based billing works in Supabase.',
subtitle: 'Learn how organzation-based billing works in Supabase.',
}
@@ -315,6 +315,15 @@ If you head over to your [organizations' billing settings](https://supabase.com/
infrastructure.
</details>
<details>
<summary>
<i>Where do I change my project add-ons such as PITR, Compute and Custom Domain?</i>
</summary>
Head over to your project [Add-ons page](https://supabase.com/dashboard/project/_/settings/addons) to change your compute size, Point-In-Time-Recovery or custom domain.
</details>
<details>
<summary>
<i>I have additional questions/concerns, how do I get help?</i>
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

@@ -17,7 +17,7 @@ thumb: 2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-thumb.png
_Contributed by: [Egor Romanov](https://github.com/egor-romanov)_
[Supabase Vector](https://supabase.com/vector) is about to get a lot faster. Starting today, new Supabase databases will ship with pgvector v0.5.0 which adds a new type of index: Hierarchical Navigable Small World (HNSW).
[Supabase Vector](https://supabase.com/vector) is about to get a lot faster. New Supabase databases will ship with pgvector v0.5.0 which adds a new type of index: Hierarchical Navigable Small World (HNSW).
HNSW is an algorithm for approximate nearest neighbor search, often used in high-dimensional spaces like those found in embeddings.
+15 -6
View File
@@ -3,10 +3,17 @@ import Link from 'next/link'
import { Badge } from 'ui'
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
const AnnouncementBadge = () => (
interface Props {
url: string
announcement: string
badge?: string
target?: '_self' | '_blank'
}
const AnnouncementBadge = ({ url, announcement, badge, target = '_self' }: Props) => (
<div className="w-full max-w-xl flex justify-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
<Link href="/launch-week">
<Link href={url}>
<a
target={target}
className="
group
relative
@@ -26,10 +33,12 @@ const AnnouncementBadge = () => (
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
"
>
<Badge color="brand" size="large" className="py-1">
Explore
</Badge>
<span className="text-foreground">Launch Week 8 announcements</span>
{badge && (
<Badge color="brand" size="large" className="py-1">
{badge}
</Badge>
)}
<span className="text-foreground">{announcement}</span>
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
<div
className="absolute inset-0 -z-10 bg-gradient-to-br
+4 -29
View File
@@ -8,7 +8,7 @@ import SectionContainer from '~/components/Layouts/SectionContainer'
import HeroFrameworks from './HeroFrameworks'
import styles from './hero.module.css'
import Image from 'next/image'
import AnnouncementBadge from '../Announcement/Badge'
// import AnnouncementBadge from '../Announcement/Badge'
const Hero = () => {
const router = useRouter()
@@ -19,7 +19,7 @@ const Hero = () => {
return (
<div className="relative -mt-[65px]">
<SectionContainer className="pt-8 md:pt-16 lg:pt-24 overflow-hidden">
<SectionContainer className="pt-8 md:pt-16 overflow-hidden">
<div className="relative">
<div className="mx-auto">
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
@@ -30,9 +30,9 @@ const Hero = () => {
].join(' ')}
>
<div className="flex flex-col items-center">
<div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
{/* <div className="z-40 w-full flex justify-center mb-8 lg:mb-8">
<AnnouncementBadge />
</div>
</div> */}
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
Build in a weekend
@@ -80,32 +80,7 @@ const Hero = () => {
</div>
</div>
</div>
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0 flex items-center justify-center">
<span className="absolute inset-0">
<Image
src="/images/launchweek/8/stars.svg"
alt="stars background"
layout="fill"
objectFit="cover"
className="opacity-70"
draggable={false}
/>
</span>
</div>
</SectionContainer>
<div className="absolute w-full max-w-[1600px] mx-auto h-[500px] lg:h-[750px] inset-0 z-0 flex items-center justify-center">
<Image
src="/images/launchweek/8/LW8-gradient.png"
alt="Launch Week 8 purple background gradient"
className="not-sr-only"
layout="fill"
objectFit="cover"
objectPosition="top"
priority
draggable={false}
/>
</div>
<div className="absolute top-0 left-0 right-0 h-screen bg-gradient-to-b from-[#020405] to-transparent -z-10" />
</div>
)
}
+5 -5
View File
@@ -32,7 +32,7 @@ const Developers = () => {
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d={icon} />
</svg>
)}
{Svg && <Svg />}
@@ -60,11 +60,11 @@ const Developers = () => {
return (
<div className="grid grid-cols-12">
<nav className="col-span-6 py-8" aria-labelledby="developersResources">
<div className="m-3 grid grid-cols-12 gap-4 py-4 pr-3">{iconSections}</div>
<nav className="col-span-6 py-6" aria-labelledby="developersResources">
<div className="m-3 grid grid-cols-12 gap-2 pr-3">{iconSections}</div>
</nav>
<div className="col-span-6">
<div className="m-3 mx-6">
<div className="col-span-6 py-8">
<div className="py-3 mx-6">
<p className="p">Latest announcements</p>
<ul className="mt-6 space-y-3">
{AnnouncementsData.map((announcement: any, idx: number) => (
+6 -18
View File
@@ -2,8 +2,7 @@ import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Announcement from '~/components/Announcement/Announcement'
import { Button, Badge, IconStar, IconChevronDown, LW8CountdownBanner } from 'ui'
import { Button, Badge, IconStar, IconChevronDown } from 'ui'
import FlyOut from '~/components/UI/FlyOut'
import Transition from 'lib/Transition'
@@ -198,9 +197,6 @@ const Nav = () => {
return (
<>
<Announcement link="/launch-week">
<LW8CountdownBanner />
</Announcement>
<div className="sticky top-0 z-40 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
<div
className={[
@@ -412,11 +408,6 @@ const Nav = () => {
Developers
</a>
</Link>
<Link href="/company">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Company
</a>
</Link>
<Link href="/pricing">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Pricing
@@ -430,14 +421,6 @@ const Nav = () => {
Docs
</a>
</Link>
<Link href="https://github.com/supabase/supabase">
<a
target="_blank"
className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white"
>
GitHub
</a>
</Link>
<Link href="/blog">
<a
target="_blank"
@@ -446,6 +429,11 @@ const Nav = () => {
Blog
</a>
</Link>
<Link href="/support">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Support
</a>
</Link>
</div>
<div className="p-3">
<p className="mb-6 text-sm text-scale-900">Products available:</p>
+15 -15
View File
@@ -1,4 +1,19 @@
[
{
"type": "Announcement",
"title": "Supabase Beta August 2023",
"description": "Launch Week 8 review and more things we shipped 🚀",
"imgUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
"logoUrl": "/images/blog/2023-09-07-beta-update-august-2023/monthly-update-august-2023.jpg",
"organization": "Supabase",
"url": "/blog/beta-update-august-2023",
"postMeta": {
"name": "Ant Wilson",
"publishDate": "Sep 08, 2023",
"readLength": 5
},
"ctaText": "Learn more"
},
{
"type": "Announcement",
"title": "Launch Week 8 Hackathon Winners",
@@ -13,20 +28,5 @@
"readLength": 3
},
"ctaText": "See the winners"
},
{
"type": "Announcement",
"title": "Supabase Launch Week 8",
"description": "One new feature (or more) every day during a week.",
"imgUrl": "/images/launchweek/8/lw8-og.jpg",
"logoUrl": "/images/launchweek/8/lw8-og.jpg",
"organization": "Supabase",
"url": "/launch-week",
"postMeta": {
"name": "Paul Copplestone",
"publishDate": "Aug 07, 2023",
"readLength": 5
},
"ctaText": "Learn more"
}
]
+12 -6
View File
@@ -1,38 +1,44 @@
export const links = [
{
text: 'Resources',
description: 'View examples, curated content from the community, migration guides, and more.',
description: 'View examples, curated content from the community, migration guides, and more',
url: '/docs/guides/resources',
icon: 'M19.4994 17.9996V7.50107L19.4864 3L14.0723 3L4.5 3L4.5 18.75M4.5 18.75C4.5 19.9926 5.50736 21 6.75 21H19.5V18.5255L19.4871 16.5H14.0728H6.75C5.50736 16.5 4.5 17.5074 4.5 18.75ZM7.50724 3V9L9.75771 6.45713L12.0072 9V3H7.50724Z',
},
{
text: 'Changelog',
description: 'See the latest updates and product improvements.',
description: 'See the latest updates and product improvements',
url: '/changelog',
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
},
{
text: 'Careers',
description: 'Join the Supabase team and get involved.',
description: 'Join the Supabase team and get involved',
url: '/careers',
icon: 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
},
{
text: 'Open Source',
description: 'We support existing open source tools and communities wherever possible.',
description: 'We support existing open source tools and communities wherever possible',
url: '/open-source',
icon: 'M10.7607 3.08859C15.7723 2.38592 20.0295 5.94789 20.8525 10.7642C21.5964 15.1122 19.4634 19.1692 15.8778 20.9967L14.0736 16.084C15.129 15.4346 15.8232 14.4223 16.0198 13.0819C16.3702 10.7219 14.7442 8.47426 12.3801 8.25648C10.2228 8.04047 8.33304 9.61601 7.99823 11.7702L7.99761 11.7743C7.73427 13.5096 8.47082 15.1847 9.92827 16.0841L8.12693 21C6.34571 20.1066 4.88125 18.6129 3.97963 16.7387L3.97879 16.7369C3.15872 15.0395 3.07406 13.7081 3.01524 12.783C3.01012 12.7024 3.0052 12.6249 3 12.5506C3.10941 7.30353 6.59243 3.6764 10.7607 3.08859Z',
},
{
text: 'Partners',
description: 'Become a Supabase Partner and enable new business opportunities.',
description: 'Become a Supabase Partner and enable new business opportunities',
url: '/partners',
icon: 'M12.0003 6.1488L10.5812 4.76334C8.68921 2.91613 5.62161 2.91613 3.72957 4.76334C1.98628 6.46532 1.84923 9.14165 3.31842 10.9968C3.44378 11.1551 3.77166 11.514 3.92041 11.6592M12.0003 6.1488L13.4193 4.76334C15.3113 2.91613 18.3789 2.91613 20.271 4.76334C22.163 6.61054 22.163 9.60546 20.271 11.4527L19.2017 12.5186M12.0003 6.1488L10.7542 7.39725C10.0214 8.13141 10.0219 9.32043 10.7554 10.0539C11.4894 10.7879 12.6793 10.7879 13.4133 10.0539L16.0693 7.39787M14.9131 13.4531L17.4954 16.1809M17.4954 16.1809C18.2089 16.9347 18.1764 18.1242 17.4226 18.8378C16.6693 19.5509 15.4807 19.5189 14.7669 18.7663L13.3188 17.2217M17.4954 16.1809C18.2086 16.9344 19.3982 16.9676 20.1516 16.2543C20.9051 15.5411 20.9376 14.3521 20.2244 13.5987L17.6415 10.8703M6.13555 14.7589L7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 4.21251 15.4925 3.47889 14.7589C2.74528 14.0253 2.74528 12.8358 3.47889 12.1022L4.80722 10.7739C5.54084 10.0403 6.73026 10.0403 7.46388 10.7739C8.19749 11.5075 8.19749 12.6969 7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 5.40193 16.6819 6.13555 17.4155C6.86916 18.1492 8.05859 18.1492 8.7922 17.4155M7.46387 13.4306C8.19749 12.6969 9.38691 12.6969 10.1205 13.4306C10.8541 14.1642 10.8541 15.3536 10.1205 16.0872M8.7922 17.4155L10.1205 16.0872M8.7922 17.4155C8.05859 18.1492 8.05859 19.3386 8.7922 20.0722C9.52582 20.8058 10.7152 20.8058 11.4489 20.0722L12.7772 18.7439C13.5108 18.0102 13.5108 16.8208 12.7772 16.0872C12.0436 15.3536 10.8541 15.3536 10.1205 16.0872',
},
{
text: 'Integrations',
description: 'Use your favorite tools with Supabase.',
description: 'Use your favorite tools with Supabase',
url: '/partners/integrations',
icon: 'M6.87232 21.5743C9.09669 21.5743 10.8999 19.7711 10.8999 17.5467C10.8999 15.3223 9.09669 13.5191 6.87232 13.5191C4.64794 13.5191 2.84473 15.3223 2.84473 17.5467C2.84473 19.7711 4.64794 21.5743 6.87232 21.5743Z M17.127 3.67236V11.1724M20.877 7.42274H13.377M3.12305 3.67236H10.6231V11.1724H3.12305V3.67236ZM13.377 13.7966H20.877V21.2966H13.377V13.7966ZM10.8999 17.5467C10.8999 19.7711 9.09669 21.5743 6.87232 21.5743C4.64794 21.5743 2.84473 19.7711 2.84473 17.5467C2.84473 15.3223 4.64794 13.5191 6.87232 13.5191C9.09669 13.5191 10.8999 15.3223 10.8999 17.5467Z',
},
{
text: 'Support',
description: 'The Supabase Support Team is ready to help',
url: '/support',
icon: 'M8.72766 9C9.27678 7.83481 10.7584 7 12.5001 7C14.7092 7 16.5001 8.34315 16.5001 10C16.5001 11.3994 15.2224 12.5751 13.4943 12.9066C12.9519 13.0107 12.5001 13.4477 12.5001 14M12.5 17H12.51M21.5 12C21.5 16.9706 17.4706 21 12.5 21C7.52944 21 3.5 16.9706 3.5 12C3.5 7.02944 7.52944 3 12.5 3C17.4706 3 21.5 7.02944 21.5 12Z',
},
]
+4
View File
@@ -63,6 +63,10 @@
"text": "Brand Assets / Logos",
"url": "/brand-assets"
},
{
"text": "Security and Compliance",
"url": "/security"
},
{
"text": "DPA",
"url": "/legal/dpa"
+5 -5
View File
@@ -51,8 +51,8 @@ const Partners = () => {
<path
d="M91 1C120.005 1 145.841 14.5702 162.505 35.708"
stroke="url(#paint0_linear_4766_6117)"
stroke-width="2"
stroke-linecap="round"
strokeWidth="2"
strokeLinecap="round"
/>
<defs>
<linearGradient
@@ -63,9 +63,9 @@ const Partners = () => {
y2="21.5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1CF7C3" stop-opacity="0" />
<stop offset="0.510417" stop-color="#1CF7C3" />
<stop offset="1" stop-color="#1CF7C3" stop-opacity="0" />
<stop stopColor="#1CF7C3" stopOpacity="0" />
<stop offset="0.510417" stopColor="#1CF7C3" />
<stop offset="1" stopColor="#1CF7C3" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
+9 -40
View File
@@ -14,6 +14,7 @@ import { useTheme } from 'common/Providers'
import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
import { plans } from 'shared-data/plans'
import { ArrowNarrowRightIcon } from '@heroicons/react/outline'
import AnnouncementBadge from '../../components/Announcement/Badge'
export default function IndexPage() {
const router = useRouter()
@@ -151,7 +152,7 @@ export default function IndexPage() {
/>
<div>
<div className="relative z-10 py-16 lg:py-28">
<div className="relative z-10 py-16 lg:py-20">
<div className="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
<div className="mx-auto max-w-3xl space-y-2 lg:max-w-none">
<h1 className="text-brand text-base">Pricing</h1>
@@ -159,45 +160,13 @@ export default function IndexPage() {
<p className="p text-lg">
Start building for free, collaborate with a team, then scale to millions of users.
</p>
<div className="w-full flex justify-center items-center opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
<Link href="/blog/organization-based-billing" passHref>
<a
target="_blank"
className="
group
relative
flex flex-row
items-center
pr-3 p-1
text-sm
w-auto
gap-2
text-left
rounded-full
bg-opacity-20
border
border-background-surface-100
hover:border-background-surface-300
overflow-hidden
focus:outline-none focus:ring-brand-600 focus:ring-2 focus:rounded-full
"
>
<Badge color="brand" size="large" className="py-1">
Update
</Badge>
<span className="text-foreground">Changes to how we bill</span>
<ArrowNarrowRightIcon className="h-4 ml-2 -translate-x-1 transition-transform group-hover:translate-x-0" />
<div
className="absolute inset-0 -z-10 bg-gradient-to-br
opacity-70
overflow-hidden rounded-full
from-background-surface-100
to-background-surface-300
backdrop-blur-md
"
/>
</a>
</Link>
<div className="w-full inline-flex justify-center items-center pt-3 pb-6">
<AnnouncementBadge
url="/blog/organization-based-billing"
badge="Update"
announcement="Changes to how we bill"
target="_blank"
/>
</div>
</div>
</div>
+5 -2
View File
@@ -141,6 +141,7 @@ transforms:
source: |-
.metadata.host = "db-default"
.metadata.parsed.timestamp = .timestamp
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
if err != null || parsed == null {
@@ -149,8 +150,10 @@ transforms:
if parsed != null {
.metadata.parsed.error_severity = parsed.level
}
if .metadata.parsed.error_severity == "info" {
.metadata.parsed.error_severity = "log"
}
.metadata.parsed.error_severity = upcase(.metadata.parsed.error_severity)
sinks:
logflare_auth:
+2 -2
View File
@@ -141,7 +141,7 @@
"source": [
"# add records to the collection\n",
"docs.upsert(\n",
" vectors=[\n",
" records=[\n",
" (\n",
" \"vec0\", # the vector's identifier\n",
" [0.1, 0.2, 0.3], # the vector. list or np.array\n",
@@ -273,7 +273,7 @@
],
"source": [
"docs.query(\n",
" query_vector=[0.4,0.5,0.6], # required\n",
" data=[0.4,0.5,0.6], # required\n",
" limit=5, # number of records to return\n",
" filters={}, # metadata filters\n",
" measure=\"cosine_distance\", # distance measure to use\n",
+33 -31
View File
@@ -33658,36 +33658,6 @@
"version": "2.2.2",
"license": "MIT"
},
"node_modules/react-datepicker": {
"version": "4.12.0",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.2",
"classnames": "^2.2.6",
"date-fns": "^2.24.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.12.2",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-datepicker/node_modules/date-fns": {
"version": "2.30.0",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/react-dnd": {
"version": "14.0.5",
"license": "MIT",
@@ -42543,7 +42513,7 @@
"react-beautiful-dnd": "^13.1.0",
"react-contexify": "^5.0.0",
"react-csv": "^2.0.3",
"react-datepicker": "^4.8.0",
"react-datepicker": "^4.16.0",
"react-dnd": "^14.0.3",
"react-dnd-html5-backend": "^14.0.1",
"react-dom": "^17.0.2",
@@ -42926,6 +42896,21 @@
"tslib": "^2.4.1"
}
},
"studio/node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"studio/node_modules/doctrine": {
"version": "2.1.0",
"license": "Apache-2.0",
@@ -43107,6 +43092,23 @@
"url": "https://opencollective.com/ramda"
}
},
"studio/node_modules/react-datepicker": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.16.0.tgz",
"integrity": "sha512-hNQ0PAg/LQoVbDUO/RWAdm/RYmPhN3cz7LuQ3hqbs24OSp69QCiKOJRrQ4jk1gv1jNR5oYu8SjjgfDh8q6Q1yw==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.12.2",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"studio/node_modules/resolve": {
"version": "2.0.0-next.4",
"license": "MIT",
+1 -1
View File
@@ -94,7 +94,7 @@ export const plans: PricingInformation[] = [
'Daily backups stored for 14 days',
'Standardised Security Questionnaire',
'SOC2',
'HIPAA',
'HIPAA available as paid add-on',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'28-day log retention',
+2 -2
View File
@@ -469,8 +469,8 @@ export const pricing: Pricing = {
plans: {
free: false,
pro: false,
team: true,
enterprise: true,
team: 'Available as paid add-on',
enterprise: 'Available as paid add-on',
},
tooltips: {
main: 'Available as a paid add-on on Team plan and above.',
+1 -1
View File
@@ -992,7 +992,7 @@ functions:
```dart
await supabase
.from('users')
.upsert({ 'username': 'supabot' }, { 'onConflict': 'username' });
.upsert({ 'username': 'supabot' }, onConflict: 'username');
```
- id: return-the-exact-number-of-rows
name: Return the exact number of rows
+8
View File
@@ -0,0 +1,8 @@
export const get = jest.fn()
export const post = jest.fn()
export const patch = jest.fn()
export const del = jest.fn()
export const put = jest.fn()
export const head = jest.fn()
export const trace = jest.fn()
export const options = jest.fn()
+44 -15
View File
@@ -1,22 +1,35 @@
/* eslint-disable react/display-name */
import DataGrid, { DataGridHandle, RowsChangeData } from '@supabase/react-data-grid'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { forwardRef } from 'react'
import { memo } from 'react-tracked'
import DataGrid, { DataGridHandle, RowsChangeData } from '@supabase/react-data-grid'
import { IconLoader } from 'ui'
import { GridProps, SupaRow } from '../../types'
import { useDispatch, useTrackedState } from '../../store'
import RowRenderer from './RowRenderer'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useDispatch, useTrackedState } from '../../store'
import { GridProps, SupaRow } from '../../types'
import RowRenderer from './RowRenderer'
import { Button } from 'ui'
function rowKeyGetter(row: SupaRow) {
const rowKeyGetter = (row: SupaRow) => {
return row?.idx ?? -1
}
const updateColumnResize = (index: number, width: number, dispatch: (value: unknown) => void) => {
dispatch({
type: 'UPDATE_COLUMN_SIZE',
payload: { index, width: Math.round(width) },
})
}
const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 500)
interface IGrid extends GridProps {
rows: any[]
updateRow: (previousRow: any, updatedData: any) => void
onAddRow?: () => void
onImportData?: () => void
onEditForeignKeyColumnValue: (args: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
@@ -36,6 +49,8 @@ export const Grid = memo(
rowClass,
rows,
updateRow,
onAddRow,
onImportData,
onEditForeignKeyColumnValue,
},
ref: React.Ref<DataGridHandle> | undefined
@@ -126,17 +141,31 @@ export const Grid = memo(
className={gridClass}
rowClass={rowClass}
style={{ height: '100%' }}
noRowsFallback={
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-base">This table is empty</p>
{onAddRow !== undefined && onImportData !== undefined && (
<>
<p className="text-sm text-light mt-1">Add or generate rows to get started.</p>
<div className="flex items-center space-x-2 mt-4">
{/* [Joshen] Leaving this as a placeholder */}
{/* <Button type="outline">Generate random data</Button> */}
{onAddRow !== undefined && onImportData !== undefined && (
<Button type="default" onClick={onImportData}>
Import data via CSV
</Button>
)}
</div>
</>
)}
</div>
}
/>
</div>
)
}
)
)
const updateColumnResize = (index: number, width: number, dispatch: (value: unknown) => void) => {
dispatch({
type: 'UPDATE_COLUMN_SIZE',
payload: { index, width: Math.round(width) },
})
}
const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 500)
@@ -150,7 +150,9 @@ const DefaultHeader = ({
>
<div className="">
<p>Insert row</p>
<p className="text-scale-1000">Insert a new row into {table.name}</p>
<p className="text-scale-1000 text-xs">
Insert a new row into {table.name}
</p>
</div>
</Dropdown.Item>,
]
@@ -176,7 +178,9 @@ const DefaultHeader = ({
>
<div className="">
<p>Insert column</p>
<p className="text-scale-1000">Insert a new column into {table.name}</p>
<p className="text-scale-1000 text-xs">
Insert a new column into {table.name}
</p>
</div>
</Dropdown.Item>,
]
@@ -203,7 +207,7 @@ const DefaultHeader = ({
>
<div className="">
<p>Import data from CSV</p>
<p className="text-scale-1000">Insert new rows from a CSV</p>
<p className="text-scale-1000 text-xs">Insert new rows from a CSV</p>
</div>
</Dropdown.Item>,
]
@@ -0,0 +1,287 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw } from 'ui'
import { FilterPopover, LogDetailsPanel } from 'components/interfaces/AuditLogs'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { DatePicker } from 'components/ui/DatePicker'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { AuditLog } from 'data/organizations/organization-audit-logs-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProfileAuditLogsQuery } from 'data/profile/profile-audit-logs-query'
import { useProjectsQuery } from 'data/projects/projects-query'
const AuditLogs = () => {
const currentTime = dayjs().utc().set('millisecond', 0)
const [dateSortDesc, setDateSortDesc] = useState(true)
const [dateRange, setDateRange] = useState({
from: currentTime.subtract(1, 'day').toISOString(),
to: currentTime.toISOString(),
})
const [selectedLog, setSelectedLog] = useState<AuditLog>()
const [filters, setFilters] = useState<{ projects: string[] }>({
projects: [], // project_ref[]
})
const { data: projects } = useProjectsQuery()
const { data: organizations } = useOrganizationsQuery()
const { data, error, isLoading, isSuccess, isError, isRefetching, refetch } =
useProfileAuditLogsQuery({
iso_timestamp_start: dateRange.from,
iso_timestamp_end: dateRange.to,
})
// This feature depends on the subscription tier of the user. Free user can view logs up to 1 day
// in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is
// viewed for more than 5 minutes, the call parameters needs to be updated. This also works with
// higher tiers (7 days of logs).The user will see a loading shimmer.
useEffect(() => {
const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to))
const interval = setInterval(() => {
const currentTime = dayjs().utc().set('millisecond', 0)
setDateRange({
from: currentTime.add(duration).toISOString(),
to: currentTime.toISOString(),
})
}, 5 * 60000)
return () => clearInterval(interval)
}, [dateRange.from, dateRange.to])
const retentionPeriod = data?.retention_period ?? 0
const logs = data?.result ?? []
const sortedLogs = logs
?.sort((a, b) =>
dateSortDesc
? Number(new Date(b.occurred_at)) - Number(new Date(a.occurred_at))
: Number(new Date(a.occurred_at)) - Number(new Date(b.occurred_at))
)
?.filter((log) => {
if (filters.projects.length > 0) {
return filters.projects.includes(log.target.metadata.project_ref || '')
} else {
return log
}
})
return (
<>
<div className="space-y-4 flex flex-col">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<p className="text-xs prose">Filter by</p>
<FilterPopover
name="Projects"
options={projects ?? []}
labelKey="name"
valueKey="ref"
activeOptions={filters.projects}
onSaveFilters={(values) => setFilters({ ...filters, projects: values })}
/>
<DatePicker
hideTime
hideClear
triggerButtonType="dashed"
triggerButtonTitle=""
from={dateRange.from}
to={dateRange.to}
minDate={dayjs().subtract(retentionPeriod, 'days').toDate()}
maxDate={dayjs().toDate()}
onChange={(value) => {
if (value.from !== null && value.to !== null) {
const current = dayjs().utc()
const from = dayjs(value.from)
.utc()
.hour(current.hour())
.minute(current.minute())
.second(current.second())
.toISOString()
const to = dayjs(value.to)
.utc()
.hour(current.hour())
.minute(current.minute())
.second(current.second())
.toISOString()
setDateRange({ from, to })
}
}}
renderFooter={() => {
return (
<Alert title="" variant="info" className="mx-3 pl-2 pr-2 pt-1 pb-2">
You have a log retention period of{' '}
<span className="text-brand">
{retentionPeriod} day
{retentionPeriod > 1 ? 's' : ''}
</span>
. You may only view logs from{' '}
{dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the
earliest date.
</Alert>
)
}}
/>
{isSuccess && (
<>
<div className="h-[20px] border-r border-scale-700 !ml-4 !mr-2" />
<p className="prose text-xs">Viewing {sortedLogs.length} logs in total</p>
</>
)}
</div>
<Button
type="default"
disabled={isLoading || isRefetching}
icon={<IconRefreshCw className={isRefetching ? 'animate-spin' : ''} />}
onClick={() => refetch()}
>
{isRefetching ? 'Refreshing' : 'Refresh'}
</Button>
</div>
{isLoading && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve audit logs" />}
{isSuccess && (
<>
{logs.length === 0 ? (
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
<p className="prose text-sm">You do not have any audit logs available yet</p>
</div>
) : logs.length > 0 && sortedLogs.length === 0 ? (
<div className="bg-scale-100 dark:bg-scale-300 border rounded p-4 flex items-center justify-between">
<p className="prose text-sm">No audit logs found based on the filters applied</p>
</div>
) : (
<Table
head={[
<Table.th key="action" className="py-2">
Action
</Table.th>,
<Table.th key="target" className="py-2">
Target
</Table.th>,
<Table.th key="date" className="py-2">
<div className="flex items-center space-x-2">
<p>Date</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="text"
className="px-1"
icon={
dateSortDesc ? (
<IconArrowDown strokeWidth={1.5} size={14} />
) : (
<IconArrowUp strokeWidth={1.5} size={14} />
)
}
onClick={() => setDateSortDesc(!dateSortDesc)}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Portal>
<Tooltip.Content side="right">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">
{dateSortDesc ? 'Sort latest first' : 'Sort earliest first'}
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Table.th>,
<Table.th key="actions" className="py-2"></Table.th>,
]}
body={
sortedLogs?.map((log) => {
const project = projects?.find(
(project) => project.ref === log.target.metadata.project_ref
)
const organization = organizations?.find(
(org) => org.slug === log.target.metadata.org_slug
)
const hasStatusCode = log.action.metadata[0]?.status !== undefined
return (
<Table.tr
key={log.occurred_at}
onClick={() => setSelectedLog(log)}
className="cursor-pointer hover:!bg-scale-100 transition duration-100"
>
<Table.td className="max-w-[250px]">
<div className="flex items-center space-x-2">
{hasStatusCode && (
<p className="bg-scale-400 rounded px-1 flex items-center justify-center text-xs font-mono border">
{log.action.metadata[0].status}
</p>
)}
<p className="truncate" title={log.action.name}>
{log.action.name}
</p>
</div>
</Table.td>
<Table.td>
<p
className="text-scale-1100 max-w-[230px] truncate"
title={project?.name ?? organization?.name ?? '-'}
>
{project?.name
? 'Project: '
: organization?.name
? 'Organization: '
: null}
{project?.name ?? organization?.name ?? '-'}
</p>
<p
className="text-scale-1000 text-xs mt-0.5 truncate"
title={log.target.metadata.project_ref ?? log.target.metadata.org_slug}
>
{log.target.metadata.project_ref
? 'Ref: '
: log.target.metadata.org_slug
? 'Slug: '
: null}
{log.target.metadata.project_ref ?? log.target.metadata.org_slug}
</p>
</Table.td>
<Table.td>
{dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')}
</Table.td>
<Table.td align="right">
<Button type="default">View details</Button>
</Table.td>
</Table.tr>
)
}) ?? []
}
/>
)}
</>
)}
</div>
<LogDetailsPanel selectedLog={selectedLog} onClose={() => setSelectedLog(undefined)} />
</>
)
}
export default AuditLogs
@@ -1,6 +1,7 @@
import AccessTokenList from './AccessTokenList'
import AuditLogs from './AuditLogs'
import NewAccessTokenButton from './NewAccessTokenButton'
import NewTokenBanner from './NewTokenBanner'
import TOTPFactors from './TOTPFactors'
export { AccessTokenList, NewTokenBanner, NewAccessTokenButton, TOTPFactors }
export { AccessTokenList, NewTokenBanner, NewAccessTokenButton, TOTPFactors, AuditLogs }
@@ -1,12 +1,12 @@
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
import { OrganizationAuditLog } from 'data/organizations/organization-audit-logs-query'
import { AuditLog } from 'data/organizations/organization-audit-logs-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import dayjs from 'dayjs'
import { Input, SidePanel } from 'ui'
export interface LogDetailsPanelProps {
selectedLog?: OrganizationAuditLog
selectedLog?: AuditLog
onClose: () => void
}
@@ -0,0 +1,4 @@
import FilterPopover from './FilterPopover'
import LogDetailsPanel from './LogDetailsPanel'
export { FilterPopover, LogDetailsPanel }
@@ -35,12 +35,9 @@ const NoProjectsOnPaidOrgInfo = ({ organization }: NoProjectsOnPaidOrgInfoProps)
<div>
The monthly fees for the paid plan still apply. To cancel your subscription, head over to
your{' '}
<Link href={`/org/${organization?.slug}/billing`}>
<a>
<span className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</span>
.
<Link href={`/org/${organization?.slug}/billing`} passHref>
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings .
</a>
</Link>
</div>
@@ -22,9 +22,7 @@ import {
} from 'components/layouts/ProjectLayout/ProjectContext'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
export interface AddOnsProps {}
const AddOns = ({}: AddOnsProps) => {
const AddOns = () => {
const { ref: projectRef } = useParams()
const snap = useSubscriptionPageStateSnapshot()
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
@@ -6,10 +6,13 @@ import SubscriptionTier from './Tier/SubscriptionTier'
import { SUBSCRIPTION_PANEL_KEYS, useSubscriptionPageStateSnapshot } from 'state/subscription-page'
import { useSelectedOrganization } from 'hooks'
import { useEffect } from 'react'
import InformationBox from 'components/ui/InformationBox'
import { Button, IconExternalLink, IconInfo } from 'ui'
import Link from 'next/link'
export interface SubscriptionProps { }
export interface SubscriptionProps {}
const Subscription = ({ }: SubscriptionProps) => {
const Subscription = ({}: SubscriptionProps) => {
const router = useRouter()
const organization = useSelectedOrganization()
const snap = useSubscriptionPageStateSnapshot()
@@ -37,37 +40,74 @@ const Subscription = ({ }: SubscriptionProps) => {
<>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-6">
<h3 className="text-scale-1200 text-xl">
{isOrgBilling ? 'Project Add Ons' : 'Subscription'}
</h3>
<h3 className="text-scale-1200 text-xl">Subscription</h3>
</div>
</div>
{!isOrgBilling && (
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<SubscriptionTier />
</div>
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 pt-8">
<InformationBox
icon={<IconInfo size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="We're upgrading our billing system"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
This organization uses the legacy project-based billing. We’ve recently made some
big improvements to our billing system. To migrate to the new organization-based
billing, head over to your{' '}
<Link href={`/org/${organization?.slug}/billing`} passHref>
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</a>
</Link>
.
</p>
<div className="space-x-3">
<Link href="https://supabase.com/blog/organization-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Announcement
</a>
</Button>
</Link>
<Link href="https://supabase.com/docs/guides/platform/org-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Documentation
</a>
</Button>
</Link>
</div>
</div>
}
/>
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<SubscriptionTier />
</div>
)}
{!isOrgBilling && (
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<CostControl />
</div>
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<CostControl />
</div>
)}
<div className={`${isOrgBilling ? '' : 'border-b'}`}>
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<AddOns />
</div>
</div>
{!isOrgBilling && (
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<BillingBreakdown />
</div>
<div className="border-b">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 py-16">
<BillingBreakdown />
</div>
)}
</div>
</>
)
}
@@ -42,13 +42,11 @@ const SubscriptionTier = ({}: SubscriptionTierProps) => {
<div className="text-sm text-scale-1000">
To manage your billing address, emails or Tax ID, head to your{' '}
<Link href={`/org/${orgSlug}/billing`}>
<a>
<span className="text-sm text-green-900 transition hover:text-green-1000">
organization settings
</span>
.
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization settings
</a>
</Link>
.
</div>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
@@ -137,19 +137,17 @@ const TierUpdateSidePanel = () => {
icon={<IconInfo size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Legacy Billing"
title="We're upgrading our billing system"
className="mt-4"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
This organization uses the legacy project-based billing. We’ve recently made some
big improvements to our billing system. To opt-in to the new organization-based
big improvements to our billing system. To migrate to the new organization-based
billing, head over to your{' '}
<Link href={`/org/${slug}/billing`}>
<a>
<span className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</span>
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</a>
</Link>
.
@@ -1,18 +1,20 @@
import dayjs from 'dayjs'
import { IconBarChart2 } from 'ui'
import Panel from 'components/ui/Panel'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { DataPoint } from 'data/analytics/constants'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Alert, Button, IconBarChart2 } from 'ui'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { getAddons } from '../Subscription/Subscription.utils'
import SectionContent from './SectionContent'
import SectionHeader from './SectionHeader'
import { USAGE_CATEGORIES } from './Usage.constants'
import { getUpgradeUrl } from './Usage.utils'
import UsageBarChart from './UsageBarChart'
import Panel from 'components/ui/Panel'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { getAddons } from '../Subscription/Subscription.utils'
import { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings } from './UsageWarningAlerts'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
export interface InfrastructureProps {
projectRef: string
@@ -28,11 +30,12 @@ const Infrastructure = ({
currentBillingCycleSelected,
}: InfrastructureProps) => {
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
const { data: resourceWarnings } = useResourceWarningsQuery()
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'infra')
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
const isFreeTier = subscription?.plan?.id === 'free'
const isFreePlan = subscription?.plan?.id === 'free'
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const selectedAddons = addons?.selected_addons ?? []
@@ -125,81 +128,14 @@ const Infrastructure = ({
<SectionContent section={attribute}>
{attribute.key === 'disk_io_consumption' && (
<>
{hasLatest && latestIoBudgetConsumption >= 100 ? (
<Alert withIcon variant="danger" title="Your Disk IO Budget has been used up">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and is now running at the
baseline performance. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : hasLatest && latestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="danger"
title="You are close to running out of Disk IO Budget"
>
<p className="mb-4">
Your workload has consumed {latestIoBudgetConsumption}% of your Disk IO
Budget. If you use up all your Disk IO Budget, your instance will reverted
to baseline performance. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? (
<Alert
withIcon
variant="warning"
title="You ran out of IO Budget at least once"
>
<p className="mb-4">
Your workload has used up all your Disk IO Budget and reverted to baseline
performance at least once during this billing cycle. If you need consistent
disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="warning"
title="You were close to using all your IO Budget at least once"
>
<p className="mb-4">
Your workload has consumed {highestIoBudgetConsumption}% of your Disk IO
budget during this billing cycle. If you use up all your Disk IO Budget,
your instance will reverted to baseline performance. If you need consistent
disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreeTier ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : null}
<DiskIOBandwidthWarnings
upgradeUrl={upgradeUrl}
isFreePlan={isFreePlan}
hasLatest={hasLatest}
currentBillingCycleSelected={currentBillingCycleSelected}
latestIoBudgetConsumption={latestIoBudgetConsumption}
highestIoBudgetConsumption={highestIoBudgetConsumption}
/>
<div className="space-y-1">
<p>Disk IO Bandwidth</p>
@@ -246,6 +182,21 @@ const Infrastructure = ({
</div>
</>
)}
{attribute.key === 'max_cpu_usage' && (
<CPUWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.cpu_exhaustion}
/>
)}
{attribute.key === 'ram_usage' && (
<RAMWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
/>
)}
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
@@ -1,21 +1,24 @@
import clsx from 'clsx'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { useMemo, useRef, useState } from 'react'
import { InView } from 'react-intersection-observer'
import { Button, IconAlertCircle, IconExternalLink, IconInfo, IconLoader, cn } from 'ui'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import { useSelectedOrganization } from 'hooks'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
import { useMemo, useRef, useState } from 'react'
import { InView } from 'react-intersection-observer'
import { IconAlertCircle, IconLoader, cn } from 'ui'
import Activity from './Activity'
import Bandwidth from './Bandwidth'
import Infrastructure from './Infrastructure'
import SizeAndCounts from './SizeAndCounts'
import { USAGE_CATEGORIES, USAGE_STATUS } from './Usage.constants'
import { getUsageStatus } from './Usage.utils'
import Link from 'next/link'
import InformationBox from 'components/ui/InformationBox'
type UsageSectionIds = 'infra' | 'bandwidth' | 'sizeCount' | 'activity'
@@ -214,6 +217,47 @@ const Usage = () => {
</div>
</div>
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 lg:px-16 2xl:px-32 pt-8">
<InformationBox
icon={<IconInfo size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="We're upgrading our billing system"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
This organization uses the legacy project-based billing. We’ve recently made some
big improvements to our billing system. To migrate to the new organization-based
billing, head over to your{' '}
<Link href={`/org/${organization?.slug}/billing`}>
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</a>
</Link>
.
</p>
<div className="space-x-3">
<Link href="https://supabase.com/blog/organization-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Announcement
</a>
</Button>
</Link>
<Link href="https://supabase.com/docs/guides/platform/org-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Documentation
</a>
</Button>
</Link>
</div>
</div>
}
/>
</div>
<InView as="div" threshold={0.2} onChange={(inView) => inView && setActiveTab('infra')}>
<div id="infrastructure" ref={infrastructureRef} style={{ scrollMarginTop: '100px' }}>
<Infrastructure
@@ -0,0 +1,71 @@
import { AlertTitle } from '@ui/components/shadcn/ui/alert'
import Link from 'next/link'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconAlertCircle } from 'ui'
interface CPUWarningsProps {
isFreePlan: boolean
upgradeUrl: string
severity?: 'warning' | 'critical' | null
}
const CPUWarnings = ({ isFreePlan, upgradeUrl, severity }: CPUWarningsProps) => {
if (severity === 'warning') {
return (
<Alert_Shadcn_ variant="warning">
<IconAlertCircle />
<AlertTitle>Your max CPU usage has exceeded 80%</AlertTitle>
<AlertDescription_Shadcn_>
High CPU usage could result in slower queries, disruption of daily back up routines, and
in rare cases, your instance may become unresponsive. If you need more resources, consider
upgrading to a larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
if (severity === 'critical') {
return (
<Alert_Shadcn_ variant="destructive">
<IconAlertCircle />
<AlertTitle>Your max CPU usage has reached 100%</AlertTitle>
<AlertDescription_Shadcn_>
High CPU usage could result in slower queries, disruption of daily back up routines, and
in rare cases, your instance may become unresponsive. If you need more resources, consider
upgrading to a larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
return null
}
export default CPUWarnings
@@ -0,0 +1,106 @@
import Link from 'next/link'
import { Alert, Button } from 'ui'
// [Joshen] In the future, conditionals should be from resource exhaustion endpoint as single source of truth
interface DiskIOBandwidthWarningsProps {
isFreePlan: boolean
hasLatest: boolean
upgradeUrl: string
currentBillingCycleSelected: boolean
latestIoBudgetConsumption: number
highestIoBudgetConsumption: number
}
const DiskIOBandwidthWarnings = ({
isFreePlan,
hasLatest,
currentBillingCycleSelected,
upgradeUrl,
latestIoBudgetConsumption,
highestIoBudgetConsumption,
}: DiskIOBandwidthWarningsProps) => {
if (hasLatest && latestIoBudgetConsumption >= 100) {
return (
<Alert withIcon variant="danger" title="Your Disk IO Budget has been used up">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and is now running at the baseline
performance. If you need consistent disk performance, consider upgrading to a larger
compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (hasLatest && latestIoBudgetConsumption >= 80) {
return (
<Alert withIcon variant="danger" title="You are close to running out of Disk IO Budget">
<p className="mb-4">
Your workload has consumed {latestIoBudgetConsumption}% of your Disk IO Budget. If you use
up all your Disk IO Budget, your instance will reverted to baseline performance. If you
need consistent disk performance, consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (currentBillingCycleSelected && highestIoBudgetConsumption >= 100) {
return (
<Alert withIcon variant="warning" title="You ran out of IO Budget at least once">
<p className="mb-4">
Your workload has used up all your Disk IO Budget and reverted to baseline performance at
least once during this billing cycle. If you need consistent disk performance, consider
upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
if (currentBillingCycleSelected && highestIoBudgetConsumption >= 80) {
return (
<Alert
withIcon
variant="warning"
title="You were close to using all your IO Budget at least once"
>
<p className="mb-4">
Your workload has consumed {highestIoBudgetConsumption}% of your Disk IO budget during
this billing cycle. If you use up all your Disk IO Budget, your instance will reverted to
baseline performance. If you need consistent disk performance, consider upgrading to a
larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
)
}
return null
}
export default DiskIOBandwidthWarnings
@@ -0,0 +1,71 @@
import { AlertTitle } from '@ui/components/shadcn/ui/alert'
import Link from 'next/link'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconAlertCircle } from 'ui'
interface RAMWarningsProps {
isFreePlan: boolean
upgradeUrl: string
severity?: 'warning' | 'critical' | null
}
const RAMWarnings = ({ isFreePlan, upgradeUrl, severity }: RAMWarningsProps) => {
if (severity === 'warning') {
return (
<Alert_Shadcn_ variant="warning">
<IconAlertCircle />
<AlertTitle>Your memory usage has exceeded 80%</AlertTitle>
<AlertDescription_Shadcn_>
High memory usage could result in overall degraded performance, and in rare cases, your
instance may become unresponsive. If you need more resources, consider upgrading to a
larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-ram">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
if (severity === 'critical') {
return (
<Alert_Shadcn_ variant="destructive">
<IconAlertCircle />
<AlertTitle>Your memory usage has reached 100%</AlertTitle>
<AlertDescription_Shadcn_>
High memory usage could result in overall degraded performance, and in rare cases, your
instance may become unresponsive. If you need more resources, consider upgrading to a
larger compute add-on.
</AlertDescription_Shadcn_>
<div className="mt-3 flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/exhaust-cpu">
<a>
<Button type="default">Learn more</Button>
</a>
</Link>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</div>
</Alert_Shadcn_>
)
}
return null
}
export default RAMWarnings
@@ -0,0 +1,5 @@
import CPUWarnings from './CPUWarnings'
import DiskIOBandwidthWarnings from './DiskIOBandwidthWarnings'
import RAMWarnings from './RAMWarnings'
export { CPUWarnings, DiskIOBandwidthWarnings, RAMWarnings }
@@ -1,65 +1,27 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useRouter } from 'next/router'
import { Badge, Button } from 'ui'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation'
import { setProjectStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'
import { DatabaseBackup } from 'data/database/backups-query'
import { useCheckPermissions } from 'hooks'
interface BackupItemProps {
projectRef: string
backup: any
index: number
backup: DatabaseBackup
onSelectBackup: () => void
}
const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => {
const router = useRouter()
const { ui } = useStore()
const queryClient = useQueryClient()
const BackupItem = ({ index, backup, onSelectBackup }: BackupItemProps) => {
const canTriggerScheduledBackups = useCheckPermissions(
PermissionAction.INFRA_EXECUTE,
'queue_job.restore.prepare'
)
const { mutate: restoreFromBackup, isLoading: isRestoring } = useBackupRestoreMutation({
onSuccess: () => {
setTimeout(() => {
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message: `Restoring database back to ${dayjs(backup.inserted_at).format(
'DD MMM YYYY HH:mm:ss'
)}`,
})
router.push(`/project/${projectRef}`)
}, 3000)
},
})
const onRestoreClick = () => {
confirmAlert({
title: 'Confirm to restore',
message: `Are you sure you want to restore from ${dayjs(backup.inserted_at).format(
'DD MMM YYYY'
)}? This will destroy any new data written since this backup was made.`,
onAsyncConfirm: async () => restoreFromBackup({ ref: projectRef, backup }),
})
}
const generateSideButtons = (backup: any) => {
if (backup.status === 'COMPLETED')
return (
<div className="flex space-x-4">
<Button
type="default"
disabled={!canTriggerScheduledBackups || isRestoring}
onClick={onRestoreClick}
>
<Button type="default" disabled={!canTriggerScheduledBackups} onClick={onSelectBackup}>
Restore
</Button>
</div>
@@ -1,27 +1,58 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { observer } from 'mobx-react-lite'
import { IconAlertCircle, IconClock } from 'ui'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { IconAlertCircle, IconClock, Modal } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import InformationBox from 'components/ui/InformationBox'
import Panel from 'components/ui/Panel'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { useBackupsQuery } from 'data/database/backups-query'
import { useCheckPermissions } from 'hooks'
import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation'
import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query'
import { setProjectStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'
import BackupItem from './BackupItem'
import BackupsEmpty from './BackupsEmpty'
const BackupsList = () => {
const { ui } = useStore()
const router = useRouter()
const queryClient = useQueryClient()
const { project: selectedProject } = useProjectContext()
const projectRef = selectedProject?.ref || 'default'
const { data: backups } = useBackupsQuery({ projectRef })
const [selectedBackup, setSelectedBackup] = useState<DatabaseBackup>()
const canTriggerScheduledBackups = useCheckPermissions(
PermissionAction.INFRA_EXECUTE,
'queue_job.restore.prepare'
)
const { data: backups } = useBackupsQuery({ projectRef })
const {
mutate: restoreFromBackup,
isLoading: isRestoring,
isSuccess: isSuccessBackup,
} = useBackupRestoreMutation({
onSuccess: () => {
setTimeout(() => {
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message: `Restoring database back to ${dayjs(selectedBackup?.inserted_at).format(
'DD MMM YYYY HH:mm:ss'
)}`,
})
router.push(`/project/${projectRef}`)
}, 3000)
},
})
const planKey = backups?.tierKey ?? ''
const sortedBackups = (backups?.backups ?? []).sort(
(a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
@@ -43,25 +74,57 @@ const BackupsList = () => {
if (isPitrEnabled) return null
return (
<div className="space-y-6">
{sortedBackups.length === 0 && planKey !== 'FREE' ? (
<BackupsEmpty />
) : (
<>
{!canTriggerScheduledBackups && (
<InformationBox
icon={<IconAlertCircle className="text-scale-1100" strokeWidth={2} />}
title="You need additional permissions to trigger a scheduled backup"
/>
)}
<Panel>
{sortedBackups?.map((x: any, i: number) => {
return <BackupItem key={x.id} projectRef={projectRef} backup={x} index={i} />
})}
</Panel>
</>
)}
</div>
<>
<div className="space-y-6">
{sortedBackups.length === 0 && planKey !== 'FREE' ? (
<BackupsEmpty />
) : (
<>
{!canTriggerScheduledBackups && (
<InformationBox
icon={<IconAlertCircle className="text-scale-1100" strokeWidth={2} />}
title="You need additional permissions to trigger a scheduled backup"
/>
)}
<Panel>
{sortedBackups?.map((x, i: number) => {
return (
<BackupItem
key={x.id}
backup={x}
index={i}
onSelectBackup={() => setSelectedBackup(x)}
/>
)
})}
</Panel>
</>
)}
</div>
<ConfirmationModal
size="medium"
buttonLabel="Confirm restore"
buttonLoadingLabel="Restoring"
visible={selectedBackup !== undefined}
header="Confirm to restore from backup"
loading={isRestoring || isSuccessBackup}
onSelectCancel={() => setSelectedBackup(undefined)}
onSelectConfirm={() => {
if (selectedBackup === undefined) return console.error('Backup required')
restoreFromBackup({ ref: projectRef, backup: selectedBackup })
}}
>
<Modal.Content>
<div className="pt-6 pb-5">
<p>
Are you sure you want to restore from $
{dayjs(selectedBackup?.inserted_at).format('DD MMM YYYY')}? This will destroy any new
data written since this backup was made.
</p>
</div>
</Modal.Content>
</ConfirmationModal>
</>
)
}
@@ -36,7 +36,11 @@ const PITRSelection = () => {
const [showConfirmation, setShowConfirmation] = useState(false)
const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
const { mutate: restoreFromPitr, isLoading: isRestoring } = usePitrRestoreMutation({
const {
mutate: restoreFromPitr,
isLoading: isRestoring,
isSuccess: isSuccessPITR,
} = usePitrRestoreMutation({
onSuccess: (res, variables) => {
setTimeout(() => {
setShowConfirmation(false)
@@ -321,15 +325,15 @@ const PITRSelection = () => {
<div className="flex items-center justify-end space-x-2">
<Button
type="default"
disabled={isRestoring}
disabled={isRestoring || isSuccessPITR}
onClick={() => setShowConfirmation(false)}
>
Cancel
</Button>
<Button
type="warning"
disabled={isRestoring}
loading={isRestoring}
disabled={isRestoring || isSuccessPITR}
loading={isRestoring || isSuccessPITR}
onClick={onConfirmRestore}
>
I understand, begin restore
@@ -77,12 +77,17 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
].join(' ')}
>
<div className="flex items-center gap-1 max-w-[85%]">
<h3
title={extension.name}
className="h-5 m-0 text-sm uppercase truncate cursor-pointer text-scale-1200"
>
{extension.name}
</h3>
<div className="flex items-center space-x-2 truncate">
<h3
title={extension.name}
className="h-5 m-0 text-sm truncate cursor-pointer text-scale-1200"
>
{extension.name}
</h3>
<p className="text-sm text-light">
{extension?.installed_version ?? extension.default_version}
</p>
</div>
{extensions.find((item: any) => item.name === extension.name) ? (
<Link
href={
@@ -129,7 +134,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
<div className="py-3 px-4">
<div className="flex items-center flex-grow space-x-2 text-sm text-scale-1100">
<span>Schema:</span>
<Badge>{`${extension.schema}`}</Badge>
<Badge color="scale">{`${extension.schema}`}</Badge>
</div>
</div>
)}
@@ -1,9 +1,12 @@
import Snippets from 'components/to-be-cleaned/Docs/Snippets'
import CodeSnippet from 'components/to-be-cleaned/Docs/CodeSnippet'
import Param from 'components/to-be-cleaned/Docs/Param'
import Description from 'components/to-be-cleaned/Docs/Description'
import { useParams } from 'common'
import { IconTable } from 'ui'
import CodeSnippet from 'components/to-be-cleaned/Docs/CodeSnippet'
import Description from 'components/to-be-cleaned/Docs/Description'
import Param from 'components/to-be-cleaned/Docs/Param'
import Snippets from 'components/to-be-cleaned/Docs/Snippets'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
const ResourceContent = ({
autoApiService,
resourceId,
@@ -14,7 +17,12 @@ const ResourceContent = ({
showApiKey,
refreshDocs,
}: any) => {
if (!paths || !definitions) return null
const { ref } = useParams()
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}`
: autoApiService.endpoint
const keyToShow = !!showApiKey ? showApiKey : 'SUPABASE_KEY'
const resourcePaths = paths[`/${resourceId}`]
@@ -28,6 +36,8 @@ const ResourceContent = ({
required: resourceDefinition?.required?.includes(id),
}))
if (!paths || !definitions) return null
return (
<>
<h2 className="doc-section__table-name text-scale-1200 mt-0 flex items-center px-6 gap-2">
@@ -75,7 +85,7 @@ const ResourceContent = ({
snippet={Snippets.readColumns({
title: `Select ${x.id}`,
resourceId,
endpoint: autoApiService.endpoint,
endpoint: endpoint,
apiKey: keyToShow,
columnName: x.id,
})}
@@ -106,23 +116,23 @@ const ResourceContent = ({
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.readAll(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.readAll(resourceId, endpoint, keyToShow)}
/>
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.readColumns({
resourceId,
endpoint: autoApiService.endpoint,
endpoint: endpoint,
apiKey: keyToShow,
})}
/>
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.readForeignTables(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.readForeignTables(resourceId, endpoint, keyToShow)}
/>
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.readRange(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.readRange(resourceId, endpoint, keyToShow)}
/>
</article>
</div>
@@ -143,7 +153,7 @@ const ResourceContent = ({
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.readFilters(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.readFilters(resourceId, endpoint, keyToShow)}
/>
</article>
</div>
@@ -174,15 +184,15 @@ const ResourceContent = ({
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.insertSingle(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.insertSingle(resourceId, endpoint, keyToShow)}
/>
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.insertMany(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.insertMany(resourceId, endpoint, keyToShow)}
/>
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.upsert(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.upsert(resourceId, endpoint, keyToShow)}
/>
</article>
</div>
@@ -214,7 +224,7 @@ const ResourceContent = ({
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.update(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.update(resourceId, endpoint, keyToShow)}
/>
</article>
</div>
@@ -242,7 +252,7 @@ const ResourceContent = ({
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.delete(resourceId, autoApiService.endpoint, keyToShow)}
snippet={Snippets.delete(resourceId, endpoint, keyToShow)}
/>
</article>
</div>
@@ -1,6 +1,7 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import clsx from 'clsx'
import { useParams } from 'common'
import dayjs from 'dayjs'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -18,7 +19,6 @@ import {
Toggle,
} from 'ui'
import { useParams } from 'common/hooks'
import {
FormActions,
FormHeader,
@@ -29,6 +29,7 @@ import {
} from 'components/ui/Forms'
import Panel from 'components/ui/Panel'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation'
import { useEdgeFunctionUpdateMutation } from 'data/edge-functions/edge-functions-update-mutation'
@@ -44,6 +45,7 @@ const EdgeFunctionDetails = () => {
const [showInstructions, setShowInstructions] = useState(false)
const { data: settings } = useProjectApiQuery({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const { mutateAsync: updateEdgeFunction, isLoading: isUpdating } = useEdgeFunctionUpdateMutation()
const { mutate: deleteEdgeFunction, isLoading: isDeleting } = useEdgeFunctionDeleteMutation({
@@ -66,7 +68,10 @@ const EdgeFunctionDetails = () => {
: '[YOUR ANON KEY]'
const endpoint = apiService?.app_config.endpoint ?? ''
const functionUrl = `${apiService?.protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}`
const functionUrl =
customDomainData?.customDomain?.status === 'active'
? `${apiService?.protocol}://${customDomainData.customDomain.hostname}/functions/v1/${selectedFunction?.slug}`
: `${apiService?.protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}`
const { managementCommands, secretCommands, invokeCommands } = generateCLICommands(
selectedFunction,
@@ -8,6 +8,7 @@ import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Table from 'components/to-be-cleaned/Table'
import { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
interface EdgeFunctionsListItemProps {
function: EdgeFunctionsResponse
@@ -16,14 +17,22 @@ interface EdgeFunctionsListItemProps {
const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => {
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const [isCopied, setIsCopied] = useState(false)
const { project } = useProjectContext()
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
// get the .co or .net TLD from the restUrl
const restUrl = project?.restUrl
const restUrlTld = restUrl !== undefined ? new URL(restUrl as string).hostname.split('.').pop() : 'co'
const restUrlTld =
restUrl !== undefined ? new URL(restUrl as string).hostname.split('.').pop() : 'co'
const functionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${item.slug}`
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}/functions/v1/${item.slug}`
: functionUrl
return (
<Table.tr
key={item.id}
@@ -38,7 +47,7 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) =
</Table.td>
<Table.td className="">
<div className="text-xs text-scale-1100 flex gap-2 items-center truncate">
<p className="font-mono truncate hidden md:inline">{functionUrl}</p>
<p className="font-mono truncate hidden md:inline">{endpoint}</p>
<button
type="button"
className="text-scale-900 hover:text-scale-1200 transition"
@@ -51,7 +60,7 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) =
}, 3000)
}
event.stopPropagation()
onCopy(functionUrl)
onCopy(endpoint)
}}
>
{isCopied ? (
@@ -7,6 +7,7 @@ import { Button, IconBookOpen, IconCode, IconMaximize2, IconMinimize2, IconTermi
import CommandRender from 'components/interfaces/Functions/CommandRender'
import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { Commands } from './Functions.types'
interface TerminalInstructionsProps {
@@ -21,8 +22,10 @@ const TerminalInstructions = ({
const router = useRouter()
const { ref: projectRef } = useParams()
const [showInstructions, setShowInstructions] = useState(!closable)
const { data: tokens } = useAccessTokensQuery()
const { data: settings } = useProjectApiQuery({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const apiService = settings?.autoApiService
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
@@ -30,7 +33,10 @@ const TerminalInstructions = ({
: '[YOUR ANON KEY]'
const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
const functionsEndpoint = `${endpoint}/functions/v1`
const functionsEndpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}/functions/v1`
: `https://${endpoint}/functions/v1`
// get the .co or .net TLD from the restUrl
const restUrl = settings?.autoApiService.restUrl
@@ -70,7 +76,7 @@ const TerminalInstructions = ({
jsx: () => {
return (
<>
<span className="text-brand-600">curl</span> -L -X POST 'https://{functionsEndpoint}
<span className="text-brand-600">curl</span> -L -X POST '{functionsEndpoint}
/hello-world' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '}
{`--data '{"name":"Functions"}'`}
</>
@@ -93,7 +99,7 @@ const TerminalInstructions = ({
<div className="flex items-center justify-center w-8 h-8 p-2 border rounded bg-scale-100">
<IconTerminal strokeWidth={2} />
</div>
<h4>Terminal instructions</h4>
<h4>Create your first Edge Function via the CLI</h4>
</div>
{closable && (
<div className="cursor-pointer" onClick={() => setShowInstructions(!showInstructions)}>
@@ -1,24 +1,19 @@
import {
Badge,
IconAlertTriangle,
IconGitBranch,
IconGitHub,
IconLoader,
IconPauseCircle,
IconTriangle,
} from 'ui'
import { IconGitBranch, IconGitHub } from 'ui'
import CardButton from 'components/ui/CardButton'
import { useProjectReadOnlyStatus } from 'hooks/misc/useProjectReadOnlyStatus'
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import { Project } from 'types'
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import { ProjectCardStatus } from './ProjectCardStatus'
import { inferProjectStatus } from './ProjectCard.utils'
export interface ProjectCardProps {
project: Project
rewriteHref?: string
githubIntegration?: IntegrationProjectConnection
vercelIntegration?: IntegrationProjectConnection
resourceWarnings?: ResourceWarning
}
const ProjectCard = ({
@@ -26,30 +21,26 @@ const ProjectCard = ({
rewriteHref,
githubIntegration,
vercelIntegration,
resourceWarnings,
}: ProjectCardProps) => {
const { name, ref: projectRef } = project
const desc = `${project.cloud_provider} | ${project.region}`
const isReadonly = useProjectReadOnlyStatus(projectRef)
const isBranchingEnabled = project.preview_branch_refs?.length > 0
const isBranchingEnabled = project.preview_branch_refs.length > 0
const isGithubIntegrated = githubIntegration !== undefined
const isVercelIntegrated = vercelIntegration !== undefined
const githubRepository = githubIntegration?.metadata.name ?? undefined
// Project status should supersede is read only status
const isHealthy = project.status === PROJECT_STATUS.ACTIVE_HEALTHY
const isPausing =
project.status === PROJECT_STATUS.GOING_DOWN || project.status === PROJECT_STATUS.PAUSING
const isPaused = project.status === PROJECT_STATUS.INACTIVE
const isRestoring = project.status === PROJECT_STATUS.RESTORING
const projectStatus = inferProjectStatus(project)
return (
<li className="col-span-1 list-none">
<CardButton
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
containerHeightClassName="h-44"
title={
<div className="w-full justify-between space-y-1">
<p className="flex-shrink truncate">{name}</p>
<div className="w-full justify-between space-y-1.5 px-6">
<p className="flex-shrink truncate text-base">{name}</p>
<span className="text-sm lowercase text-scale-1000">{desc}</span>
<div className="flex items-center space-x-1.5">
{isVercelIntegrated && (
<div className="w-fit p-1 border rounded-md flex items-center border-scale-600">
@@ -77,55 +68,16 @@ const ProjectCard = ({
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-scale-1000">{desc}</span>
{isHealthy && isReadonly && (
<div className="grow text-right">
<Badge color="yellow">
<div className="flex items-center gap-2">
<IconAlertTriangle size={14} strokeWidth={2} />
<span className="truncate">Read-only mode</span>
</div>
</Badge>
</div>
)}
{isRestoring && (
<div className="grow text-right">
<Badge color="brand">
<div className="flex items-center gap-2">
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
<span className="truncate">Restoring</span>
</div>
</Badge>
</div>
)}
{isPausing && (
<div className="grow text-right">
<Badge color="scale">
<div className="flex items-center gap-2">
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
<span className="truncate">Pausing</span>
</div>
</Badge>
</div>
)}
{isPaused && (
<div className="grow text-right">
<Badge color="scale">
<div className="flex items-center gap-2">
<IconPauseCircle size={14} strokeWidth={2} />
<span className="truncate">Paused</span>
</div>
</Badge>
</div>
<div className="mb-[-26px]">
{resourceWarnings && (
<ProjectCardStatus
projectStatus={projectStatus}
resourceWarnings={resourceWarnings}
/>
)}
</div>
}
/>
></CardButton>
</li>
)
}
@@ -0,0 +1,34 @@
import { PROJECT_STATUS } from 'lib/constants'
import { Project } from 'types'
export const inferProjectStatus = (project: Project) => {
let status = undefined
switch (project.status) {
case PROJECT_STATUS.ACTIVE_HEALTHY:
status = 'isHealthy'
break
case PROJECT_STATUS.GOING_DOWN:
case PROJECT_STATUS.PAUSING:
status = 'isPausing'
break
case PROJECT_STATUS.INACTIVE:
status = 'isPaused'
break
case PROJECT_STATUS.RESTORING:
status = 'isRestoring'
break
case PROJECT_STATUS.UNKNOWN:
case PROJECT_STATUS.COMING_UP:
status = 'isComingUp'
break
}
return status as InferredProjectStatus
}
export type InferredProjectStatus =
| 'isHealthy'
| 'isPausing'
| 'isPaused'
| 'isRestoring'
| 'isComingUp'
| undefined
@@ -0,0 +1,98 @@
import { RESOURCE_WARNING_MESSAGES } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants'
import { ResourceWarning } from 'data/usage/resource-warnings-query'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
IconAlertTriangle,
IconLoader,
IconPauseCircle,
IconRefreshCw,
} from 'ui'
import { InferredProjectStatus } from './ProjectCard.utils'
import { getWarningContent } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils'
export interface ProjectCardWarningsProps {
resourceWarnings: ResourceWarning
projectStatus: InferredProjectStatus
}
export const ProjectCardStatus = ({
resourceWarnings,
projectStatus,
}: ProjectCardWarningsProps) => {
//const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
const showResourceExhaustionWarnings = false
// [Joshen] Read only takes higher precedence over multiple resource warnings
const activeWarnings = resourceWarnings.is_readonly_mode_enabled
? ['is_readonly_mode_enabled']
: Object.keys(resourceWarnings).filter(
(property) =>
property !== 'project' &&
property !== 'is_readonly_mode_enabled' &&
resourceWarnings[property as keyof typeof resourceWarnings] !== null
)
const hasCriticalWarning = activeWarnings.some(
(x) => resourceWarnings[x as keyof typeof resourceWarnings] === 'critical'
)
const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning
const getTitle = () => {
if (projectStatus === 'isPaused') return 'Project is paused'
if (projectStatus === 'isPausing') return 'Project is pausing'
if (projectStatus === 'isComingUp') return 'Project is coming up'
if (projectStatus === 'isRestoring') return 'Project is restoring'
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].title
: getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')?.title
}
const getDescription = () => {
if (projectStatus === 'isPaused') return 'This project will not accept requests until resumed'
if (projectStatus === 'isPausing') return 'The pause process will complete in a few minutes'
if (projectStatus === 'isComingUp') return 'Your project will be ready in a few minutes'
if (projectStatus === 'isRestoring') return 'Your project will be ready in a few minutes'
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1 && showResourceExhaustionWarnings
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].description
: getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')?.description
}
const alertTitle = getTitle()
const alertDescription = getDescription()
const alertType = isCritical
? 'destructive'
: projectStatus === 'isPaused'
? 'default'
: 'warning'
if (activeWarnings.length === 0 && projectStatus === 'isHealthy') return <div className="py-2" />
return (
<div>
<Alert_Shadcn_
variant={alertType}
className="border-0 rounded-none rounded-b-md my-2 mb-2.5 [&>svg]:w-[28px] [&>svg]:h-[28px] [&>svg]:mt-1 [&>svg]:p-1.5 [&>svg]:left-6 pl-6"
>
{projectStatus === 'isPaused' || projectStatus === 'isPausing' ? (
<IconPauseCircle strokeWidth={2} />
) : projectStatus === 'isRestoring' || projectStatus === 'isComingUp' ? (
<IconRefreshCw size={14} strokeWidth={2} />
) : (
<IconAlertTriangle strokeWidth={2} />
)}
<AlertTitle_Shadcn_ className="text-sm mb-0.5">{alertTitle}</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="text-xs">{alertDescription}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</div>
)
}
@@ -1,8 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { groupBy } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { Badge, Button, IconPlus } from 'ui'
import { Button, IconExternalLink, IconPlus, Modal } from 'ui'
import AlertError from 'components/ui/AlertError'
import {
@@ -12,12 +11,13 @@ import {
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useCheckPermissions } from 'hooks'
import { ResourceWarning, useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { IS_PLATFORM } from 'lib/constants'
import { makeRandomString } from 'lib/helpers'
import { Organization, Project, ResponseError } from 'types'
import ProjectCard from './ProjectCard'
import ShimmeringCard from './ShimmeringCard'
import { useState } from 'react'
export interface ProjectListProps {
rewriteHref?: (projectRef: string) => string
@@ -36,6 +36,7 @@ const ProjectList = ({ rewriteHref }: ProjectListProps) => {
isError: isErrorPermissions,
error: permissionsError,
} = usePermissionsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: IS_PLATFORM })
const projectsByOrg = groupBy(allProjects, 'organization_id')
const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
@@ -51,6 +52,7 @@ const ProjectList = ({ rewriteHref }: ProjectListProps) => {
overdueInvoices={(allOverdueInvoices ?? []).filter(
(it) => it.organization_id === organization.id
)}
resourceWarnings={resourceWarnings ?? []}
rewriteHref={rewriteHref}
isLoadingPermissions={isLoadingPermissions}
isErrorPermissions={isErrorPermissions}
@@ -73,6 +75,7 @@ type OrganizationProjectsProps = {
organization: Organization
projects: Project[]
overdueInvoices: OverdueInvoicesResponse[]
resourceWarnings: ResourceWarning[]
isLoadingPermissions: boolean
isErrorPermissions: boolean
permissionsError: ResponseError | null
@@ -83,9 +86,10 @@ type OrganizationProjectsProps = {
}
const OrganizationProjects = ({
organization: { id, name, slug, subscription_id },
organization: { name, slug, subscription_id },
projects,
overdueInvoices,
resourceWarnings,
isLoadingPermissions,
isErrorPermissions,
permissionsError,
@@ -96,6 +100,8 @@ const OrganizationProjects = ({
}: OrganizationProjectsProps) => {
const isEmpty = !projects || projects.length === 0
const [orgBillingMigrationModalVisible, setOrgBillingMigrationModalVisible] = useState(false)
return (
<div className="space-y-3" key={makeRandomString(5)}>
<div className="flex space-x-4 items-center">
@@ -110,6 +116,14 @@ const OrganizationProjects = ({
</Link>
</div>
)}
{!subscription_id && (
<div>
<Button onClick={() => setOrgBillingMigrationModalVisible(true)} type="warning">
Action Required
</Button>
</div>
)}
</div>
{isLoadingPermissions || isLoadingProjects ? (
@@ -141,11 +155,56 @@ const OrganizationProjects = ({
key={makeRandomString(5)}
project={project}
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
resourceWarnings={resourceWarnings.find(
(resourceWarning) => resourceWarning.project === project.ref
)}
/>
))
)}
</ul>
)}
<Modal
closable
hideFooter
size="small"
visible={orgBillingMigrationModalVisible}
onCancel={() => setOrgBillingMigrationModalVisible(false)}
header="We're upgrading our billing system"
>
<Modal.Content className="py-4 space-y-4">
<div className="space-y-3">
<p className="text-sm leading-normal">
The organization "{name}" still uses the legacy project-based billing. We've recently
made some big improvements to our billing system and require your action. To migrate
to the new organization-based billing, head over to your{' '}
<Link href={`/org/${slug}/billing`} passHref>
<a className="text-sm text-green-900 transition hover:text-green-1000">
organization billing settings
</a>
</Link>
.
</p>
<div className="space-x-3">
<Link href="https://supabase.com/blog/organization-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Announcement
</a>
</Button>
</Link>
<Link href="https://supabase.com/docs/guides/platform/org-based-billing" passHref>
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<a target="_blank" rel="noreferrer">
Documentation
</a>
</Button>
</Link>
</div>
</div>
</Modal.Content>
</Modal>
</div>
)
}
@@ -1,7 +1,9 @@
import { ReactNode, useRef, useState } from 'react'
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { IntegrationConnectionsCreateVariables } from 'data/integrations/integrations.types'
import {
IntegrationConnectionsCreateVariables,
IntegrationProjectConnection,
} from 'data/integrations/integrations.types'
import { useSelectedOrganization } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { toast } from 'react-hot-toast'
@@ -19,8 +21,6 @@ import {
Popover_Shadcn_,
cn,
} from 'ui'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
export interface Project {
id: string
@@ -1,25 +1,24 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import dayjs from 'dayjs'
import Image from 'next/image'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw, IconUser } from 'ui'
import { useParams } from 'common'
import { FilterPopover, LogDetailsPanel } from 'components/interfaces/AuditLogs'
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { DatePicker } from 'components/ui/DatePicker'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import {
OrganizationAuditLog,
AuditLog,
useOrganizationAuditLogsQuery,
} from 'data/organizations/organization-audit-logs-query'
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw, IconUser } from 'ui'
import FilterPopover from './FilterPopover'
import LogDetailsPanel from './LogDetailsPanel'
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
// [Joshen considerations]
// - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible
@@ -35,7 +34,7 @@ const AuditLogs = () => {
from: currentTime.subtract(1, 'day').toISOString(),
to: currentTime.toISOString(),
})
const [selectedLog, setSelectedLog] = useState<OrganizationAuditLog>()
const [selectedLog, setSelectedLog] = useState<AuditLog>()
const [filters, setFilters] = useState<{ users: string[]; projects: string[] }>({
users: [], // gotrue_id[]
projects: [], // project_ref[]
@@ -52,6 +51,23 @@ const AuditLogs = () => {
iso_timestamp_end: dateRange.to,
})
// This feature depends on the subscription tier of the user. Free user can view logs up to 1 day
// in the past. The API limits the logs to maximum of 1 day and 5 minutes so when the page is
// viewed for more than 5 minutes, the call parameters needs to be updated. This also works with
// higher tiers (7 days of logs).The user will see a loading shimmer.
useEffect(() => {
const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to))
const interval = setInterval(() => {
const currentTime = dayjs().utc().set('millisecond', 0)
setDateRange({
from: currentTime.add(duration).toISOString(),
to: currentTime.toISOString(),
})
}, 5 * 60000)
return () => clearInterval(interval)
}, [dateRange.from, dateRange.to])
const members = detailData?.members ?? []
const roles = rolesData?.roles ?? []
@@ -276,21 +292,21 @@ const AuditLogs = () => {
<div className="flex items-center space-x-4">
<div>{userIcon}</div>
<div>
<p className="text-scale-1100">{user?.username ?? 'Unknown'}</p>
<p className="text-scale-1100">{user?.username ?? '-'}</p>
{role && (
<p className="mt-0.5 text-xs text-scale-1000">{role?.name}</p>
)}
</div>
</div>
</Table.td>
<Table.td>
<Table.td className="max-w-[250px]">
<div className="flex items-center space-x-2">
{hasStatusCode && (
<p className="bg-scale-400 rounded px-1 flex items-center justify-center text-xs font-mono border">
{log.action.metadata[0].status}
</p>
)}
<p className="max-w-[170px] truncate" title={log.action.name}>
<p className="truncate" title={log.action.name}>
{log.action.name}
</p>
</div>
@@ -298,7 +314,7 @@ const AuditLogs = () => {
<Table.td>
<p
className="text-scale-1100 max-w-[230px] truncate"
title={project?.name ?? organization?.name ?? 'Unknown'}
title={project?.name ?? organization?.name ?? '-'}
>
{project?.name
? 'Project: '
@@ -14,8 +14,10 @@ import { Alert, Button } from 'ui'
import { BILLING_BREAKDOWN_METRICS } from './BillingBreakdown.constants'
import BillingMetric from './BillingMetric'
import UpcomingInvoice from './UpcomingInvoice'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
const BillingBreakdown = () => {
const snap = useOrgSettingsPageStateSnapshot()
const { slug: orgSlug } = useParams()
const {
data: usage,
@@ -93,7 +95,16 @@ const BillingBreakdown = () => {
variant="danger"
title="Your organization's usage has exceeded its included quota"
actions={[
<Button key="upgrade-button" type="default" className="ml-8" onClick={() => {}}>
<Button
key="upgrade-button"
type="default"
className="ml-8"
onClick={() =>
snap.setPanelKey(
currentPlan?.id === 'free' ? 'subscriptionPlan' : 'costControl'
)
}
>
{currentPlan?.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}
</Button>,
]}
@@ -25,6 +25,7 @@ import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/or
import InformationBox from 'components/ui/InformationBox'
import AlertError from 'components/ui/AlertError'
import { SubscriptionTier } from 'data/subscriptions/types'
import Table from 'components/to-be-cleaned/Table'
// [Joshen TODO] Need to remove all contexts of "projects"
@@ -322,70 +323,71 @@ const PlanUpdateSidePanel = () => {
<span className="text-sm">Estimating monthly costs...</span>
)}
{subscriptionPreviewInitialized && (
<InformationBox
defaultVisibility={false}
title={
<span>
Estimated monthly price is $
{Math.round(
subscriptionPreview.breakdown.reduce((prev, cur) => prev + cur.total_price, 0)
)}{' '}
+ usage
</span>
}
hideCollapse={false}
description={
<div>
<table className="w-full">
<thead>
<tr className="border-b">
<th className="py-2 font-normal text-left text-sm text-scale-1000 w-1/2">
Item
</th>
<th className="py-2 font-normal text-left text-sm text-scale-1000">
Count
</th>
<th className="py-2 font-normal text-left text-sm text-scale-1000">
Unit Price
</th>
<th className="py-2 font-normal text-right text-sm text-scale-1000">
Price
</th>
</tr>
</thead>
<tbody>
{subscriptionPreview.breakdown.map((item) => (
<tr key={item.description} className="border-b">
<td className="py-2 text-sm">{item.description ?? 'Unknown'}</td>
<td className="py-2 text-sm">{item.quantity}</td>
<td className="py-2 text-sm">
{item.unit_price === 0 ? 'FREE' : `$${item.unit_price}`}
</td>
<td className="py-2 text-sm text-right">${item.total_price}</td>
</tr>
))}
</tbody>
<div>
<InformationBox
defaultVisibility={false}
title={
<span>
Estimated monthly price is $
{Math.round(
subscriptionPreview.breakdown.reduce((prev, cur) => prev + cur.total_price, 0)
)}{' '}
+ usage
</span>
}
hideCollapse={false}
description={
<Table
borderless={true}
head={[
<Table.th key="header-item">Item</Table.th>,
<Table.th key="header-count">Count</Table.th>,
<Table.th key="header-unit-price">Unit Price</Table.th>,
<Table.th key="header-price" className="text-right">
Price
</Table.th>,
]}
body={
<>
{subscriptionPreview.breakdown.map((item) => (
<Table.tr key={item.description}>
<Table.td>{item.description ?? 'Unknown'}</Table.td>
<Table.td>{item.quantity}</Table.td>
<Table.td>
{item.unit_price === 0 ? 'FREE' : `$${item.unit_price}`}
</Table.td>
<Table.td className="text-right">${item.total_price}</Table.td>
</Table.tr>
))}
<tbody>
<tr>
<td className="py-2 text-sm">Total</td>
<td className="py-2 text-sm" />
<td className="py-2 text-sm" />
<td className="py-2 text-sm text-right">
$
{Math.round(
subscriptionPreview.breakdown.reduce(
(prev, cur) => prev + cur.total_price,
0
)
) ?? 0}
</td>
</tr>
</tbody>
</table>
</div>
}
/>
<Table.tr>
<Table.td>Total</Table.td>
<Table.td />
<Table.td />
<Table.td className=" text-right">
$
{Math.round(
subscriptionPreview.breakdown.reduce(
(prev, cur) => prev + cur.total_price,
0
)
) ?? 0}
</Table.td>
</Table.tr>
</>
}
></Table>
}
/>
{subscriptionPreview.number_of_projects &&
subscriptionPreview.number_of_projects > 1 && (
<p className="text-sm mt-2">
All {subscriptionPreview.number_of_projects} projects from your organization "
{selectedOrganization?.name}" will use the new {planMeta?.name} plan.
</p>
)}
</div>
)}
</Modal.Content>
@@ -11,12 +11,13 @@ import { useProjectsQuery } from 'data/projects/projects-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganization } from 'hooks'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
import { Button, IconExternalLink, IconInfo, Listbox } from 'ui'
import { Alert, Button, IconExternalLink, IconInfo, Listbox } from 'ui'
import Activity from './Activity'
import Bandwidth from './Bandwidth'
import SizeAndCounts from './SizeAndCounts'
import InformationBox from 'components/ui/InformationBox'
import Link from 'next/link'
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
const Usage = () => {
const { slug, projectRef } = useParams()
@@ -32,6 +33,9 @@ const Usage = () => {
isError: isErrorSubscription,
isSuccess: isSuccessSubscription,
} = useOrgSubscriptionQuery({ orgSlug: slug })
const { data: usage } = useOrgUsageQuery({ orgSlug: slug })
const orgProjects = projects?.filter((project) => project.organization_id === organization?.id)
useEffect(() => {
@@ -88,6 +92,13 @@ const Usage = () => {
? orgProjects?.find((it) => it.ref === selectedProjectRef)
: undefined
const hasExceededAnyLimits = Boolean(
usage?.usages.find(
(metric) =>
!metric.unlimited && metric.capped && metric.usage > (metric?.pricing_free_units ?? 0)
)
)
return (
<>
<ScaffoldContainer className="sticky top-0 border-b bg-scale-200 z-10 overflow-hidden">
@@ -156,6 +167,33 @@ const Usage = () => {
</div>
</ScaffoldContainer>
{!selectedProject && subscription && hasExceededAnyLimits && (
<ScaffoldContainer className="mt-5">
<Alert
withIcon
variant="danger"
title="Your organization's usage has exceeded its included quota"
actions={[
<Link
key="upgrade-button"
href={`/org/${slug}/billing?panel=${
subscription.plan.id === 'free' ? 'subscriptionPlan' : 'costControl'
}`}
>
<Button asChild key="upgrade-button" type="default" className="ml-8">
<a>{subscription.plan.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}</a>
</Button>
</Link>,
]}
>
Your projects can become unresponsive or enter read only mode.{' '}
{subscription.plan.id === 'free'
? 'Please upgrade to the Pro plan to ensure that your projects remain available.'
: 'Please disable spend cap to ensure that your projects remain available.'}
</Alert>
</ScaffoldContainer>
)}
{selectedProjectRef && (
<ScaffoldContainer className="mt-5">
<InformationBox
@@ -228,6 +228,52 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
title: '',
queries: {},
},
[Presets.STORAGE]: {
title: 'Storage',
queries: {
cacheHitRate: {
queryType: 'logs',
// storage report does not perform any filtering
sql: (_filters) => `
-- cache-hit-rate
SELECT
timestamp_trunc(timestamp, hour) as timestamp,
countif( h.cf_cache_status in ('HIT', 'STALE', 'REVALIDATED', 'UPDATING') ) as hit_count,
countif( h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') ) as miss_count
from edge_logs f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(m.response) as res
cross join unnest(res.headers) as h
where starts_with(r.path, '/storage/v1/object') and r.method = 'GET'
group by timestamp
order by timestamp desc
`,
},
topCacheMisses: {
queryType: 'logs',
// storage report does not perform any filtering
sql: (_filters) => `
-- top-cache-misses
SELECT
r.path as path,
r.search as search,
count(id) as count
from edge_logs f
cross join unnest(f.metadata) as m
cross join unnest(m.request) as r
cross join unnest(m.response) as res
cross join unnest(res.headers) as h
where starts_with(r.path, '/storage/v1/object')
and r.method = 'GET'
and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC')
group by path, search
order by count desc
limit 12
`,
},
},
},
[Presets.QUERY_PERFORMANCE]: {
title: 'Query performance',
queries: {
@@ -3,6 +3,7 @@ import { DEFAULT_QUERY_PARAMS } from './Reports.constants'
export enum Presets {
API = 'api',
STORAGE = 'storage',
AUTH = 'auth',
QUERY_PERFORMANCE = 'query_performance',
}
@@ -0,0 +1,86 @@
import { TextFormatter } from 'components/interfaces/Settings/Logs/LogsFormatters'
import Table from 'components/to-be-cleaned/Table'
import StackedBarChart from 'components/ui/Charts/StackedBarChart'
import { Fragment } from 'react'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
export const CacheHitRateChartRenderer = (
props: ReportWidgetProps<{
timestamp: string
hit_count: number
miss_count: number
}>
) => {
const stackedData = props.data.flatMap((datum) => [
{
timestamp: datum.timestamp,
count: datum.hit_count,
type: 'hit',
},
{
timestamp: datum.timestamp,
count: datum.miss_count,
type: 'miss',
},
])
return (
<StackedBarChart
hideHeader
variant="percentages"
data={stackedData}
xAxisKey="timestamp"
yAxisKey="count"
stackKey="type"
stackColors={['brand', 'amber']}
/>
)
}
export const TopCacheMissesRenderer = (
props: ReportWidgetRendererProps<{
path: string
search: string
count: number
}>
) => {
if (props.data.length === 0) return null
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-scale-400'
const cellClasses = '!text-xs !py-2'
return (
<>
<h4>Top Cache Misses</h4>
<Table
head={
<>
<Table.th className={headerClasses}>Request</Table.th>
<Table.th className={headerClasses + ' text-right'}>Count</Table.th>
</>
}
body={
<>
{props.data.map((datum) => (
<Fragment key={datum.path + (datum.search || '')}>
<Table.tr className="p-0">
<Table.td className={[cellClasses].join(' ')}>
<div className=" truncate max-w-sm lg:max-w-lg">
<TextFormatter className="text-scale-1100" value={datum.path} />
<TextFormatter
className="max-w-sm text-scale-900 truncate "
value={decodeURIComponent(datum.search || '')}
/>
</div>
</Table.td>
<Table.td className={[cellClasses, 'text-right align-top'].join(' ')}>
{datum.count}
</Table.td>
</Table.tr>
</Fragment>
))}
</>
}
/>
</>
)
}
@@ -67,7 +67,9 @@ const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => {
return (
<div className="bg-table-header-light dark:bg-table-header-dark">
<div className="flex flex-row justify-between items-center pr-8">
<p className="m-0 border-0 px-6 py-4 font-mono">{result.error.message ?? result.error}</p>
<p className="m-0 border-0 px-6 py-4 font-mono text-sm">
Error running SQL: {result.error.message ?? result.error}
</p>
{supabaseAIEnabled && !hasHipaaAddon && (
<Button
icon={
@@ -134,14 +134,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Optimized compute</p>
<p className="m-0">Optimized compute</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/compute-add-ons">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About compute add-ons</p>
<p className="text-sm m-0">About compute add-ons</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -151,7 +151,7 @@ const Addons = () => {
<Link href="https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">Connection Pooler</p>
<p className="text-sm m-0">Connection Pooler</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -339,14 +339,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Point in time recovery</p>
<p className="m-0">Point in time recovery</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/backups#point-in-time-recovery">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About PITR backups</p>
<p className="text-sm m-0">About PITR backups</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -403,14 +403,14 @@ const Addons = () => {
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="space-y-6">
<p>Custom domain</p>
<p className="m-0">Custom domain</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">More information</p>
<p className="text-sm text-scale-1100 m-0">More information</p>
<div>
<Link href="https://supabase.com/docs/guides/platform/custom-domains">
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">About custom domains</p>
<p className="text-sm m-0">About custom domains</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -122,6 +122,7 @@ interface ConfigProps {
export const PgbouncerConfig = ({ projectRef, bouncerInfo, connectionInfo }: ConfigProps) => {
const { ui } = useStore()
const { project } = useProjectContext()
const [updates, setUpdates] = useState({
pool_mode: bouncerInfo.pool_mode || 'transaction',
default_pool_size: bouncerInfo.default_pool_size || undefined,
@@ -140,7 +141,12 @@ export const PgbouncerConfig = ({ projectRef, bouncerInfo, connectionInfo }: Con
const canUpdateConnectionPoolingConfiguration = useCheckPermissions(
PermissionAction.UPDATE,
'projects'
'projects',
{
resource: {
project_id: project?.id,
},
}
)
const updateConfig = async (updatedConfig: any) => {
@@ -1,8 +1,16 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Fragment, useMemo, useState } from 'react'
import { IconBarChart2, IconExternalLink } from 'ui'
import { useParams } from 'common'
import { getAddons } from 'components/interfaces/BillingV2/Subscription/Subscription.utils'
import {
CPUWarnings,
DiskIOBandwidthWarnings,
RAMWarnings,
} from 'components/interfaces/BillingV2/Usage/UsageWarningAlerts'
import UsageBarChart from 'components/interfaces/Organization/Usage/UsageBarChart'
import {
ScaffoldContainer,
ScaffoldDivider,
@@ -19,10 +27,8 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganization } from 'hooks'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
import { Alert, Button, IconBarChart2, IconExternalLink } from 'ui'
import { getAddons } from '../../BillingV2/Subscription/Subscription.utils'
import UsageBarChart from '../../Organization/Usage/UsageBarChart'
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
const InfrastructureActivity = () => {
const { ref: projectRef } = useParams()
@@ -30,17 +36,17 @@ const InfrastructureActivity = () => {
const isOrgBilling = !!organization?.subscription_id
const [dateRange, setDateRange] = useState<any>()
const currentBillingCycleSelected = true
const upgradeUrl = '/'
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const isFreePlan = subscription?.plan?.id === 'free'
const { data: resourceWarnings } = useResourceWarningsQuery()
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const selectedAddons = addons?.selected_addons ?? []
const { computeInstance } = getAddons(selectedAddons)
const currentComputeInstanceSpecs = computeInstance?.variant?.meta ?? {
baseline_disk_io_mbs: 87,
@@ -50,6 +56,27 @@ const InfrastructureActivity = () => {
memory_gb: 1,
}
const currentBillingCycleSelected = useMemo(() => {
// Selected by default
if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
const { current_period_start, current_period_end } = subscription
return (
dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
)
}, [dateRange, subscription])
const upgradeUrl =
subscription === undefined
? `/`
: subscription.plan.id === 'free'
? `/org/${organization?.slug ?? '[slug]'}/billing#subscription`
: `/project/${projectRef}/settings/addons`
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
const startDate = useMemo(() => {
if (dateRange?.period_start?.date === 'Invalid Date') return undefined
@@ -116,6 +143,13 @@ const InfrastructureActivity = () => {
dateFormat,
})
const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day'))
const latestIoBudgetConsumption =
hasLatest && ioBudgetData?.data?.slice(-1)?.[0]
? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption)
: 0
const highestIoBudgetConsumption = Math.max(
...(ioBudgetData?.data || []).map((x) => Number(x.disk_io_consumption) ?? 0),
0
@@ -180,11 +214,11 @@ const InfrastructureActivity = () => {
<div className="sticky top-32 space-y-6">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<p className="text-base capitalize">{attribute.name}</p>
<h4 className="text-base capitalize m-0">{attribute.name}</h4>
</div>
<div className="grid gap-4">
{attribute.description.split('\n').map((value, idx) => (
<p key={`desc-${idx}`} className="text-sm text-scale-1000 pr-8">
<p key={`desc-${idx}`} className="text-sm text-scale-1000 pr-8 m-0">
{value}
</p>
))}
@@ -199,7 +233,7 @@ const InfrastructureActivity = () => {
<Link href={link.url}>
<a target="_blank" rel="noreferrer">
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm">{link.name}</p>
<p className="text-sm m-0">{link.name}</p>
<IconExternalLink size={16} strokeWidth={1.5} />
</div>
</a>
@@ -212,46 +246,14 @@ const InfrastructureActivity = () => {
<ScaffoldSectionContent>
{attribute.key === 'disk_io_consumption' && (
<>
{currentBillingCycleSelected && highestIoBudgetConsumption >= 100 ? (
<Alert
withIcon
variant="danger"
title="IO Budget for today has been used up"
>
<p className="mb-4">
Your workload has used up all the burst IO throughput minutes and ran at
the baseline performance. If you need consistent disk performance,
consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="danger">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : currentBillingCycleSelected && highestIoBudgetConsumption >= 80 ? (
<Alert
withIcon
variant="warning"
title="IO Budget for today is running out"
>
<p className="mb-4">
Your workload is about to use up all the burst IO throughput minutes
during the day. Once this is completely used up, your workload will run
at the baseline performance. If you need consistent disk performance,
consider upgrading to a larger compute add-on.
</p>
<Link href={upgradeUrl}>
<a>
<Button type="warning">
{isFreePlan ? 'Upgrade project' : 'Change compute add-on'}
</Button>
</a>
</Link>
</Alert>
) : null}
<DiskIOBandwidthWarnings
upgradeUrl={upgradeUrl}
isFreePlan={isFreePlan}
hasLatest={hasLatest}
currentBillingCycleSelected={currentBillingCycleSelected}
latestIoBudgetConsumption={latestIoBudgetConsumption}
highestIoBudgetConsumption={highestIoBudgetConsumption}
/>
<div className="space-y-1">
<p>Disk IO Bandwidth</p>
@@ -302,6 +304,21 @@ const InfrastructureActivity = () => {
</div>
</>
)}
{attribute.key === 'max_cpu_usage' && (
<CPUWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.cpu_exhaustion}
/>
)}
{attribute.key === 'ram_usage' && (
<RAMWarnings
isFreePlan={isFreePlan}
upgradeUrl={upgradeUrl}
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
/>
)}
<div className="space-y-1">
<div className="flex flex-row justify-between">
{attribute.key === 'disk_io_consumption' ? (
@@ -228,7 +228,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
name="allowed_mime_types"
layout="vertical"
label="Allowed MIME types"
placeholder="e.g image/jpg, image/png, audio/mpeg, video/mp4, etc"
placeholder="e.g image/jpeg, image/png, audio/mpeg, video/mp4, etc"
descriptionText="Comma separated values"
/>
</div>
@@ -237,7 +237,7 @@ const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) =>
name="allowed_mime_types"
layout="vertical"
label="Allowed MIME types"
placeholder="e.g image/jpg, image/png, audio/mpeg, video/mp4, etc"
placeholder="e.g image/jpeg, image/png, audio/mpeg, video/mp4, etc"
labelOptional="Comma separated values"
descriptionText="Leave the field blank to allow any MIME type to be uploaded"
/>
@@ -31,6 +31,8 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
const {
data: jsonSchema,
error: jsonSchemaError,
isError: isErrorJSONSchema,
isSuccess: isSuccessJSONSchema,
refetch,
} = useProjectJsonSchemaQuery({ projectRef: ref })
const { data: settings } = useProjectApiQuery({ projectRef: ref })
@@ -92,14 +94,15 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
>
<div className="Docs Docs--table-editor">
<SidePanel.Content>
{jsonSchemaError ? (
{isErrorJSONSchema && (
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
<div className="text-scale-1000">
<p>Error connecting to API</p>
<p>{`${jsonSchemaError}`}</p>
</div>
</div>
) : (
)}
{isSuccessJSONSchema && (
<>
{jsonSchema ? (
<>
@@ -101,6 +101,13 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
},
]
: []),
{
isActive: router.pathname === `/account/audit`,
icon: `${router.basePath}/img/user.svg`,
label: 'Audit Logs',
href: `/account/audit`,
key: `/account/audit`,
},
],
},
]
@@ -56,9 +56,6 @@ const UserSettingsDropdown = () => {
<Link passHref href="/account/me">
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onSelect={() => {
router.push('/account/me')
}}
onClick={() => setOpen(false)}
asChild
>
@@ -68,15 +65,21 @@ const UserSettingsDropdown = () => {
<Link passHref href="/account/tokens">
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onSelect={() => {
router.push('/account/tokens')
}}
onClick={() => setOpen(false)}
asChild
>
<a>Access tokens</a>
</DropdownMenuItem_Shadcn_>
</Link>
<Link passHref href="/account/audit">
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onClick={() => setOpen(false)}
asChild
>
<a>Audit logs</a>
</DropdownMenuItem_Shadcn_>
</Link>
<DropdownMenuSeparator_Shadcn_ />
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
@@ -6,20 +6,17 @@ import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { Badge, Button, IconArrowRight, IconLoader } from 'ui'
import { useParams } from 'common'
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
import { invalidateProjectsQuery } from 'data/projects/projects-query'
import { useSelectedProject } from 'hooks'
import { getWithTimeout } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { Project } from 'types'
import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
import { useParams } from 'common'
export interface BuildingStateProps {
project: Project
}
const BuildingState = ({ project }: BuildingStateProps) => {
const BuildingState = () => {
const { ref } = useParams()
const project = useSelectedProject()
const queryClient = useQueryClient()
const checkServerInterval = useRef<number>()
@@ -48,6 +45,8 @@ const BuildingState = ({ project }: BuildingStateProps) => {
}
}, [])
if (project === undefined) return null
return (
<div className="mx-auto my-16 w-full max-w-6xl items-center justify-center">
<div className="mx-6 flex flex-col space-y-16">
@@ -1,9 +1,14 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useMemo } from 'react'
import { Badge } from 'ui'
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
import { useProjectReadOnlyQuery } from 'data/config/project-read-only-query'
import {
getResourcesExceededLimits,
getResourcesExceededLimitsOrg,
} from 'components/ui/OveragesBanner/OveragesBanner.utils'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
@@ -13,39 +18,55 @@ import HelpPopover from './HelpPopover'
import NotificationsPopover from './NotificationsPopover'
import OrgDropdown from './OrgDropdown'
import ProjectDropdown from './ProjectDropdown'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
const { ref: projectRef } = useParams()
const { data: isReadOnlyMode } = useProjectReadOnlyQuery({
projectRef: selectedProject?.ref,
connectionString: selectedProject?.connectionString,
})
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
// Skip with org-based-billing, as quota is for the entire org
const { data: usage } = useProjectUsageQuery(
const { data: projectUsage } = useProjectUsageQuery(
{ projectRef },
{ enabled: Boolean(selectedOrganization && !selectedOrganization.subscription_id) }
)
const { data: orgUsage } = useOrgUsageQuery(
{ orgSlug: selectedOrganization?.slug },
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
)
const exceedingLimits = useMemo(() => {
if (orgUsage) {
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
} else if (projectUsage) {
return getResourcesExceededLimits(projectUsage).length > 0
} else {
return false
}
}, [projectUsage, orgUsage])
// Skip with org-based-billing, as quota is for the entire org
const { data: projectSubscription } = useProjectSubscriptionV2Query(
{ projectRef },
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
)
const resourcesExceededLimits = getResourcesExceededLimits(usage)
// Skip with org-based-billing, as quota is for the entire org
const { data: subscription } = useProjectSubscriptionV2Query(
const { data: orgSubscription } = useProjectSubscriptionV2Query(
{ projectRef },
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
)
const subscription = useMemo(() => {
return projectSubscription || orgSubscription
}, [projectSubscription, orgSubscription])
const projectHasNoLimits = subscription?.usage_billing_enabled === true
const showOverUsageBadge =
useFlag('overusageBadge') &&
subscription !== undefined &&
(subscription.plan.id === 'free' || subscription?.plan.id === 'pro') &&
(subscription.plan.id === 'free' || subscription.plan.id === 'pro') &&
!projectHasNoLimits &&
resourcesExceededLimits.length > 0
exceedingLimits
return (
<div
@@ -81,21 +102,15 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
{/* Project Dropdown */}
<ProjectDropdown />
{/* [Terry] Temporary until we figure out how we want to display this permanently */}
{/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/}
{isReadOnlyMode && (
<div className="ml-2">
<Link href={`/project/${projectRef}/settings/billing/usage`}>
<a>
<Badge color="red">Project is in read-only mode</Badge>
</a>
</Link>
</div>
)}
{showOverUsageBadge && (
<div className="ml-2">
<Link href={`/project/${projectRef}/settings/billing/usage`}>
<Link
href={
selectedOrganization?.subscription_id
? `/org/${selectedOrganization.slug}/usage`
: `/project/${projectRef}/settings/billing/usage`
}
>
<a>
<Badge color="red">Exceeding usage limits</Badge>
</a>
@@ -4,8 +4,10 @@ import { Fragment, PropsWithChildren, ReactNode } from 'react'
import { useParams } from 'common/hooks'
import Connecting from 'components/ui/Loading'
import ResourceExhaustionWarningBanner from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
import { useFlag, useSelectedOrganization, useSelectedProject, withAuth } from 'hooks'
import { PROJECT_STATUS, IS_PLATFORM } from 'lib/constants'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import AppLayout from '../AppLayout/AppLayout'
import BuildingState from './BuildingState'
import ConnectingState from './ConnectingState'
import LayoutHeader from './LayoutHeader'
@@ -16,7 +18,6 @@ import { ProjectContextProvider } from './ProjectContext'
import ProjectPausedState from './ProjectPausedState'
import RestoringState from './RestoringState'
import UpgradingState from './UpgradingState'
import AppLayout from '../AppLayout/AppLayout'
// [Joshen] This is temporary while we unblock users from managing their project
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
@@ -24,6 +25,7 @@ const routesToIgnoreProjectDetailsRequest = [
'/project/[ref]/settings/general',
'/project/[ref]/settings/database',
'/project/[ref]/settings/storage',
'/project/[ref]/settings/infrastructure',
'/project/[ref]/settings/billing/subscription',
'/project/[ref]/settings/billing/usage',
'/project/[ref]/settings/billing/invoices',
@@ -35,6 +37,7 @@ const routesToIgnorePostgrestConnection = [
'/project/[ref]/reports',
'/project/[ref]/settings/general',
'/project/[ref]/settings/database',
'/project/[ref]/settings/infrastructure',
'/project/[ref]/settings/billing/subscription',
'/project/[ref]/settings/billing/usage',
'/project/[ref]/settings/billing/invoices',
@@ -68,6 +71,7 @@ const ProjectLayout = ({
const organizationName = selectedOrganization?.name
const navLayoutV2 = useFlag('navigationLayoutV2')
const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
const ignorePausedState =
@@ -94,14 +98,12 @@ const ProjectLayout = ({
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
{/* Product menu bar */}
{!showPausedState && (
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
</MenuBarWrapper>
)}
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
{showPausedState ? (
@@ -111,7 +113,10 @@ const ProjectLayout = ({
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
<ContentWrapper isLoading={isLoading}>
{showResourceExhaustionWarnings && <ResourceExhaustionWarningBanner />}
{children}
</ContentWrapper>
)}
</main>
</div>
@@ -192,7 +197,7 @@ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => {
) : requiresDbConnection && isProjectRestoring ? (
<RestoringState />
) : requiresDbConnection && isProjectBuilding ? (
<BuildingState project={selectedProject} />
<BuildingState />
) : (
<Fragment key={selectedProject?.ref}>{children}</Fragment>
)}
@@ -222,6 +227,7 @@ export const ProjectLayoutNonBlocking = ({
const showPausedState = isPaused && !ignorePausedState
const navLayoutV2 = useFlag('navigationLayoutV2')
const showResourceExhaustionWarnings = useFlag('resourceExhaustionWarnings')
return (
<AppLayout>
@@ -249,7 +255,10 @@ export const ProjectLayoutNonBlocking = ({
</div>
</div>
) : (
children
<>
{showResourceExhaustionWarnings && <ResourceExhaustionWarningBanner />}
{children}
</>
)}
</main>
</div>
@@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react'
import ProductMenu from 'components/ui/ProductMenu'
import { useSelectedProject, withAuth } from 'hooks'
import ProjectLayout from '../'
import { generateReportsMenu } from './ReportsMenu.utils'
import { useFlag } from 'hooks'
interface ReportsLayoutProps {
title?: string
@@ -15,12 +15,61 @@ const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps
const project = useSelectedProject()
const page = router.pathname.split('/')[4] || ''
const ref = project?.ref ?? 'default'
const storageReportFlag = useFlag('storageReport')
const menuItems = [
{
title: '',
items: [
{
name: 'Custom reports',
key: '',
url: `/project/${ref}/reports`,
items: [],
},
],
},
{
items: [
{
name: 'API',
key: 'api-overview',
url: `/project/${ref}/reports/api-overview`,
items: [],
},
...(storageReportFlag
? [
{
name: 'Storage',
key: 'storage',
url: `/project/${ref}/reports/storage`,
items: [],
label: 'NEW',
},
]
: []),
{
name: 'Database',
key: 'database',
url: `/project/${ref}/reports/database`,
items: [],
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/reports/query-performance`,
items: [],
},
],
},
]
return (
<ProjectLayout
title={title}
product="Reports"
productMenu={<ProductMenu page={page} menu={generateReportsMenu(project)} />}
productMenu={<ProductMenu page={page} menu={menuItems} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
@@ -1,44 +0,0 @@
import { Project } from 'types'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
export const generateReportsMenu = (project?: Project): ProductMenuGroup[] => {
const ref = project?.ref ?? 'default'
return [
{
title: '',
items: [
{
name: 'Custom reports',
key: '',
url: `/project/${ref}/reports`,
items: [],
},
],
},
{
items: [
{
name: 'API',
key: 'api-overview',
url: `/project/${ref}/reports/api-overview`,
items: [],
label: 'NEW',
},
{
name: 'Database',
key: 'database',
url: `/project/${ref}/reports/database`,
items: [],
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/reports/query-performance`,
items: [],
label: 'NEW',
},
],
},
]
}
@@ -7,6 +7,7 @@ const AccountSettingsMenu = () => {
{ label: 'Preferences', pathname: `/account/me` },
{ label: 'Access Tokens', pathname: `/account/tokens` },
{ label: 'Security', pathname: `/account/security` },
{ label: 'Audit logs', pathname: `/account/audit` },
]
return (
@@ -1,8 +1,9 @@
import Link from 'next/link'
import Snippets from '../Snippets'
import CodeSnippet from '../CodeSnippet'
import { useRouter } from 'next/router'
import { makeRandomString } from 'lib/helpers'
import CodeSnippet from '../CodeSnippet'
import Snippets from '../Snippets'
const randomPassword = makeRandomString(20)
@@ -212,10 +213,7 @@ export default function UserManagement({ autoApiService, selectedLang, showApiKe
</p>
</article>
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.authThirdPartyLogin(autoApiService.endpoint, keyToShow)}
/>
<CodeSnippet selectedLang={selectedLang} snippet={Snippets.authThirdPartyLogin()} />
</article>
</div>
@@ -275,7 +273,7 @@ export default function UserManagement({ autoApiService, selectedLang, showApiKe
<article className="code">
<CodeSnippet
selectedLang={selectedLang}
snippet={Snippets.authLogout(autoApiService.endpoint, keyToShow, randomPassword)}
snippet={Snippets.authLogout(autoApiService.endpoint, keyToShow)}
/>
</article>
</div>
@@ -1,5 +1,5 @@
const snippets = {
endpoint: (endpoint) => ({
endpoint: (endpoint: string) => ({
title: 'API URL',
bash: {
language: 'bash',
@@ -18,7 +18,7 @@ const snippets = {
code: `npm install --save @supabase/supabase-js`,
},
}),
init: (endpoint) => ({
init: (endpoint: string) => ({
title: 'Initializing',
bash: {
language: 'bash',
@@ -56,7 +56,7 @@ Future<void> main() async {
}`,
},
}),
authKey: (title, varName, apikey) => ({
authKey: (title: string, varName: string, apikey: string) => ({
title: `${title}`,
bash: {
language: 'bash',
@@ -67,7 +67,11 @@ Future<void> main() async {
code: `const ${varName} = '${apikey}'`,
},
}),
authKeyExample: (defaultApiKey, endpoint, { keyName, showBearer = true }) => ({
authKeyExample: (
defaultApiKey: string,
endpoint: string,
{ keyName, showBearer = true }: { keyName: string; showBearer: boolean }
) => ({
title: 'Example usage',
bash: {
language: 'bash',
@@ -90,7 +94,19 @@ const supabase = createClient(SUPABASE_URL, process.env.${keyName || 'SUPABASE_K
`,
},
}),
rpcSingle: ({ rpcName, rpcParams, endpoint, apiKey, showBearer = true }) => {
rpcSingle: ({
rpcName,
rpcParams,
endpoint,
apiKey,
showBearer = true,
}: {
rpcName: string
rpcParams: any[]
endpoint: string
apiKey: string
showBearer: boolean
}) => {
let rpcList = rpcParams.map((x) => `"${x.name}": "value"`).join(', ')
let noParams = !rpcParams.length
let bashParams = noParams ? '' : `\n-d '{ ${rpcList} }' \\`
@@ -131,7 +147,7 @@ else console.log(data)
},
}
},
subscribeAll: (listenerName, resourceId) => ({
subscribeAll: (listenerName: string, resourceId: string) => ({
title: 'Subscribe to all events',
bash: {
language: 'bash',
@@ -151,7 +167,7 @@ const ${listenerName} = supabase.channel('custom-all-channel')
.subscribe()`,
},
}),
subscribeInserts: (listenerName, resourceId) => ({
subscribeInserts: (listenerName: string, resourceId: string) => ({
title: 'Subscribe to inserts',
bash: {
language: 'bash',
@@ -171,7 +187,7 @@ const ${listenerName} = supabase.channel('custom-insert-channel')
.subscribe()`,
},
}),
subscribeUpdates: (listenerName, resourceId) => ({
subscribeUpdates: (listenerName: string, resourceId: string) => ({
title: 'Subscribe to updates',
bash: {
language: 'bash',
@@ -191,7 +207,7 @@ const ${listenerName} = supabase.channel('custom-update-channel')
.subscribe()`,
},
}),
subscribeDeletes: (listenerName, resourceId) => ({
subscribeDeletes: (listenerName: string, resourceId: string) => ({
title: 'Subscribe to deletes',
bash: {
language: 'bash',
@@ -211,7 +227,7 @@ const ${listenerName} = supabase.channel('custom-delete-channel')
.subscribe()`,
},
}),
subscribeEq: (listenerName, resourceId, columnName, value) => ({
subscribeEq: (listenerName: string, resourceId: string, columnName: string, value: string) => ({
title: 'Subscribe to specific rows',
bash: {
language: 'bash',
@@ -231,7 +247,7 @@ const ${listenerName} = supabase.channel('custom-filter-channel')
.subscribe()`,
},
}),
readAll: (resourceId, endpoint, apiKey) => ({
readAll: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Read all rows',
bash: {
language: 'bash',
@@ -256,6 +272,12 @@ let { data: ${resourceId}, error } = await supabase
endpoint,
apiKey,
columnName = 'some_column,other_column',
}: {
title?: string
resourceId: string
endpoint: string
apiKey: string
columnName?: string
}) => ({
title,
bash: {
@@ -275,7 +297,7 @@ let { data: ${resourceId}, error } = await supabase
`,
},
}),
readForeignTables: (resourceId, endpoint, apiKey) => ({
readForeignTables: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Read foreign tables',
bash: {
language: 'bash',
@@ -299,7 +321,7 @@ let { data: ${resourceId}, error } = await supabase
`,
},
}),
readRange: (resourceId, endpoint, apiKey) => ({
readRange: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'With pagination',
bash: {
language: 'bash',
@@ -320,7 +342,7 @@ let { data: ${resourceId}, error } = await supabase
`,
},
}),
readFilters: (resourceId, endpoint, apiKey) => ({
readFilters: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'With filtering',
bash: {
language: 'bash',
@@ -357,7 +379,7 @@ let { data: ${resourceId}, error } = await supabase
`,
},
}),
insertSingle: (resourceId, endpoint, apiKey) => ({
insertSingle: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Insert a row',
bash: {
language: 'bash',
@@ -382,7 +404,7 @@ const { data, error } = await supabase
`,
},
}),
insertMany: (resourceId, endpoint, apiKey) => ({
insertMany: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Insert many rows',
bash: {
language: 'bash',
@@ -407,7 +429,7 @@ const { data, error } = await supabase
`,
},
}),
upsert: (resourceId, endpoint, apiKey) => ({
upsert: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Upsert matching rows',
bash: {
language: 'bash',
@@ -430,7 +452,7 @@ const { data, error } = await supabase
`,
},
}),
update: (resourceId, endpoint, apiKey) => ({
update: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Update matching rows',
bash: {
language: 'bash',
@@ -454,7 +476,7 @@ const { data, error } = await supabase
`,
},
}),
delete: (resourceId, endpoint, apiKey) => ({
delete: (resourceId: string, endpoint: string, apiKey: string) => ({
title: 'Delete matching rows',
bash: {
language: 'bash',
@@ -474,7 +496,7 @@ const { error } = await supabase
`,
},
}),
authSignup: (endpoint, apiKey, randomPassword) => ({
authSignup: (endpoint: string, apiKey: string, randomPassword: string) => ({
title: 'User signup',
bash: {
language: 'bash',
@@ -498,7 +520,7 @@ let { data, error } = await supabase.auth.signUp({
`,
},
}),
authLogin: (endpoint, apiKey, randomPassword) => ({
authLogin: (endpoint: string, apiKey: string, randomPassword: string) => ({
title: 'User login',
bash: {
language: 'bash',
@@ -522,7 +544,7 @@ let { data, error } = await supabase.auth.signInWithPassword({
`,
},
}),
authMagicLink: (endpoint, apiKey) => ({
authMagicLink: (endpoint: string, apiKey: string) => ({
title: 'User login',
bash: {
language: 'bash',
@@ -544,7 +566,7 @@ let { data, error } = await supabase.auth.signInWithOtp({
`,
},
}),
authPhoneSignUp: (endpoint, apiKey) => ({
authPhoneSignUp: (endpoint: string, apiKey: string) => ({
title: 'Phone Signup',
bash: {
language: 'bash',
@@ -568,7 +590,7 @@ let { data, error } = await supabase.auth.signUp({
`,
},
}),
authMobileOTPLogin: (endpoint, apiKey) => ({
authMobileOTPLogin: (endpoint: string, apiKey: string) => ({
title: 'Phone Login',
bash: {
language: 'bash',
@@ -590,7 +612,7 @@ let { data, error } = await supabase.auth.signInWithOtp({
`,
},
}),
authMobileOTPVerify: (endpoint, apiKey) => ({
authMobileOTPVerify: (endpoint: string, apiKey: string) => ({
title: 'Verify Pin',
bash: {
language: 'bash',
@@ -616,7 +638,7 @@ let { data, error } = await supabase.auth.verifyOtp({
`,
},
}),
authInvite: (endpoint, apiKey) => ({
authInvite: (endpoint: string, apiKey: string) => ({
title: 'Invite User',
bash: {
language: 'bash',
@@ -637,7 +659,7 @@ let { data, error } = await supabase.auth.api.inviteUserByEmail('someone@email.c
`,
},
}),
authThirdPartyLogin: (endpoint, apiKey) => ({
authThirdPartyLogin: () => ({
title: 'Third Party Login',
bash: {
language: 'bash',
@@ -652,7 +674,7 @@ let { data, error } = await supabase.auth.signInWithOAuth({
`,
},
}),
authUser: (endpoint, apiKey) => ({
authUser: (endpoint: string, apiKey: string) => ({
title: 'Get User',
bash: {
language: 'bash',
@@ -669,7 +691,7 @@ const { data: { user } } = await supabase.auth.getUser()
`,
},
}),
authRecover: (endpoint, apiKey) => ({
authRecover: (endpoint: string, apiKey: string) => ({
title: 'Password Recovery',
bash: {
language: 'bash',
@@ -689,7 +711,7 @@ let { data, error } = await supabase.auth.resetPasswordForEmail(email)
`,
},
}),
authUpdate: (endpoint, apiKey) => ({
authUpdate: (endpoint: string, apiKey: string) => ({
title: 'Update User',
bash: {
language: 'bash',
@@ -718,7 +740,7 @@ const { data, error } = await supabase.auth.updateUser({
`,
},
}),
authLogout: (endpoint, apiKey, randomPassword) => ({
authLogout: (endpoint: string, apiKey: string) => ({
title: 'User logout',
bash: {
language: 'bash',
@@ -1,11 +1,21 @@
import { Form, Modal, Input, Button } from 'ui'
import dayjs from 'dayjs'
import { observer } from 'mobx-react-lite'
import { Button, Form, Input, Listbox, Modal } from 'ui'
import { DATETIME_FORMAT } from 'lib/constants'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
export interface CustomExpiryModalProps {
onCopyUrl: (name: string, url: string) => void
}
const unitMap = {
days: 3600 * 24,
weeks: 3600 * 24 * 7,
months: 3600 * 24 * 30,
years: 3600 * 24 * 365,
}
const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
const storageExplorerStore = useStorageStore()
const { getFileUrl, selectedFileCustomExpiry, setSelectedFileCustomExpiry } = storageExplorerStore
@@ -25,34 +35,57 @@ const CustomExpiryModal = ({ onCopyUrl }: CustomExpiryModalProps) => {
>
<Form
validateOnBlur
initialValues={{ expiresIn: '' }}
initialValues={{ expiresIn: '', units: 'days' }}
onSubmit={async (values: any, { setSubmitting }: any) => {
setSubmitting(true)
onCopyUrl(
selectedFileCustomExpiry.name,
await getFileUrl(selectedFileCustomExpiry, values.expiresIn)
await getFileUrl(
selectedFileCustomExpiry,
values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years']
)
)
setSubmitting(false)
onClose()
}}
validate={(values: any) => {
const errors: any = {}
if (values.expiresIn !== '' && values.expiresIn <= 0)
if (values.expiresIn !== '' && values.expiresIn <= 0) {
errors.expiresIn = 'Expiry duration cannot be less than 0'
}
return errors
}}
>
{({ values, isSubmitting }: { values: any; isSubmitting: boolean }) => (
<>
<div className="py-6">
<div className="pt-4 pb-2">
<Modal.Content>
<Input
disabled={isSubmitting}
type="number"
id="expiresIn"
label="Enter the duration for which the URL will be valid:"
actions={<p className="text-sm text-scale-1000 mr-2">seconds</p>}
/>
<p className="text-sm text-light mb-2">
Enter the duration for which the URL will be valid for:
</p>
<div className="flex items-center space-x-2">
<Input disabled={isSubmitting} type="number" id="expiresIn" className="w-full" />
<Listbox id="units" className="w-[150px]">
<Listbox.Option id="days" label="days" value="days">
days
</Listbox.Option>
<Listbox.Option id="weeks" label="weeks" value="weeks">
weeks
</Listbox.Option>
<Listbox.Option id="months" label="months" value="months">
months
</Listbox.Option>
<Listbox.Option id="years" label="years" value="years">
years
</Listbox.Option>
</Listbox>
</div>
{values.expiresIn !== '' && (
<p className="text-sm text-light mt-2">
URL will expire on{' '}
{dayjs().add(values.expiresIn, values.units).format(DATETIME_FORMAT)}
</p>
)}
</Modal.Content>
</div>
<Modal.Separator />
@@ -409,6 +409,7 @@ const FileExplorerRow = ({
/>
) : (
<Dropdown
modal={false}
side="bottom"
align="end"
overlay={[
@@ -57,10 +57,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
const { ui } = useStore()
const { ref } = useParams()
const { data: customDomainData } = useCustomDomainsQuery(
{ projectRef: ref },
{ enabled: IS_PLATFORM }
)
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref })
const apiService = (projectSettings?.services ?? []).find(
(x) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
+7 -2
View File
@@ -31,8 +31,13 @@ const AlertError = ({ ref, subject, error, className }: AlertErrorProps) => {
<IconAlertCircle className="h-4 w-4" />
<AlertTitle_Shadcn_>{subject}</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
{error?.message && <p>Error: {error?.message}</p>}
Try refreshing your browser, but if the issue persists, please reach out to us via support.
<div>
{error?.message && <p>Error: {error?.message}</p>}
<p>
Try refreshing your browser, but if the issue persists, please reach out to us via
support.
</p>
</div>
<div>
<Link key="contact-support" href={href} passHref>
<Button type="warning" asChild>
+2 -2
View File
@@ -27,7 +27,7 @@ const CardButton = ({
imgAlt,
onClick,
icon,
containerHeightClassName = 'h-32',
containerHeightClassName = '',
loading = false,
}: PropsWithChildren<CardButtonProps>) => {
const LinkContainer = ({ children }: { children: React.ReactNode }) => (
@@ -47,7 +47,7 @@ const CardButton = ({
'group relative text-left',
'bg-panel-header-light dark:bg-panel-header-dark',
'border border-panel-border-light dark:border-panel-border-dark',
'rounded-md py-4 px-6 flex flex-row',
'rounded-md py-4 flex flex-row',
'transition ease-in-out duration-150',
containerHeightClassName,
]
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more