Merge branch 'master' into hf/fix-nullable-issues
No files matched your search
@@ -357,6 +357,11 @@ export const SocialLoginItems = [
|
||||
icon: '/docs/img/icons/discord-icon',
|
||||
url: '/guides/auth/social-login/auth-discord',
|
||||
},
|
||||
{
|
||||
name: 'Figma',
|
||||
icon: '/docs/img/icons/figma-icon',
|
||||
url: '/guides/auth/social-login/auth-figma',
|
||||
},
|
||||
{
|
||||
name: 'Kakao',
|
||||
icon: '/docs/img/icons/kakao-icon',
|
||||
|
||||
@@ -49,6 +49,16 @@ const authProviders = [
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Figma',
|
||||
logo: '/docs/img/icons/figma-icon',
|
||||
href: '/guides/auth/social-login/auth-figma',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
logo: '/docs/img/icons/github-icon',
|
||||
|
||||
@@ -4,29 +4,24 @@ export const TabPanel = Tabs.Panel
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-choosing-compute-addon',
|
||||
title: 'Choosing Compute Add-on',
|
||||
description: 'Choosing the right Compute Add-on for your workload.',
|
||||
subtitle: 'Choosing the right Compute Add-on for your workload.',
|
||||
title: 'Choosing your Compute Add-on',
|
||||
description: 'Choosing the right Compute Add-on for your vector workload.',
|
||||
subtitle: 'Choosing the right Compute Add-on for your vector workload.',
|
||||
sidebar_label: 'Choosing Compute Add-on',
|
||||
}
|
||||
|
||||
This guide will help you choose the right Compute Add-on for your workload. We'll provide general guidance, as it is impossible to provide specific instructions for every possible use case. The goal is to give you a starting point from which you can make your own benchmarks and optimizations.
|
||||
You have two options for scaling your vector workload:
|
||||
|
||||
Note that it is only useful for index searches, not for sequential scans. Sequential scans will result to significantly higher latencies and lower throughput, but will guarantee 100% precision and will not be RAM bound. Therefore it is possible to use a smaller plan for sequential scans.
|
||||
1. Increase the size of your database. This guide will help you choose the right size for your workload.
|
||||
2. Spread your workload across multiple databases. You can find more details about this approach in [Engineering for Scale](engineering-for-scale).
|
||||
|
||||
For more information about engineering at scale, see our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide.
|
||||
## Dimensionality
|
||||
|
||||
## Simple workloads
|
||||
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:
|
||||
|
||||
We've run a set of benchmarks using
|
||||
### 1536 Dimensions
|
||||
|
||||
- The [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset. This dataset contains 1,000,000 embeddings for text, with each embedding being 1536 dimensions made using OpenAI API.
|
||||
- The [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset. This dataset contains 1,000,000 embeddings for images, with each embedding being 960 dimensions.
|
||||
- The [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) dataset, which contains 1,623,397 embeddings for words, with each embedding being 512 dimensions.
|
||||
|
||||
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:
|
||||
|
||||
### Results
|
||||
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. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -36,8 +31,6 @@ We used [Vecs](https://github.com/supabase/vecs) to create a collection, upload
|
||||
>
|
||||
<TabPanel id="dbpedia1536" label="OpenAI-1536, probes = 10">
|
||||
|
||||
Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vectors.
|
||||
|
||||
| Plan | Vectors | Lists | RPS | 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 |
|
||||
@@ -56,8 +49,6 @@ For 1,000,000 vectors 10 probes results to precision of 0.91. And for 500,000 ve
|
||||
</TabPanel>
|
||||
<TabPanel id="dbpedia1536_40" label="OpenAI-1536, probes = 40">
|
||||
|
||||
Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vectors.
|
||||
|
||||
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
|
||||
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
|
||||
| Free | 20,000 | 40 | - | - | - | - | 1 GB |
|
||||
@@ -74,9 +65,32 @@ Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vecto
|
||||
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.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="gist960" label="gist-960, probes = 10">
|
||||
</Tabs>
|
||||
|
||||
Emdeddings of 960 dimensions from gist-960 dataset with 1,000,000 vectors.
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/going-prod/size-to-rps--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/going-prod/size-to-rps--dark.png"
|
||||
/>
|
||||
</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 |
|
||||
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
|
||||
@@ -92,9 +106,19 @@ Emdeddings of 960 dimensions from gist-960 dataset with 1,000,000 vectors.
|
||||
| 16XL | 1,000,000 | 1000 | 1345 | 0.072 sec | 0.106 sec | 17.5 GB | 256 GB |
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="glove512" label="GloVe-512, probes = 10">
|
||||
</Tabs>
|
||||
|
||||
Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 vectors.
|
||||
### 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 |
|
||||
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
|
||||
@@ -109,13 +133,9 @@ Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 v
|
||||
| 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 |
|
||||
|
||||
Random vectors were generated for queries.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="glove512_60" label="GloVe-512, probes = 60">
|
||||
|
||||
Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 vectors.
|
||||
|
||||
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
|
||||
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
|
||||
| Free | 100,000 | 100 | - | - | - | - | 1 GB |
|
||||
@@ -130,8 +150,6 @@ Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 v
|
||||
| 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 |
|
||||
|
||||
Random vectors were generated for queries.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -141,7 +159,34 @@ It is possible to upload more vectors to a single table if Memory allows it (for
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Methodology
|
||||
## Performance tips
|
||||
|
||||
There are various ways to improve your pgvector performance. Here are some tips:
|
||||
|
||||
### Pre-warming your database
|
||||
|
||||
It's useful to execute a few thousand “warm-up” queries before going into production. This helps help with RAM utilization. This can also help to determine that you've selected the right instance size for your workload.
|
||||
|
||||
### Increase the number of lists
|
||||
|
||||
You can increase the Requests per Second by increasing the number of `lists`. This also has an important caveat: building the index takes longer with more lists.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/instance-type/lists-for-1m--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/instance-type/lists-for-1m--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Check out more tips and the complete step-by-step guide in [Going to Production for AI applications](going-to-prod).
|
||||
|
||||
## Benchmark Methodology
|
||||
|
||||
We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology. A Python test runner is responsible for uploading the data, creating the index, and running the queries. The pgvector engine is implemented using [vecs](https://github.com/supabase/vecs), a Python client for pgvector.
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-figma',
|
||||
title: 'Login with Figma',
|
||||
description: 'Add Figma OAuth to your Supabase project',
|
||||
}
|
||||
|
||||
To enable Figma Auth for your project, you need to set up a Figma OAuth application and add the application credentials to your Supabase Dashboard.
|
||||
|
||||
## Overview
|
||||
|
||||
Setting up Figma logins for your application consists of 3 parts:
|
||||
|
||||
- Create and configure a Figma App on the [Figma Developers page](https://www.figma.com/developers).
|
||||
- Add your Figma `client_id` and `client_secret` to your [Supabase Project](https://app.supabase.com).
|
||||
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
|
||||
|
||||
## Access the Figma Developers page
|
||||
|
||||
- Go to the [Figma Developers page](https://www.figma.com/developers)
|
||||
- Click on `My apps` at the top right
|
||||
- Log in (if necessary)
|
||||
|
||||

|
||||
|
||||
## Find your callback URL
|
||||
|
||||
<SocialProviderSetup provider="Figma" />
|
||||
|
||||
## Create a Figma OAuth app
|
||||
|
||||
- Enter your `App name`, `Website URL` and upload your app logo
|
||||
- Click on `Add callback`
|
||||
- Add your `Callback URL`
|
||||
- Click on `Save`
|
||||
|
||||

|
||||
|
||||
- Copy and save your newly-generated `Client ID`
|
||||
- Copy and save your newly-generated `Client Secret`
|
||||
|
||||

|
||||
|
||||
## Enter your Figma credentials into your Supabase Project
|
||||
|
||||
<SocialProviderSettingsSupabase provider="Figma" />
|
||||
|
||||
## Add login code to your client app
|
||||
|
||||
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `figma` as the `provider`:
|
||||
|
||||
```js
|
||||
async function signInWithFigma() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'figma',
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
|
||||
|
||||
```js
|
||||
async function signout() {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Figma Developers page](https://www.figma.com/developers)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="300" height="300" viewBox="-50 0 300 300" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#0acf83}.st1{fill:#a259ff}.st2{fill:#f24e1e}.st3{fill:#ff7262}.st4{fill:#1abcfe}</style><path id="path0_fill" class="st0" d="M50 300c27.6 0 50-22.4 50-50v-50H50c-27.6 0-50 22.4-50 50s22.4 50 50 50z"/><path id="path1_fill" class="st1" d="M0 150c0-27.6 22.4-50 50-50h50v100H50c-27.6 0-50-22.4-50-50z"/><path id="path1_fill_1_" class="st2" d="M0 50C0 22.4 22.4 0 50 0h50v100H50C22.4 100 0 77.6 0 50z"/><path id="path2_fill" class="st3" d="M100 0h50c27.6 0 50 22.4 50 50s-22.4 50-50 50h-50V0z"/><path id="path3_fill" class="st4" d="M200 150c0 27.6-22.4 50-50 50s-50-22.4-50-50 22.4-50 50-50 50 22.4 50 50z"/></svg>
|
||||
|
After Width: | Height: | Size: 717 B |
@@ -18056,11 +18056,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase": {
|
||||
"version": "0.2.2",
|
||||
"license": "MIT",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cmdk-supabase/-/cmdk-supabase-1.0.7.tgz",
|
||||
"integrity": "sha512-5Ts9dZTLSqNwCEFzps/dtNK6Q1baje1JuA+5qjEvtfkk+SGZG0S5D1uqxbAk7YQqF/ctGZHxe8O+0+vNPYMk2A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-id": "^0.0.6",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"command-score": "0.1.2",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
@@ -18069,112 +18071,10 @@
|
||||
"react-dom": "^17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.0",
|
||||
"@radix-ui/react-focus-guards": "1.0.0",
|
||||
"@radix-ui/react-focus-scope": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-portal": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-slot": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-id": {
|
||||
"version": "0.0.6",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.0.6.tgz",
|
||||
"integrity": "sha512-PzmraF34fYggsYvTIZVJ5S68WMp3aKUN3HkSmGnz4zn9zpRjkAbbg7Xn3ueQI3FQsLWKgyUfnpsmWFDndpcqYg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
@@ -18182,119 +18082,6 @@
|
||||
"react": "^16.8 || ^17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/react-remove-scroll": {
|
||||
"version": "2.5.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"dev": true,
|
||||
@@ -18437,7 +18224,8 @@
|
||||
},
|
||||
"node_modules/command-score": {
|
||||
"version": "0.1.2",
|
||||
"license": "MIT"
|
||||
"resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz",
|
||||
"integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w=="
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
@@ -40809,7 +40597,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk-supabase": "^0.2.2",
|
||||
"cmdk-supabase": "^1.0.7",
|
||||
"color": "^4.2.3",
|
||||
"colorjs.io": "^0.4.3",
|
||||
"common-tags": "^1.8.2",
|
||||
|
||||
@@ -139,6 +139,13 @@ export {
|
||||
PopoverContent as PopoverContent_Shadcn_,
|
||||
} from './src/components/shadcn/ui/popover'
|
||||
|
||||
export {
|
||||
Accordion as Accordion_Shadcn_,
|
||||
AccordionItem as AccordionItem_Shadcn_,
|
||||
AccordionTrigger as AccordionTrigger_Shadcn_,
|
||||
AccordionContent as AccordionContent_Shadcn_,
|
||||
} from './src/components/shadcn/ui/accordion'
|
||||
|
||||
// links
|
||||
|
||||
export * from './src/components/TextLink'
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk-supabase": "^0.2.2",
|
||||
"cmdk-supabase": "^1.0.7",
|
||||
"color": "^4.2.3",
|
||||
"colorjs.io": "^0.4.3",
|
||||
"common-tags": "^1.8.2",
|
||||
|
||||
@@ -103,7 +103,7 @@ export function Item({ icon, className, ...props }: ItemProps) {
|
||||
{...props}
|
||||
>
|
||||
{icon && icon}
|
||||
<span>{props.children}</span>
|
||||
<span className="w-full">{props.children}</span>
|
||||
</RadixDropdown.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { hideIcon?: boolean }
|
||||
>(({ className, children, hideIcon, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
|
||||
{!hideIcon && <ChevronDown className="h-4 w-4 transition-transform duration-200" />}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
|
||||
@@ -70,8 +70,12 @@ CommandList.displayName = CommandPrimitive.List.displayName
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className={cn('py-6 text-center text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
@@ -153,7 +153,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -1768,13 +1768,22 @@ functions:
|
||||
- id: conditional-chaining
|
||||
name: Conditional Chaining
|
||||
description: |
|
||||
Filters can be built up one step at a time and then executed. For example:
|
||||
Filters can be built up one step at a time and then executed as shown in the example code.
|
||||
code: |
|
||||
```dart
|
||||
final data = await supabase
|
||||
.from('users')
|
||||
.select()
|
||||
.eq('address->postcode', 90210);
|
||||
final filterByName = null;
|
||||
final filterPopLow = 1000;
|
||||
final filterPopHigh = 10000;
|
||||
|
||||
var query = supabase
|
||||
.from('cities')
|
||||
.select('name, country_id');
|
||||
|
||||
if (filterByName != null) { query = query.eq('name', filterByName); }
|
||||
if (filterPopLow != null) { query = query.gte('population', filterPopLow); }
|
||||
if (filterPopHigh != null) { query = query.lt('population', filterPopHigh); }
|
||||
|
||||
final data = await query;
|
||||
```
|
||||
- id: filter-by-value-within-json-column
|
||||
name: Filter by values within a JSON column
|
||||
|
||||
@@ -25,7 +25,7 @@ const AccessTokenList = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl">
|
||||
<div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="header-token">Token</Table.th>,
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner'
|
||||
import { useFlag } from 'hooks'
|
||||
import { IconExternalLink } from 'ui'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
const AppBannerWrapper: FC = ({ children }) => {
|
||||
const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col">
|
||||
{ongoingIncident && (
|
||||
<Link href="https://status.supabase.com">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 dark:text-scale-1200">
|
||||
<p className="text-sm font-medium">
|
||||
We are currently investigating a technical issue, follow status.supabase.com for
|
||||
updates
|
||||
</p>
|
||||
<IconExternalLink size={16} strokeWidth={2} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{ongoingIncident && <IncidentBanner />}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -4,16 +4,17 @@ import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useParams } from 'common'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
|
||||
// Ideally these could all be within a _middleware when we use Next 12
|
||||
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const orgSlug = router.query.slug
|
||||
const { ref, slug } = useParams()
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
/**
|
||||
* Array of urls/routes that should be ignored
|
||||
@@ -43,14 +44,14 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (orgsInitialized && orgSlug) {
|
||||
if (orgsInitialized && slug) {
|
||||
// Check validity of organization that user is trying to access
|
||||
const organizations = organizationsRef.current ?? []
|
||||
const isValidOrg = organizations.some((org) => org.slug === orgSlug)
|
||||
const isValidOrg = organizations.some((org) => org.slug === slug)
|
||||
|
||||
if (!isValidOrg) {
|
||||
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
|
||||
router.push('/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : '/projects')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -63,19 +64,43 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (projectsInitialized && projectRef) {
|
||||
if (projectsInitialized && ref) {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = projectsRef.current ?? []
|
||||
const isValidProject = projects.some((project) => project.ref === projectRef)
|
||||
const isValidProject = projects.some((project) => project.ref === ref)
|
||||
|
||||
if (!isValidProject) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push('/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : '/projects')
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [projectsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
if (orgsInitialized && slug) {
|
||||
// Save organization slug to local storage
|
||||
const organizations = organizationsRef.current ?? []
|
||||
const organization = organizations.find((org) => org.slug === slug)
|
||||
if (organization) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
|
||||
}
|
||||
}
|
||||
}, [slug, orgsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
if (projectsInitialized && ref) {
|
||||
// Save organization slug to local storage
|
||||
const projects = projectsRef.current ?? []
|
||||
const project = projects.find((project) => project.ref === ref)
|
||||
const organizationId = project?.organization_id
|
||||
const organization = organizations?.find((organization) => organization.id === organizationId)
|
||||
if (organization) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
|
||||
}
|
||||
}
|
||||
}, [ref, projectsInitialized])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ export interface AuthConfig {
|
||||
EXTERNAL_FACEBOOK_CLIENT_ID: string | null
|
||||
EXTERNAL_FACEBOOK_SECRET: string | null
|
||||
|
||||
EXTERNAL_FIGMA_ENABLED: boolean
|
||||
EXTERNAL_FIGMA_CLIENT_ID: string | null
|
||||
EXTERNAL_FIGMA_SECRET: string | null
|
||||
|
||||
EXTERNAL_GITHUB_ENABLED: false
|
||||
EXTERNAL_GITHUB_CLIENT_ID: string | null
|
||||
EXTERNAL_GITHUB_SECRET: string | null
|
||||
|
||||
@@ -24,7 +24,7 @@ const ProjectCard = ({ project, rewriteHref }: ProjectCardProps) => {
|
||||
const isRestoring = project.status === PROJECT_STATUS.RESTORING
|
||||
|
||||
return (
|
||||
<li className="col-span-1">
|
||||
<li className="col-span-1 list-none">
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
title={
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Collapsible, Form, IconChevronRight, Input, Toggle } from 'ui'
|
||||
|
||||
import { useParams } from 'common/hooks'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import {
|
||||
FormActions,
|
||||
FormPanel,
|
||||
@@ -18,9 +19,8 @@ import { invalidateOrganizationsQuery } from 'data/organizations/organizations-q
|
||||
import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { isResponseOk, patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import OrganizationDeletePanel from './OrganizationDeletePanel'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import { Organization } from 'types'
|
||||
import OrganizationDeletePanel from './OrganizationDeletePanel'
|
||||
|
||||
const GeneralSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -82,7 +82,7 @@ const InfrastructureInfo = () => {
|
||||
label="Current version"
|
||||
actions={[
|
||||
isOnLatestVersion && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Root key="tooltip-latest" delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Badge color="green" className="mr-1">
|
||||
Latest
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSignOut } from 'lib/auth'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { SidebarSection } from './AccountLayout.types'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import SettingsLayout from '../SettingsLayout/SettingsLayout'
|
||||
|
||||
export interface AccountLayoutProps {
|
||||
title: string
|
||||
@@ -23,12 +24,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
const signOut = useSignOut()
|
||||
const onClickLogout = async () => {
|
||||
await signOut()
|
||||
|
||||
await router.push('/sign-in')
|
||||
}
|
||||
|
||||
@@ -126,6 +127,10 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
: []),
|
||||
]
|
||||
|
||||
if (navLayoutV2) {
|
||||
return <SettingsLayout>{children}</SettingsLayout>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -137,7 +142,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
style={{ height: maxHeight, maxHeight }}
|
||||
className="flex flex-col flex-1 w-full overflow-y-auto"
|
||||
>
|
||||
<WithSidebar title={title} breadcrumbs={breadcrumbs} sections={sectionsWithHeaders}>
|
||||
<WithSidebar
|
||||
hideSidebar={navLayoutV2}
|
||||
title={title}
|
||||
breadcrumbs={breadcrumbs}
|
||||
sections={sectionsWithHeaders}
|
||||
>
|
||||
{children}
|
||||
</WithSidebar>
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useFlag } from 'hooks'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Menu, IconArrowUpRight, Badge, IconLogOut } from 'ui'
|
||||
import { useFlag } from 'hooks'
|
||||
import { Badge, IconArrowUpRight, IconLogOut, Menu } from 'ui'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
import { SidebarLink, SidebarSection } from './AccountLayout.types'
|
||||
|
||||
interface Props {
|
||||
interface WithSidebarProps {
|
||||
title: string
|
||||
breadcrumbs: any[]
|
||||
sections: SidebarSection[]
|
||||
@@ -27,7 +27,7 @@ The information hierarchy for WithSidebar is:
|
||||
SidebarItem
|
||||
SidebarLink
|
||||
*/
|
||||
const WithSidebar: FC<Props> = ({
|
||||
const WithSidebar = ({
|
||||
title,
|
||||
header,
|
||||
breadcrumbs = [],
|
||||
@@ -37,9 +37,10 @@ const WithSidebar: FC<Props> = ({
|
||||
subitemsParentKey,
|
||||
hideSidebar = false,
|
||||
customSidebarContent,
|
||||
}) => {
|
||||
}: WithSidebarProps) => {
|
||||
const noContent = !sections && !customSidebarContent
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
return (
|
||||
@@ -89,7 +90,7 @@ const WithSidebar: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col">
|
||||
<LayoutHeader breadcrumbs={breadcrumbs} />
|
||||
{!navLayoutV2 && <LayoutHeader breadcrumbs={breadcrumbs} />}
|
||||
<div className="flex-1 flex-grow overflow-auto">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
|
||||
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
|
||||
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
|
||||
import OrganizationDropdown from './OrganizationDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
|
||||
const AppHeader = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-1 bg-scale-200 border-b">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href={organization !== undefined ? `/org/${organization?.slug}` : '/'}>
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<OrganizationDropdown />
|
||||
{ref !== undefined && <ProjectDropdown />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<FeedbackDropdown alt />
|
||||
<NotificationsPopover alt />
|
||||
<HelpPopover alt />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<UserSettingsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppHeader
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import AppHeader from './AppHeader'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
const AppLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
|
||||
const appHeaderHeight = navLayoutV2 ? 49 : 0
|
||||
const incidentBannerHeight = 44
|
||||
|
||||
return (
|
||||
<div>
|
||||
{navLayoutV2 && <AppHeader />}
|
||||
<div
|
||||
style={{
|
||||
height: ongoingIncident
|
||||
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
|
||||
: `calc(100vh - ${appHeaderHeight}px)`,
|
||||
maxHeight: ongoingIncident
|
||||
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
|
||||
: `calc(100vh - ${appHeaderHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
import { IconExternalLink } from 'ui'
|
||||
|
||||
const IncidentBanner = () => {
|
||||
return (
|
||||
<Link href="https://status.supabase.com">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 dark:text-scale-1200">
|
||||
<p className="text-sm font-medium">
|
||||
We are currently investigating a technical issue, follow status.supabase.com for updates
|
||||
</p>
|
||||
<IconExternalLink size={16} strokeWidth={2} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default IncidentBanner
|
||||
@@ -0,0 +1,129 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
|
||||
const OrganizationDropdown = () => {
|
||||
const router = useRouter()
|
||||
const orgCreationV2 = useFlag('orgcreationv2')
|
||||
const orgNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
|
||||
|
||||
const slug = selectedOrganization?.slug
|
||||
const orgName = selectedOrganization?.name
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useOrgSubscriptionQuery(
|
||||
{ orgSlug: slug },
|
||||
{ enabled: isOrgBilling }
|
||||
)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (orgNameRef.current?.offsetWidth ?? 0) + 12
|
||||
|
||||
if (isLoadingOrganizations) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link passHref href={slug ? `/org/${slug}` : '/'}>
|
||||
<a ref={orgNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{orgName}</p>
|
||||
{isSuccess && <Badge color="scale">{subscription?.plan.name}</Badge>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find organization..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No organizations found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{organizations?.map((org) => {
|
||||
const href = router.pathname.includes('[slug]')
|
||||
? router.pathname.replace('[slug]', org.slug)
|
||||
: router.pathname.includes('[ref]/settings')
|
||||
? `/org/${org.slug}/general`
|
||||
: `/org/${org.slug}`
|
||||
return (
|
||||
<Link passHref href={href} key={org.slug}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
value={org.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(href)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
{org.name}
|
||||
{org.slug === slug && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link passHref href={orgCreationV2 ? `/new-with-subscription` : `/new`}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={(e) => {
|
||||
setOpen(false)
|
||||
router.push(orgCreationV2 ? `/new-with-subscription` : `/new`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconPlus size={14} strokeWidth={1.5} />
|
||||
<p>New organization</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationDropdown
|
||||
@@ -0,0 +1,200 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { Organization, Project } from 'types'
|
||||
import { useParams } from 'common'
|
||||
|
||||
// [Fran] the idea is to let users change projects without losing the current page,
|
||||
// but at the same time we need to redirect correctly between urls that might be
|
||||
// unique to a project e.g. '/project/projectRef/editor/tableId'
|
||||
// Right now, I'm gonna assume that any router query after the projectId,
|
||||
// is a unique project id/marker so we'll redirect the user to the
|
||||
// highest common route with just projectRef in the router queries.
|
||||
|
||||
export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
|
||||
const queryArray = Object.entries(routerQueries)
|
||||
|
||||
if (queryArray.length > 1) {
|
||||
// [Joshen] Ideally we shouldn't use hard coded numbers, but temp workaround
|
||||
// for storage bucket route since its longer
|
||||
const isStorageBucketRoute = 'bucketId' in routerQueries
|
||||
return route
|
||||
.split('/')
|
||||
.slice(0, isStorageBucketRoute ? 5 : 4)
|
||||
.join('/')
|
||||
} else {
|
||||
return route
|
||||
}
|
||||
}
|
||||
|
||||
const ProjectLink = ({
|
||||
project,
|
||||
organization,
|
||||
setOpen,
|
||||
}: {
|
||||
project: Project
|
||||
organization?: Organization
|
||||
setOpen: (value: boolean) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const sanitizedRoute = sanitizeRoute(router.route, router.query)
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// [Joshen] Temp while we're interim between v1 and v2 billing
|
||||
let href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}`
|
||||
if (href.endsWith('settings/addons') && !isOrgBilling) {
|
||||
href = href.replace('settings/addons', 'settings/billing/subscription')
|
||||
} else if (href.endsWith('settings/billing/subscription') && isOrgBilling) {
|
||||
href = href.replace('settings/billing/subscription', 'settings/addons')
|
||||
} else if (href.endsWith('settings/infrastructure') && !isOrgBilling) {
|
||||
href = href.replace('settings/infrastructure', 'settings/billing/usage')
|
||||
} else if (href.endsWith('settings/billing/usage') && !isOrgBilling) {
|
||||
href = href.replace('settings/billing/usage', 'settings/infrastructure')
|
||||
}
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
key={project.ref}
|
||||
value={project.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
router.push(href)
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
{project.name}
|
||||
{project.ref === ref && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const projectNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: ref },
|
||||
{ enabled: !isOrgBilling }
|
||||
)
|
||||
const projects = allProjects
|
||||
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
|
||||
.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const selectedProject = projectDetails || projects?.find((project) => project.ref === ref)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (projectNameRef.current?.offsetWidth ?? 0) + 12
|
||||
|
||||
if (isLoadingProjects) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link href={`/project/${selectedProject?.ref}`}>
|
||||
<a ref={projectNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{selectedProject?.name}</p>
|
||||
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find project..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No projects found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{projects?.map((project) => (
|
||||
<ProjectLink
|
||||
key={project.ref}
|
||||
project={project}
|
||||
organization={selectedOrganization}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link
|
||||
passHref
|
||||
href={`/new/${selectedOrganization?.slug}`}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(`/new/${selectedOrganization?.slug}`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconPlus size={14} strokeWidth={1.5} />
|
||||
<p>New project</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectDropdown
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { IconSettings } from 'ui'
|
||||
|
||||
const SettingsButton = () => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const slug = selectedOrganization?.slug
|
||||
|
||||
return (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Link href={slug ? `/org/${slug}/general` : '/'} passHref>
|
||||
<Tooltip.Trigger type="button" asChild className="px-1">
|
||||
<a>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
</a>
|
||||
</Tooltip.Trigger>
|
||||
</Link>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">Settings</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsButton
|
||||
@@ -0,0 +1,120 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
DropdownMenuCheckboxItem_Shadcn_,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuGroup_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuLabel_Shadcn_,
|
||||
DropdownMenuRadioGroup_Shadcn_,
|
||||
DropdownMenuRadioItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuShortcut_Shadcn_,
|
||||
DropdownMenuSubContent_Shadcn_,
|
||||
DropdownMenuSubTrigger_Shadcn_,
|
||||
DropdownMenuSub_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCheck,
|
||||
IconUser,
|
||||
useCommandMenu,
|
||||
} from 'ui'
|
||||
|
||||
import { useTheme } from 'common'
|
||||
import { useSignOut } from 'lib/auth'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useState } from 'react'
|
||||
|
||||
const UserSettingsDropdown = () => {
|
||||
const signOut = useSignOut()
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { profile } = useProfile()
|
||||
const { setIsOpen: setCommandMenuOpen } = useCommandMenu()
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
|
||||
const onClickLogout = async () => {
|
||||
await signOut()
|
||||
await router.push('/sign-in')
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_ className="flex items-center justify-center border font-bold rounded-full h-7 w-7 text-scale-1100 bg-surface-100">
|
||||
{profile?.first_name ? profile?.first_name?.[0] : <IconUser size={14} strokeWidth={2} />}
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ align="end" className="w-60">
|
||||
<DropdownMenuGroup_Shadcn_>
|
||||
<div key="profile" className="px-2 py-1.5">
|
||||
<p className="text-sm text-scale-1200">
|
||||
{profile?.first_name} {profile?.last_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000 truncate">{profile?.primary_email}</p>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<Link passHref href="/account/me">
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
router.push('/account/me')
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
asChild
|
||||
>
|
||||
<a>Preferences</a>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</Link>
|
||||
<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>
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
setCommandMenuOpen(true)
|
||||
}}
|
||||
>
|
||||
<span>Command menu</span>
|
||||
<DropdownMenuShortcut_Shadcn_>⌘K</DropdownMenuShortcut_Shadcn_>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuLabel_Shadcn_>Theme</DropdownMenuLabel_Shadcn_>
|
||||
<DropdownMenuRadioGroup_Shadcn_
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onValueChange={(x) => {
|
||||
const dark = x === 'dark'
|
||||
toggleTheme(Boolean(dark))
|
||||
}}
|
||||
>
|
||||
<DropdownMenuRadioItem_Shadcn_ value={'dark'}>Dark</DropdownMenuRadioItem_Shadcn_>
|
||||
<DropdownMenuRadioItem_Shadcn_ value={'light'}>Light</DropdownMenuRadioItem_Shadcn_>
|
||||
</DropdownMenuRadioGroup_Shadcn_>
|
||||
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
onClickLogout()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuGroup_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserSettingsDropdown
|
||||
@@ -5,16 +5,23 @@ import { PropsWithChildren } from 'react'
|
||||
import { Tabs } from 'ui'
|
||||
import { AccountLayout } from './'
|
||||
import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold'
|
||||
import SettingsLayout from './SettingsLayout/SettingsLayout'
|
||||
|
||||
const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const router = useRouter()
|
||||
const { slug } = useParams()
|
||||
const id = router.asPath.split('/').at(-1)?.split('?')[0]?.split('#')[0]
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const showOAuthApps = useFlag('oauthApps')
|
||||
const showAuditLogs = useFlag('auditLogs')
|
||||
const showIntegrationsV2 = useFlag('integrationsV2')
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
|
||||
if (navLayoutV2) {
|
||||
return <SettingsLayout>{children}</SettingsLayout>
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountLayout
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, IconMessageCircle, Popover } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
IconMessageCircle,
|
||||
Popover,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import FeedbackWidget from './FeedbackWidget'
|
||||
|
||||
const FeedbackDropdown = () => {
|
||||
interface FeedbackDropdownProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [screenshot, setScreenshot] = useState<string>()
|
||||
@@ -18,16 +29,29 @@ const FeedbackDropdown = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<Popover_Shadcn_
|
||||
modal={false}
|
||||
open={isOpen}
|
||||
onOpenChange={(e) => {
|
||||
setIsOpen(e)
|
||||
if (!e) setScreenshot(undefined)
|
||||
}}
|
||||
size="content"
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
asChild
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={
|
||||
alt ? null : (
|
||||
<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="hidden md:flex">Feedback</span>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ side="bottom" align="end" className="w-full p-0">
|
||||
<FeedbackWidget
|
||||
onClose={onClose}
|
||||
feedback={feedback}
|
||||
@@ -35,17 +59,8 @@ const FeedbackDropdown = () => {
|
||||
screenshot={screenshot}
|
||||
setScreenshot={setScreenshot}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
<span className="hidden md:flex">Feedback</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,21 @@ import { toPng } from 'html-to-image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChangeEvent, FC, useEffect, useRef, useState } from 'react'
|
||||
import { Button, Dropdown, IconCamera, IconImage, IconUpload, IconX, Input, Popover } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCamera,
|
||||
IconImage,
|
||||
IconUpload,
|
||||
IconX,
|
||||
Input,
|
||||
Popover,
|
||||
} from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSendFeedbackMutation } from 'data/feedback/feedback-send'
|
||||
@@ -125,13 +139,12 @@ const FeedbackWidget: FC<Props> = ({
|
||||
value={feedback}
|
||||
onChange={onFeedbackChange}
|
||||
/>
|
||||
<Popover.Separator />
|
||||
<div className="w-full h-px bg-border" />
|
||||
<div className="w-80 space-y-3 px-3 py-2 pb-4">
|
||||
<div className="flex justify-between space-x-2">
|
||||
<Button type="default" onClick={onClose} className="hover:border-gray-500">
|
||||
<Button type="default" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{screenshot !== undefined ? (
|
||||
<div
|
||||
@@ -143,7 +156,7 @@ const FeedbackWidget: FC<Props> = ({
|
||||
}}
|
||||
className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
className={[
|
||||
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
|
||||
'flex items-center justify-center absolute -top-1 -right-1',
|
||||
@@ -154,42 +167,43 @@ const FeedbackWidget: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<IconX size={8} strokeWidth={3} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
className="feedback-dropdown"
|
||||
size="small"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
<DropdownMenu_Shadcn_>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
disabled={isSavingScreenshot}
|
||||
loading={isSavingScreenshot}
|
||||
className="px-2 py-1.5"
|
||||
icon={<IconImage size={14} />}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="flex gap-2"
|
||||
key="upload-screenshot"
|
||||
icon={<IconUpload size={14} />}
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
if (uploadButtonRef.current) (uploadButtonRef.current as any).click()
|
||||
}}
|
||||
>
|
||||
<IconUpload size={14} />
|
||||
Upload screenshot
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="flex gap-2"
|
||||
key="capture-screenshot"
|
||||
icon={<IconCamera size={14} />}
|
||||
onClick={() => captureScreenshot()}
|
||||
onSelect={() => captureScreenshot()}
|
||||
>
|
||||
<IconCamera size={14} />
|
||||
Capture screenshot
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
disabled={isSavingScreenshot}
|
||||
loading={isSavingScreenshot}
|
||||
className="px-2 py-1.5"
|
||||
icon={<IconImage size={14} />}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -1,148 +1,172 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import {
|
||||
Button,
|
||||
IconActivity,
|
||||
IconBookOpen,
|
||||
IconHelpCircle,
|
||||
IconMail,
|
||||
IconMessageCircle,
|
||||
Popover,
|
||||
IconBookOpen,
|
||||
IconActivity,
|
||||
IconTool,
|
||||
Popover,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
import SVG from 'react-inlinesvg'
|
||||
interface HelpPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
interface Props {}
|
||||
|
||||
const HelpPopover: FC<Props> = () => {
|
||||
const HelpPopover = ({ alt = false }: HelpPopoverProps) => {
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}`
|
||||
|
||||
return (
|
||||
<Popover
|
||||
size="content"
|
||||
align="end"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
overlay={
|
||||
<div className="my-4 w-[400px] space-y-4">
|
||||
<div className="my-5 space-y-4 px-5">
|
||||
<h5 className="text-scale-1200">Need help with your project?</h5>
|
||||
<p className="text-sm text-scale-900">
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our
|
||||
hosted services.
|
||||
</p>
|
||||
<div className="space-x-1">
|
||||
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
|
||||
<Button asChild type="default" icon={<IconTool />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Troubleshooting
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://supabase.com/docs/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://status.supabase.com/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Supabase Status
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Popover_Shadcn_>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className="relative flex items-center">
|
||||
<Button
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
icon={
|
||||
<IconHelpCircle
|
||||
size={alt ? 18 : 16}
|
||||
strokeWidth={1.5}
|
||||
className={alt ? 'text-scale-1100' : 'text-scale-900'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!alt && <span>Help</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
{alt ? (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'space-y-2 border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-scale-1200">Help</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
) : null}
|
||||
</Tooltip.Root>
|
||||
<PopoverContent_Shadcn_ className="w-[400px] space-y-4 p-0 py-5" align="end" side="bottom">
|
||||
<div className="mb-5 space-y-4 px-5">
|
||||
<h5 className="text-scale-1200">Need help with your project?</h5>
|
||||
<p className="text-sm text-scale-900">
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our hosted
|
||||
services.
|
||||
</p>
|
||||
<div className="space-x-1">
|
||||
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
|
||||
<Button asChild type="default" icon={<IconTool />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Troubleshooting
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://supabase.com/docs/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://status.supabase.com/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Supabase Status
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-sm text-scale-900">
|
||||
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
|
||||
prioritized.
|
||||
</p>
|
||||
<div>
|
||||
<Link passHref href={supportUrl}>
|
||||
<Button asChild type="default" icon={<IconMail />}>
|
||||
<a>Contact Support</a>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Separator />
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="mb-4 px-5">
|
||||
<h5 className={'mb-2'}>Reach out to the community</h5>
|
||||
|
||||
<p className="text-sm text-scale-900">
|
||||
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
|
||||
prioritized.
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
<div>
|
||||
<Link passHref href={supportUrl}>
|
||||
<Button asChild type="default" icon={<IconMail />}>
|
||||
<a>Contact Support</a>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div
|
||||
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
|
||||
style={{ background: '#404EED' }}
|
||||
>
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="dark block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button
|
||||
type="secondary"
|
||||
icon={<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />}
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Join Discord server</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Separator />
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="mb-4 px-5">
|
||||
<h5 className={'mb-2'}>Reach out to the community</h5>
|
||||
|
||||
<p className="text-sm text-scale-900">
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div
|
||||
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
|
||||
style={{ background: '#404EED' }}
|
||||
<div className="px-5">
|
||||
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
|
||||
<a
|
||||
href="https://github.com/supabase/supabase/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block cursor-pointer"
|
||||
>
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="dark block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button
|
||||
type="secondary"
|
||||
icon={
|
||||
<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />
|
||||
}
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Join Discord server</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
|
||||
<a
|
||||
href="https://github.com/supabase/supabase/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button type="secondary" icon={<IconMessageCircle />}>
|
||||
GitHub Discussions
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button type="secondary" icon={<IconMessageCircle />}>
|
||||
GitHub Discussions
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
icon={<IconHelpCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
<span>Help</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,19 @@ import { Fragment, useState } from 'react'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useNotificationsQuery } from 'data/notifications/notifications-query'
|
||||
import { getProjectDetail } from 'data/projects/project-detail-query'
|
||||
import { invalidateProjectsQuery, setProjectPostgrestStatus } from 'data/projects/projects-query'
|
||||
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_, patch, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Project } from 'types'
|
||||
import { Alert, Button, IconArrowRight, IconBell, Popover } from 'ui'
|
||||
import { Alert, Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui'
|
||||
import NotificationRow from './NotificationRow'
|
||||
|
||||
const NotificationsPopover = () => {
|
||||
interface NotificationsPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => {
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const { meta, ui } = useStore()
|
||||
@@ -33,11 +37,12 @@ const NotificationsPopover = () => {
|
||||
|
||||
const [targetNotification, setTargetNotification] = useState<Notification>()
|
||||
|
||||
if (!notifications || !Array.isArray(notifications)) return <></>
|
||||
if (!notifications || !Array.isArray(notifications)) return null
|
||||
|
||||
const hasNewNotifications = notifications.some(
|
||||
const newNotifications = notifications?.filter(
|
||||
(notification) => notification.notification_status === NotificationStatus.New
|
||||
)
|
||||
const hasNewNotifications = newNotifications.length > 0
|
||||
|
||||
const onOpenChange = async (open: boolean) => {
|
||||
// TODO(alaister): move this to a mutation
|
||||
@@ -232,21 +237,47 @@ const NotificationsPopover = () => {
|
||||
>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className="relative flex">
|
||||
<div className="relative flex items-center">
|
||||
{hasNewNotifications && (
|
||||
<>
|
||||
{alt ? null : (
|
||||
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
|
||||
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
id="notification-button"
|
||||
type="default"
|
||||
icon={<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />}
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
icon={
|
||||
hasNewNotifications ? (
|
||||
<div className="-mr-3.5 z-10 h-4 w-4 flex items-center justify-center rounded-full bg-white">
|
||||
<p className="text-xs text-scale-100">{newNotifications.length}</p>
|
||||
</div>
|
||||
) : alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
|
||||
)
|
||||
}
|
||||
iconRight={
|
||||
hasNewNotifications ? (
|
||||
<>
|
||||
{alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
{hasNewNotifications && (
|
||||
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
|
||||
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { Button, Dropdown, IconPlus, Popover } from 'ui'
|
||||
import { Badge, Button, Dropdown, IconCode, IconPlus, Popover } from 'ui'
|
||||
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { Organization, Project } from 'types'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
|
||||
// [Fran] the idea is to let users change projects without losing the current page,
|
||||
// but at the same time we need to redirect correctly between urls that might be
|
||||
@@ -71,13 +73,23 @@ const ProjectLink = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
const ProjectDropdown = ({ alt }: { alt?: boolean }) => {
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: allProjects } = useProjectsQuery()
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
const { data: allOrganizations } = useOrganizationsQuery()
|
||||
const selectedOrganizationSlug = selectedOrganization?.slug
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: selectedProject?.ref },
|
||||
{ enabled: alt && !isOrgBilling }
|
||||
)
|
||||
|
||||
if (isLoadingProjects && alt) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
@@ -100,13 +112,23 @@ const ProjectDropdown = () => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny" className="my-1">
|
||||
<span>{selectedProject?.name}</span>
|
||||
<Button
|
||||
type="text"
|
||||
iconRight={
|
||||
alt ? <IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} /> : null
|
||||
}
|
||||
>
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
{alt && isSuccess && !isOrgBilling && (
|
||||
<Badge color="slate" className="ml-2">
|
||||
{subscription?.plan.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span>{selectedProject?.name}</span>
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
|
||||
import { useTheme } from 'common'
|
||||
import { useParams } from 'common/hooks'
|
||||
@@ -27,15 +26,13 @@ import {
|
||||
} from './NavigationBar.utils'
|
||||
import NavigationIconButton from './NavigationIconButton'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const NavigationBar: FC<Props> = ({}) => {
|
||||
const NavigationBar = ({}) => {
|
||||
const router = useRouter()
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
const activeRoute = router.pathname.split('/')[3]
|
||||
const toolRoutes = generateToolRoutes(projectRef, project)
|
||||
@@ -46,22 +43,23 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
const { setIsOpen } = useCommandMenu()
|
||||
return (
|
||||
<div
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
className={[
|
||||
'flex w-14 flex-col justify-between overflow-y-hidden p-2',
|
||||
'border-r bg-body border-scale-500',
|
||||
].join(' ')}
|
||||
>
|
||||
<ul className="flex flex-col space-y-2">
|
||||
<Link href="/projects">
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
{!navLayoutV2 && (
|
||||
<Link href="/projects">
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<NavigationIconButton
|
||||
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
|
||||
route={{
|
||||
@@ -71,7 +69,7 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
link: `/project/${projectRef}`,
|
||||
}}
|
||||
/>
|
||||
<div className="bg-scale-500 h-px w-full"></div>
|
||||
<div className="bg-scale-500 h-px w-full" />
|
||||
{toolRoutes.map((route) => (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
@@ -97,77 +95,82 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="flex flex-col space-y-4 items-center">
|
||||
{IS_PLATFORM && showCmdkHelper && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="border-none"
|
||||
>
|
||||
<div className="py-1">
|
||||
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 flex items-center space-x-1',
|
||||
].join(' ')}
|
||||
{!navLayoutV2 && (
|
||||
<ul className="flex flex-col space-y-4 items-center">
|
||||
{IS_PLATFORM && showCmdkHelper && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="border-none"
|
||||
>
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
|
||||
) : (
|
||||
<p className="text-xs">CTRL</p>
|
||||
)}
|
||||
<p className="text-xs">K</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<Dropdown
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<Link href="/account/me">
|
||||
<Dropdown.Item key="header" icon={<IconSettings size={14} strokeWidth={1.5} />}>
|
||||
Account Preferences
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Separator />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Label>Theme</Dropdown.Label>
|
||||
<Dropdown.RadioGroup
|
||||
key="theme"
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onChange={(e: any) => toggleTheme(e === 'dark')}
|
||||
>
|
||||
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
|
||||
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
|
||||
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
|
||||
<Dropdown.Radio value="light">Light</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span className="py-1 h-10 border-none">
|
||||
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ul>
|
||||
<div className="py-1">
|
||||
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 flex items-center space-x-1',
|
||||
].join(' ')}
|
||||
>
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
|
||||
) : (
|
||||
<p className="text-xs">CTRL</p>
|
||||
)}
|
||||
<p className="text-xs">K</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<Dropdown
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<Link href="/account/me">
|
||||
<Dropdown.Item
|
||||
key="header"
|
||||
icon={<IconSettings size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Account Preferences
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Separator />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Label>Theme</Dropdown.Label>
|
||||
<Dropdown.RadioGroup
|
||||
key="theme"
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onChange={(e: any) => toggleTheme(e === 'dark')}
|
||||
>
|
||||
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
|
||||
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
|
||||
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
|
||||
<Dropdown.Radio value="light">Light</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span className="py-1 h-10 border-none">
|
||||
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { useFlag } from 'hooks'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
interface Props {
|
||||
interface ProductMenuBarProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const ProductMenuBar: FC<Props> = ({ title, children }) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarProps>) => {
|
||||
return (
|
||||
<div
|
||||
style={{ height: maxHeight, maxHeight }}
|
||||
className={[
|
||||
'hide-scrollbar flex w-64 flex-col border-r', // Layout
|
||||
'bg-scale-200', // Light mode
|
||||
@@ -25,7 +19,9 @@ const ProductMenuBar: FC<Props> = ({ title, children }) => {
|
||||
>
|
||||
<h4 className="text-lg">{title}</h4>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">{children}</div>
|
||||
<div className="flex-grow overflow-y-auto" style={{ maxHeight: 'calc(100vh - 96px)' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -59,61 +60,61 @@ const ProjectLayout = ({
|
||||
}: PropsWithChildren<ProjectLayoutProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const selectedProject = useSelectedProject()
|
||||
const projectName = selectedProject?.name
|
||||
const organizationName = selectedOrganization?.name
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
|
||||
const ignorePausedState =
|
||||
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
|
||||
const showPausedState = isPaused && !ignorePausedState
|
||||
|
||||
return (
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>
|
||||
{title
|
||||
? `${title} | Supabase`
|
||||
: selectedTable
|
||||
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
|
||||
: projectName
|
||||
? `${projectName} | ${organizationName} | Supabase`
|
||||
: organizationName
|
||||
? `${organizationName} | Supabase`
|
||||
: 'Supabase'}
|
||||
</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>
|
||||
{title
|
||||
? `${title} | Supabase`
|
||||
: selectedTable
|
||||
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
|
||||
: projectName
|
||||
? `${projectName} | ${organizationName} | Supabase`
|
||||
: organizationName
|
||||
? `${organizationName} | Supabase`
|
||||
: 'Supabase'}
|
||||
</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
</Head>
|
||||
<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"
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
{/* Product menu bar */}
|
||||
{!showPausedState && (
|
||||
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
</MenuBarWrapper>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
|
||||
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,44 +211,44 @@ export const ProjectLayoutNonBlocking = ({
|
||||
const selectedProject = useSelectedProject()
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
|
||||
const ignorePausedState =
|
||||
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
|
||||
const showPausedState = isPaused && !ignorePausedState
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
return (
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
|
||||
{/* Product menu bar */}
|
||||
{productMenu && !showPausedState && (
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
)}
|
||||
|
||||
<main
|
||||
className="flex w-full flex-1 flex-col overflow-x-hidden"
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
{/* Product menu bar */}
|
||||
{productMenu && !showPausedState && (
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
|
||||
<main className="flex w-full flex-1 flex-col overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { generateSettingsMenu } from './SettingsMenu.utils'
|
||||
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import ProjectLayout from '..'
|
||||
import { useParams } from 'common'
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
const project = useSelectedProject()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// billing pages live under /billing/invoices and /billing/subscription, etc
|
||||
// so we need to pass the [5]th part of the url to the menu
|
||||
const page = router.pathname.includes('billing')
|
||||
? router.pathname.split('/')[5]
|
||||
: router.pathname.split('/')[4]
|
||||
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.selectedProjectRef) {
|
||||
meta.extensions.load()
|
||||
}
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title || 'Settings'}
|
||||
product="Settings"
|
||||
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</ProjectLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(SettingsLayout))
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
const AccountSettingsMenu = () => {
|
||||
const router = useRouter()
|
||||
const accountSettings = [
|
||||
{ label: 'Preferences', pathname: `/account/me` },
|
||||
{ label: 'Access Tokens', pathname: `/account/tokens` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Account Settings</p>
|
||||
{accountSettings.map((link) => (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={link.pathname}
|
||||
isActive={link.pathname === router.pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSettingsMenu
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
const OrganizationSettingsMenu = () => {
|
||||
const router = useRouter()
|
||||
const organization = useSelectedOrganization()
|
||||
const organizationSettings = [
|
||||
{ label: 'General', pathname: `/org/[slug]/general` },
|
||||
{ label: 'Members', pathname: `/org/[slug]/team` },
|
||||
{ label: 'Integrations', pathname: `/org/[slug]/integrations` },
|
||||
{ label: 'Billing', pathname: `/org/[slug]/billing` },
|
||||
{ label: 'Usage', pathname: `/org/[slug]/usage` },
|
||||
{ label: 'Invoices', pathname: `/org/[slug]/invoices` },
|
||||
{ label: 'OAuth Apps', pathname: `/org/[slug]/apps` },
|
||||
{ label: 'Audit Logs', pathname: `/org/[slug]/audit` },
|
||||
]
|
||||
|
||||
// const { data: allProjects } = useProjectsQuery()
|
||||
// const projects = allProjects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Organization</p>
|
||||
{organizationSettings.map((link) => (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={link.pathname.replace('[slug]', organization?.slug ?? '')}
|
||||
isActive={link.pathname === router.pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* [Joshen] Hiding below until we figure out a better UX for consolidating settings on projects */}
|
||||
{/* <div className="space-y-2">
|
||||
<p className="text-sm font-medium">Projects</p>
|
||||
<Accordion_Shadcn_ type="single" collapsible className="w-full">
|
||||
{projects?.map((project) => (
|
||||
<ProjectSettingsMenuItem key={project.ref} project={project} />
|
||||
))}
|
||||
</Accordion_Shadcn_>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button type="default" size="small">
|
||||
Create new project
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationSettingsMenu
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
AccordionContent_Shadcn_,
|
||||
AccordionItem_Shadcn_,
|
||||
AccordionTrigger_Shadcn_,
|
||||
IconChevronRight,
|
||||
} from 'ui'
|
||||
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { Project } from 'types'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
interface ProjectSettingsMenuItemProps {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const ProjectSettingsMenuItem = ({ project }: ProjectSettingsMenuItemProps) => {
|
||||
const router = useRouter()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// [Joshen] Links need to be updated once we start implementing these
|
||||
const projectSettings = isOrgBilling
|
||||
? [
|
||||
{ label: 'General', pathname: `/project/[ref]/settings/general` },
|
||||
{ label: 'Infrastructure', pathname: `/project/[ref]/settings/infrastructure` },
|
||||
{ label: 'Add Ons', pathname: `/project/[ref]/settings/addons` },
|
||||
]
|
||||
: [
|
||||
{ label: 'General', pathname: `/project/[ref]/settings/general` },
|
||||
{ label: 'Subscription', pathname: `/project/[ref]/settings/billing/subscription` },
|
||||
{ label: 'Usage', pathname: `/project/[ref]/settings/billing/usage` },
|
||||
{ label: 'Invoices', pathname: `/project/[ref]/settings/billing/invoices` },
|
||||
]
|
||||
|
||||
return (
|
||||
<AccordionItem_Shadcn_ value={project.ref} className="!border-none">
|
||||
<AccordionTrigger_Shadcn_
|
||||
hideIcon
|
||||
className="py-1.5 hover:!no-underline [&[data-state=open]>div>svg]:!rotate-90 [&[data-state=open]>div>p]:text-scale-1200"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className="h-4 w-4 transition-transform duration-200"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<p className="text-scale-1100 text-sm font-normal transition duration-200">
|
||||
{project.name}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger_Shadcn_>
|
||||
<AccordionContent_Shadcn_>
|
||||
<div className="space-y-2 pl-6 mt-1">
|
||||
{projectSettings.map((link) => {
|
||||
const href = link.pathname.replace('[ref]', project.ref)
|
||||
return (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={href}
|
||||
isActive={href === router.asPath}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent_Shadcn_>
|
||||
</AccordionItem_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectSettingsMenuItem
|
||||
@@ -1,51 +1,30 @@
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { generateSettingsMenu } from './SettingsMenu.utils'
|
||||
|
||||
import { PropsWithChildren } from 'react'
|
||||
import AppLayout from '../AppLayout/AppLayout'
|
||||
import { useParams } from 'common'
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import ProjectLayout from '../'
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
const project = useSelectedProject()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
import OrganizationSettingsMenu from './OrganizationSettingsMenu'
|
||||
import AccountSettingsMenu from './AccountSettingsMenu'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const SettingsLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const router = useRouter()
|
||||
// billing pages live under /billing/invoices and /billing/subscription, etc
|
||||
// so we need to pass the [5]th part of the url to the menu
|
||||
const page = router.pathname.includes('billing')
|
||||
? router.pathname.split('/')[5]
|
||||
: router.pathname.split('/')[4]
|
||||
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.selectedProjectRef) {
|
||||
meta.extensions.load()
|
||||
}
|
||||
}, [ui.selectedProjectRef])
|
||||
const { ref, slug } = useParams()
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title || 'Settings'}
|
||||
product="Settings"
|
||||
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</ProjectLayout>
|
||||
<AppLayout>
|
||||
<div className="flex h-full">
|
||||
{router.pathname !== '/projects' && (
|
||||
<div className="h-full overflow-y-auto min-w-[280px] border-r px-8 py-8">
|
||||
{slug === undefined && ref === undefined ? (
|
||||
<AccountSettingsMenu />
|
||||
) : (
|
||||
<OrganizationSettingsMenu />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full overflow-y-auto flex-grow">{children}</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(SettingsLayout))
|
||||
export default SettingsLayout
|
||||
@@ -0,0 +1,27 @@
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface SettingsMenuItemProps {
|
||||
href: string
|
||||
label: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const SettingsMenuItem = ({ href, label, isActive }: SettingsMenuItemProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={href}>
|
||||
<a
|
||||
className={clsx(
|
||||
'text-sm',
|
||||
isActive ? 'text-scale-1200' : 'text-scale-1100 hover:text-scale-1200 transition'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsMenuItem
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
|
||||
import { noop, partition } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconCheck,
|
||||
@@ -83,7 +84,6 @@ const TableEditorMenu = ({
|
||||
)
|
||||
|
||||
const schemas: PostgresSchema[] = meta.schemas.list()
|
||||
|
||||
const schema = schemas.find((schema) => schema.name === selectedSchema)
|
||||
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
@@ -110,6 +110,13 @@ const TableEditorMenu = ({
|
||||
<IconLoader className="animate-spin" size={12} />
|
||||
<span className="text-xs text-scale-900">Loading schemas...</span>
|
||||
</div>
|
||||
) : meta.schemas.hasError ? (
|
||||
<Alert variant="warning" title="Failed to load schemas" className="!px-3 !py-3">
|
||||
<p className="mb-2">Error: {meta.schemas?.error?.message}</p>
|
||||
<Button type="default" size="tiny" onClick={() => meta.schemas.load()}>
|
||||
Reload schemas
|
||||
</Button>
|
||||
</Alert>
|
||||
) : (
|
||||
<Listbox
|
||||
size="tiny"
|
||||
|
||||
@@ -9,7 +9,7 @@ import TableEditorLayout from './TableEditorLayout/TableEditorLayout'
|
||||
import SQLEditorLayout from './SQLEditorLayout/SQLEditorLayout'
|
||||
import DatabaseLayout from './DatabaseLayout/DatabaseLayout'
|
||||
import DocsLayout from './DocsLayout/DocsLayout'
|
||||
import SettingsLayout from './SettingsLayout/SettingsLayout'
|
||||
import SettingsLayout from './ProjectSettingsLayout/SettingsLayout'
|
||||
import StorageLayout from './StorageLayout/StorageLayout'
|
||||
import AccountLayout from './AccountLayout/AccountLayout'
|
||||
import { AccountLayoutWithoutAuth } from './AccountLayout/AccountLayout'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
/**
|
||||
* Render keyMap
|
||||
*
|
||||
@@ -13,7 +11,7 @@ const customKeyChars: any = {
|
||||
enter: '↵',
|
||||
}
|
||||
|
||||
const KeyMap: FC<any> = ({ keyMap }) => {
|
||||
const KeyMap = ({ keyMap }: any) => {
|
||||
if (!keyMap) return null
|
||||
|
||||
const keys = keyMap.split('+')
|
||||
|
||||
@@ -39,7 +39,7 @@ const ProductEmptyState: FC<Props> = ({
|
||||
<div className="flex items-center space-x-2">
|
||||
{ctaButtonLabel && onClickCta && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button type="primary" onClick={onClickCta} disabled={disabled}>
|
||||
{ctaButtonLabel}
|
||||
</Button>
|
||||
|
||||
@@ -14,7 +14,6 @@ export async function getOrganizations(signal?: AbortSignal): Promise<Organizati
|
||||
}
|
||||
|
||||
const sorted = (data as Organization[]).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,25 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useSelectedProject } from './useSelectedProject'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
|
||||
export function useSelectedOrganization({ enabled = true } = {}) {
|
||||
const { slug } = useParams()
|
||||
const { data } = useOrganizationsQuery({ enabled })
|
||||
const selectedProject = useSelectedProject()
|
||||
|
||||
const localStorageSlug = useMemo(() => {
|
||||
return typeof window !== 'undefined'
|
||||
? localStorage.getItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION)
|
||||
: null
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
return data?.find(
|
||||
(org) => org.slug === slug || (selectedProject && org.id === selectedProject.organization_id)
|
||||
)
|
||||
}, [data, selectedProject, slug])
|
||||
return data?.find((org) => {
|
||||
if (slug !== undefined) return org.slug === slug
|
||||
if (selectedProject !== undefined) return org.id === selectedProject.organization_id
|
||||
if (localStorageSlug !== undefined) return org.slug === localStorageSlug
|
||||
return undefined
|
||||
})
|
||||
}, [data, selectedProject, slug, localStorageSlug])
|
||||
}
|
||||
@@ -4,6 +4,5 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||
export function useSelectedProject() {
|
||||
const { ref } = useParams()
|
||||
const { data } = useProjectDetailQuery({ ref })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { auth } from 'lib/gotrue'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { SupaResponse } from 'types/base'
|
||||
import { Session } from '@supabase/gotrue-js'
|
||||
|
||||
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
|
||||
const message = e?.message ? `An error has occurred: ${e.message}` : 'An error has occurred'
|
||||
@@ -86,15 +87,32 @@ export async function handleResponseError<T = unknown>(
|
||||
}
|
||||
}
|
||||
|
||||
let currentSession: Session | null = null
|
||||
|
||||
auth.onAuthStateChange((event, session) => {
|
||||
currentSession = session
|
||||
})
|
||||
|
||||
export async function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await auth.getSession()
|
||||
const aboutToExpire = currentSession?.expires_at
|
||||
? currentSession.expires_at - Math.ceil(Date.now() / 1000) < 60
|
||||
: false
|
||||
|
||||
return session?.access_token
|
||||
if (!currentSession || aboutToExpire) {
|
||||
const {
|
||||
data: { session },
|
||||
} = await auth.getSession()
|
||||
|
||||
currentSession = session
|
||||
}
|
||||
|
||||
// using memory version as gotrue-js can be a bit slow when using
|
||||
// #getSession() as it has to read directly from local storage
|
||||
|
||||
return currentSession?.access_token
|
||||
}
|
||||
|
||||
export async function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
|
||||
|
||||
@@ -85,3 +85,7 @@ export const STRIPE_PUBLIC_KEY =
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || 'pk_test_XVwg5IZH3I9Gti98hZw6KRzd00v5858heG'
|
||||
|
||||
export const USAGE_APPROACHING_THRESHOLD = 0.75
|
||||
|
||||
export const LOCAL_STORAGE_KEYS = {
|
||||
RECENTLY_VISITED_ORGANIZATION: 'supabase-organization',
|
||||
}
|
||||
@@ -62,6 +62,11 @@ dayjs.extend(relativeTime)
|
||||
|
||||
dart(Prism)
|
||||
|
||||
// [Joshen TODO] Once we settle on the new nav layout - we'll need a lot of clean up in terms of our layout components
|
||||
// a lot of them are unnecessary and introduce way too many cluttered CSS especially with the height styles that make
|
||||
// debugging way too difficult. Ideal scenario is we just have one AppLayout to control the height and scroll areas of
|
||||
// the dashboard, all other layout components should not be doing that
|
||||
|
||||
function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const queryClient = useRootQueryClient()
|
||||
const [rootStore] = useState(() => new RootStore())
|
||||
|
||||
@@ -64,6 +64,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
EXTERNAL_FACEBOOK_ENABLED: false,
|
||||
EXTERNAL_FACEBOOK_CLIENT_ID: null,
|
||||
EXTERNAL_FACEBOOK_SECRET: null,
|
||||
EXTERNAL_FIGMA_ENABLED: false,
|
||||
EXTERNAL_FIGMA_CLIENT_ID: null,
|
||||
EXTERNAL_FIGMA_SECRET: null,
|
||||
EXTERNAL_GITHUB_ENABLED: false,
|
||||
EXTERNAL_GITHUB_CLIENT_ID: null,
|
||||
EXTERNAL_GITHUB_SECRET: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeneralSettings } from 'components/interfaces/Organization'
|
||||
import { GeneralSettings as GeneralSettingsLegacy } from 'components/interfaces/Organization'
|
||||
import { OrganizationLayout } from 'components/layouts'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import { usePermissionsQuery } from 'data/permissions/permissions-query'
|
||||
@@ -14,7 +14,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => {
|
||||
{selectedOrganization === undefined && isLoadingPermissions ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<GeneralSettings />
|
||||
<GeneralSettingsLegacy />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Link from 'next/link'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
|
||||
import ProjectCard from 'components/interfaces/Home/ProjectList/ProjectCard'
|
||||
import ShimmeringCard from 'components/interfaces/Home/ProjectList/ShimmeringCard'
|
||||
import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => {
|
||||
const {
|
||||
data: allProjects,
|
||||
error: projectsError,
|
||||
isLoading: isLoadingProjects,
|
||||
isError: isErrorProjects,
|
||||
isSuccess: isSuccessProjects,
|
||||
} = useProjectsQuery()
|
||||
|
||||
const organization = useSelectedOrganization()
|
||||
const projects = allProjects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12 space-y-8">
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button size="medium" type="default" iconRight={<IconPlus />}>
|
||||
New project
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg">Projects</h4>
|
||||
{isLoadingProjects && (
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ShimmeringCard />
|
||||
<ShimmeringCard />
|
||||
</ul>
|
||||
)}
|
||||
{isErrorProjects && (
|
||||
<AlertError error={projectsError} subject="Failed to retrieve projects" />
|
||||
)}
|
||||
{isSuccessProjects && (
|
||||
<>
|
||||
{(projects?.length ?? 0) === 0 ? (
|
||||
<div className="col-span-4 space-y-4 rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
|
||||
<div className="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button icon={<IconPlus />}>New Project</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{projects?.map((project) => (
|
||||
<ProjectCard key={project.ref} project={project} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectsPage.getLayout = (page) => <AppLayout>{page}</AppLayout>
|
||||
|
||||
export default ProjectsPage
|
||||
@@ -5,11 +5,15 @@ import AlertError from 'components/ui/AlertError'
|
||||
import Connecting from 'components/ui/Loading/Loading'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useFlag } from 'hooks'
|
||||
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
data: organizations,
|
||||
isLoading: isOrganizationLoading,
|
||||
@@ -19,24 +23,40 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
useAutoProjectsPrefetch()
|
||||
|
||||
const { isLoading: isProfileLoading } = useProfile()
|
||||
|
||||
const isLoading = isOrganizationLoading || isProfileLoading
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const hasWindowLoaded = typeof window !== 'undefined'
|
||||
|
||||
useEffect(() => {
|
||||
if (navLayoutV2 && isSuccess && hasWindowLoaded) {
|
||||
const localStorageSlug = localStorage.getItem(
|
||||
LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION
|
||||
)
|
||||
const verifiedSlug = organizations.some((org) => org.slug === localStorageSlug)
|
||||
|
||||
if (organizations.length === 0) router.push('/new')
|
||||
else if (localStorageSlug && verifiedSlug) router.push(`/org/${localStorageSlug}`)
|
||||
else router.push(`/org/${organizations[0].slug}`)
|
||||
}
|
||||
}, [navLayoutV2, isSuccess, hasWindowLoaded])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center space-x-2">
|
||||
{navLayoutV2 && isLoading && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<Connecting />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="py-4 px-5">
|
||||
<div
|
||||
className={`py-4 px-5 ${navLayoutV2 ? 'h-full flex items-center justify-center' : ''}`}
|
||||
>
|
||||
<AlertError subject="Failed to retrieve organizations" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
{!navLayoutV2 && isSuccess && (
|
||||
<div className="py-4 px-5">
|
||||
{IS_PLATFORM && (
|
||||
<div className="my-2">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="300" height="300" viewBox="-50 0 300 300" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#0acf83}.st1{fill:#a259ff}.st2{fill:#f24e1e}.st3{fill:#ff7262}.st4{fill:#1abcfe}</style><path id="path0_fill" class="st0" d="M50 300c27.6 0 50-22.4 50-50v-50H50c-27.6 0-50 22.4-50 50s22.4 50 50 50z"/><path id="path1_fill" class="st1" d="M0 150c0-27.6 22.4-50 50-50h50v100H50c-27.6 0-50-22.4-50-50z"/><path id="path1_fill_1_" class="st2" d="M0 50C0 22.4 22.4 0 50 0h50v100H50C22.4 100 0 77.6 0 50z"/><path id="path2_fill" class="st3" d="M100 0h50c27.6 0 50 22.4 50 50s-22.4 50-50 50h-50V0z"/><path id="path3_fill" class="st4" d="M200 150c0 27.6-22.4 50-50 50s-50-22.4-50-50 22.4-50 50-50 50 22.4 50 50z"/></svg>
|
||||
|
After Width: | Height: | Size: 717 B |
@@ -666,6 +666,44 @@ const EXTERNAL_PROVIDER_FACEBOOK = {
|
||||
},
|
||||
}
|
||||
|
||||
const EXTERNAL_PROVIDER_FIGMA = {
|
||||
$schema: JSON_SCHEMA_VERSION,
|
||||
type: 'object',
|
||||
title: 'Figma',
|
||||
properties: {
|
||||
EXTERNAL_FIGMA_ENABLED: {
|
||||
title: 'Figma enabled',
|
||||
type: 'boolean',
|
||||
},
|
||||
EXTERNAL_FIGMA_CLIENT_ID: {
|
||||
title: 'Client ID',
|
||||
type: 'string',
|
||||
},
|
||||
EXTERNAL_FIGMA_SECRET: {
|
||||
title: 'Client Secret',
|
||||
type: 'string',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
validationSchema: object().shape({
|
||||
EXTERNAL_FIGMA_ENABLED: boolean().required(),
|
||||
EXTERNAL_FIGMA_CLIENT_ID: string().when('EXTERNAL_FIGMA_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema.required('Client ID is required'),
|
||||
otherwise: (schema) => schema,
|
||||
}),
|
||||
EXTERNAL_FIGMA_SECRET: string().when('EXTERNAL_FIGMA_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema.required('Client Secret is required'),
|
||||
otherwise: (schema) => schema,
|
||||
}),
|
||||
}),
|
||||
misc: {
|
||||
iconKey: 'figma-icon',
|
||||
requiresRedirect: true,
|
||||
},
|
||||
}
|
||||
|
||||
const EXTERNAL_PROVIDER_GITHUB = {
|
||||
$schema: JSON_SCHEMA_VERSION,
|
||||
type: 'object',
|
||||
@@ -1283,6 +1321,7 @@ export const PROVIDERS_SCHEMAS = [
|
||||
EXTERNAL_PROVIDER_BITBUCKET,
|
||||
EXTERNAL_PROVIDER_DISCORD,
|
||||
EXTERNAL_PROVIDER_FACEBOOK,
|
||||
EXTERNAL_PROVIDER_FIGMA,
|
||||
EXTERNAL_PROVIDER_GITHUB,
|
||||
EXTERNAL_PROVIDER_GITLAB,
|
||||
EXTERNAL_PROVIDER_GOOGLE,
|
||||
|
||||
@@ -157,6 +157,18 @@
|
||||
"title": "Facebook secret",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_FIGMA_ENABLED": {
|
||||
"title": "Figma enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EXTERNAL_FIGMA_CLIENT_ID": {
|
||||
"title": "Figma client ID",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_FIGMA_SECRET": {
|
||||
"title": "Figma secret",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_GITHUB_ENABLED": {
|
||||
"title": "GitHub enabled",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -59,8 +59,8 @@ double_confirm_changes = true
|
||||
enable_confirmations = false
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
# `discord`, `facebook`, `figma`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`,
|
||||
# `twitch`, `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
|
||||