Bring changes from 14058 and resolve conflicts
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -63,6 +63,10 @@
|
||||
"text": "Brand Assets / Logos",
|
||||
"url": "/brand-assets"
|
||||
},
|
||||
{
|
||||
"text": "Security and Compliance",
|
||||
"url": "/security"
|
||||
},
|
||||
{
|
||||
"text": "DPA",
|
||||
"url": "/legal/dpa"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||