diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml index 5831c7d23df..03de1b2e7aa 100644 --- a/.github/workflows/search.yml +++ b/.github/workflows/search.yml @@ -17,6 +17,10 @@ on: schedule: - cron: '0 0 * * *' +concurrency: + group: ${{ github.workflow }}-prod + cancel-in-progress: false + permissions: contents: read diff --git a/apps/docs/app/api/crawlers/route.smoke.test.ts b/apps/docs/app/api/crawlers/route.smoke.test.ts index e2224180cec..649a26e93c3 100644 --- a/apps/docs/app/api/crawlers/route.smoke.test.ts +++ b/apps/docs/app/api/crawlers/route.smoke.test.ts @@ -70,3 +70,10 @@ describe('prod smoke test: crawler pages return correct data', () => { expect(headings.includes('examples')).toBe(true) }) }) + +describe('prod smoke test: ref pages without crawler versions', async () => { + it('returns 200', async () => { + const result = await fetch(REFERENCE_DOCS_URL + '/api/v1-deploy-a-function') + expect(result.status).toBe(200) + }) +}) diff --git a/apps/docs/content/_partials/database_setup.mdx b/apps/docs/content/_partials/database_setup.mdx index 6afac0bb025..e1602f62d53 100644 --- a/apps/docs/content/_partials/database_setup.mdx +++ b/apps/docs/content/_partials/database_setup.mdx @@ -9,7 +9,8 @@ Your database will be available in less than a minute. **Finding your credentials:** -You can find your project credentials inside the project [settings](https://supabase.com/dashboard/project/_/settings/), including: +You can find your project credentials on the dashboard: -- [Database credentials](https://supabase.com/dashboard/project/_/settings/database): connection strings and connection pooler details. -- [API credentials](https://supabase.com/dashboard/project/_/settings/database): your serverless API URL and `anon` / `service_role` keys. +- [Database connection strings](https://supabase.com/dashboard/project/_/settings/api?showConnect=true): Direct and Pooler connection details including the connection string and parameters. +- [Database password](https://supabase.com/dashboard/project/_/settings/database): Reset database password here if you do not have it. +- [API credentials](https://supabase.com/dashboard/project/_/settings/api): your serverless API URL and `anon` / `service_role` keys. diff --git a/apps/docs/content/_partials/migration_warnings.mdx b/apps/docs/content/_partials/migration_warnings.mdx index 5b4925104b5..a2a23f1148a 100644 --- a/apps/docs/content/_partials/migration_warnings.mdx +++ b/apps/docs/content/_partials/migration_warnings.mdx @@ -2,7 +2,7 @@ - If you're planning to migrate a database larger than 6 GB, we recommend [upgrading to at least a Large compute add-on](/docs/guides/platform/compute-add-ons). This will ensure you have the necessary resources to handle the migration efficiently. -- For databases smaller than 150 GB, you can increase the size of the disk on paid projects by navigating to [Database Settings](/dashboard/project/_/settings/database). +- For databases smaller than 150 GB, you can increase the size of the disk on paid projects by navigating to the [Compute and Disk Settings](dashboard/project/_/settings/compute-and-disk) page. - If you're dealing with a database larger than 150 GB, we strongly advise you to [contact our support team](/dashboard/support/new) for assistance in provisioning the required resources and ensuring a smooth migration process. diff --git a/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx index debd28c5710..134831fd32f 100644 --- a/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx +++ b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx @@ -34,7 +34,7 @@ If you haven't already, head over to [database.new](https://database.new) and cr When creating your project, make sure to note down your database password as you will need it to construct the `DB_URL` in the next step. -You can find the database connection string in your Supabase Dashboard [database settings](https://supabase.com/dashboard/project/_/settings/database). Select "Use connection pooling" with `Mode: Session` for a direct connection to your Postgres database. It will look something like this: +You can find your database connection string on your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true). Use the Session pooler connection string which looks like this: ```txt postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres diff --git a/apps/docs/content/guides/ai/google-colab.mdx b/apps/docs/content/guides/ai/google-colab.mdx index ddd4c76177f..c95116980ec 100644 --- a/apps/docs/content/guides/ai/google-colab.mdx +++ b/apps/docs/content/guides/ai/google-colab.mdx @@ -37,7 +37,7 @@ pip install vecs ## Connect to your database -Find the Postgres pooler connection string for your Supabase project in the [database settings](https://supabase.com/dashboard/project/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres` +On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true). The connection string should look like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres` Create a new code block below the install block (`ctrl+m b`) and add the following code using the Postgres URI you copied above: diff --git a/apps/docs/content/guides/ai/integrations/llamaindex.mdx b/apps/docs/content/guides/ai/integrations/llamaindex.mdx index 7d61103c594..6fd5427d10a 100644 --- a/apps/docs/content/guides/ai/integrations/llamaindex.mdx +++ b/apps/docs/content/guides/ai/integrations/llamaindex.mdx @@ -42,7 +42,7 @@ DB_CONNECTION = "postgresql://:@:/" vx = vecs.create_client(DB_CONNECTION) ``` -Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project. +Replace the `DB_CONNECTION` with your own connection string. You can find the connection string on your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). diff --git a/apps/docs/content/guides/ai/quickstarts/face-similarity.mdx b/apps/docs/content/guides/ai/quickstarts/face-similarity.mdx index aae47c4f2b4..1f4894d5867 100644 --- a/apps/docs/content/guides/ai/quickstarts/face-similarity.mdx +++ b/apps/docs/content/guides/ai/quickstarts/face-similarity.mdx @@ -40,7 +40,7 @@ DB_CONNECTION = "postgresql://:@:/" vx = vecs.create_client(DB_CONNECTION) ``` -Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project. +Replace the `DB_CONNECTION` with your own connection string. You can find the connection string on your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). diff --git a/apps/docs/content/guides/ai/quickstarts/hello-world.mdx b/apps/docs/content/guides/ai/quickstarts/hello-world.mdx index 5fa051216b5..cbea52b2c55 100644 --- a/apps/docs/content/guides/ai/quickstarts/hello-world.mdx +++ b/apps/docs/content/guides/ai/quickstarts/hello-world.mdx @@ -41,7 +41,7 @@ DB_CONNECTION = "postgresql://:@:/" vx = vecs.create_client(DB_CONNECTION) ``` -Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project. +Replace the `DB_CONNECTION` with your Session pooler connection string. You can find the connection string on your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). @@ -51,7 +51,7 @@ SQLAlchemy requires the connection string to start with `postgresql://` (instead -You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6. +You must use the Session pooler connection string with Google Colab since Colab does not support IPv6. diff --git a/apps/docs/content/guides/ai/quickstarts/text-deduplication.mdx b/apps/docs/content/guides/ai/quickstarts/text-deduplication.mdx index 2661d9763d2..34c183f08b0 100644 --- a/apps/docs/content/guides/ai/quickstarts/text-deduplication.mdx +++ b/apps/docs/content/guides/ai/quickstarts/text-deduplication.mdx @@ -41,7 +41,7 @@ DB_CONNECTION = "postgresql://:@:/" vx = vecs.create_client(DB_CONNECTION) ``` -Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project. +Replace the `DB_CONNECTION` with your own connection string. You can find the connection string on your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index 5ac29b3ff11..b5c17ba322e 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -418,7 +418,7 @@ const OneTapComponent = () => { } }, nonce: hashedNonce, - // with chrome's removal of third-party cookiesm, we need to use FedCM instead (https://developers.google.com/identity/gsi/web/guides/fedcm-migration) + // with chrome's removal of third-party cookies, we need to use FedCM instead (https://developers.google.com/identity/gsi/web/guides/fedcm-migration) use_fedcm_for_prompt: true, }) google.accounts.id.prompt() // Display the One Tap UI diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index 3816641f933..f68bfb8ccc5 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -77,11 +77,7 @@ The connection string looks like this: postgresql://postgres:[YOUR-PASSWORD]@db.apbkobhfnmcqqzqeeqss.supabase.co:5432/postgres ``` -Get your project's direct string from the [Database Settings](/dashboard/project/_/settings/database) page: - -1. Go to the `Settings` section. -2. Click `Database`. -3. Under `Connection string`, make sure `Display connection pooler` is unchecked. Copy the URI. +Get your project's direct connection string from your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). ## Shared pooler @@ -97,11 +93,7 @@ The connection string looks like this: postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres ``` -Get your project's session mode string from the [Database Settings](/dashboard/project/_/settings/database) page: - -1. Go to the `Settings` section. -2. Click `Database`. -3. Under `Connection string`, make sure `Display connection pooler` is checked and `Session mode` is selected. Copy the URI. +Get your project's Session pooler connection string from your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). ### Supavisor transaction mode @@ -119,11 +111,7 @@ The connection string looks like this: postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres ``` -Get your project's transaction mode string from the [Database Settings](/dashboard/project/_/settings/database) page: - -1. Go to the `Settings` section. -2. Click `Database`. -3. Under `Connection string`, make sure `Display connection pooler` is checked and `Transaction mode` is selected. Copy the URI. +Get your project's Transaction pooler connection string from your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). ## Dedicated pooler diff --git a/apps/docs/content/guides/database/dbeaver.mdx b/apps/docs/content/guides/database/dbeaver.mdx index caec18cc51b..09c3ca2b925 100644 --- a/apps/docs/content/guides/database/dbeaver.mdx +++ b/apps/docs/content/guides/database/dbeaver.mdx @@ -32,7 +32,7 @@ If you do not have DBeaver, you can download it from its [website](https://dbeav - Inside the Dashboard's [Database Settings](https://supabase.com/dashboard/project/_/settings/database), note your session mode's: + On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true), note your session pooler's: - host - username diff --git a/apps/docs/content/guides/database/metabase.mdx b/apps/docs/content/guides/database/metabase.mdx index 674a4cb03c2..f4048fc3be5 100644 --- a/apps/docs/content/guides/database/metabase.mdx +++ b/apps/docs/content/guides/database/metabase.mdx @@ -40,7 +40,7 @@ hideToc: true Connect your Postgres server to Metabase. - - On your project dashboard click on [Connect](https://supabase.com/dashboard/project/_?showConnect=true) + - On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) - View parameters under "Session pooler" diff --git a/apps/docs/content/guides/database/postgres/roles.mdx b/apps/docs/content/guides/database/postgres/roles.mdx index e28343cf3d0..490bc3fa227 100644 --- a/apps/docs/content/guides/database/postgres/roles.mdx +++ b/apps/docs/content/guides/database/postgres/roles.mdx @@ -42,7 +42,7 @@ If you use special symbols in your Postgres password, you must remember to [perc ### Changing your project password -When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to third-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`. +When you created your project you were also asked to enter a password. This is the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to third-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`. Changing the password does not result in any downtime. All connected services, such as PostgREST, PgBouncer, and other Supabase managed services, are automatically updated to use the latest password to ensure availability. However, if you have any external services connecting to the Supabase database using hardcoded username/password credentials, a manual update will be required. diff --git a/apps/docs/content/guides/database/prisma.mdx b/apps/docs/content/guides/database/prisma.mdx index dbb1606d7d3..300afb10b84 100644 --- a/apps/docs/content/guides/database/prisma.mdx +++ b/apps/docs/content/guides/database/prisma.mdx @@ -122,8 +122,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t - - Visit the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) - - Find your Supavisor Session Mode string. It should end with 5432. It will be used in your `.env` file. + - On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) + - Find your Supavisor Session pooler string. It should end with 5432. It will be used in your `.env` file. If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. diff --git a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx index 5bc0f9d243b..da14e59ce2e 100644 --- a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx +++ b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx @@ -56,7 +56,7 @@ Prisma couldn't establish a connection with Postgres or Supavisor before the tim ### Solutions: [#solution-cant-reach-database-server-at] - **Check database health**: Use the [Reports Dashboard](https://supabase.com/dashboard/project/_/reports/database) to monitor CPU, memory, and I/O usage. If the database is overloaded, consider increasing your [compute size](https://supabase.com/docs/guides/platform/compute-add-ons) or [optimizing your queries](https://supabase.com/docs/guides/database/query-optimization). -- **Verify connection string**: Double-check the connection string in your Prisma configuration to ensure it matches one in your [Database Settings](https://supabase.com/dashboard/project/_/settings/database). +- **Verify connection string**: Double-check the connection string in your Prisma configuration to ensure it matches in your [project connect page](https://supabase.com/dashboard/project/_?showConnect=true). - **Increase connection timeout**: Try increasing the `connect_timeout` parameter in your Prisma configuration to give it more time to establish a connection. ```md diff --git a/apps/docs/content/guides/database/vault.mdx b/apps/docs/content/guides/database/vault.mdx index 12aa4cc193b..dae16254c24 100644 --- a/apps/docs/content/guides/database/vault.mdx +++ b/apps/docs/content/guides/database/vault.mdx @@ -9,9 +9,9 @@ video: 'https://www.youtube.com/v/J9mTPY8rIXE' Vault is a Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets and other data in your database. This opens up a lot of possibilities to use Postgres in ways that go beyond what is available in a stock distribution. -Under the hood, the Vault is a table of Secrets and Encryption Keys that are stored using [Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) on disk. They are then available in decrypted form through a Postgres view so that the secrets can be used by applications from SQL. Because the secrets are stored on disk encrypted and authenticated, any backups or replication streams also preserve this encryption in a way that can't be decrypted or forged. +Under the hood, the Vault is a table of Secrets that are stored using [Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) on disk. They are then available in decrypted form through a Postgres view so that the secrets can be used by applications from SQL. Because the secrets are stored on disk encrypted and authenticated, any backups or replication streams also preserve this encryption in a way that can't be decrypted or forged. -Supabase provides a dashboard UI for the Vault that makes storing secrets easy. Click a button, type in your secret, and save. Optionally create your own keys you can use to encrypt your secret. Your secret will then be stored on disk encrypted using the specified key. +Supabase provides a dashboard UI for the Vault that makes storing secrets easy. Click a button, type in your secret, and save. -There are two main parts to the Vault UI, Secrets and Encryption Keys: +You can use Vault to store secrets - everything from Environment Variables to API Keys. You can then use these secrets anywhere in your database: Postgres [Functions](/docs/guides/database/functions), Triggers, and [Webhooks](/docs/guides/database/webhooks). From a SQL perspective, accessing secrets is as easy as querying a table (or in this case, a view). The underlying secrets tables will be stored in encrypted form. -## Secrets +## Using Vault -You can use the Vault to store secrets - everything from Environment Variables to API Keys. You can then use these secrets anywhere in your database: Postgres [Functions](/docs/guides/database/functions), Triggers, and [Webhooks](/docs/guides/database/webhooks). From a SQL perspective, accessing secrets is as easy as querying a table (or in this case, a view). The underlying secrets tables will be stored in encrypted form. - -## Encryption keys - -These are keys used to encrypt data inside your database. You can create different Encryption Keys for different purposes, for example: one for encrypting user-data, and another for application-data. Each key is encrypted itself using a root encryption key that lives outside of the database. See **[Encryption key location](#encryption-key-location)** for more details. - -## Using the Vault - -You can manage secrets and encryption keys from the UI or using SQL. +You can manage secrets from the UI or using SQL. ### Adding secrets @@ -71,7 +63,7 @@ id | 7095d222-efe5-4cd5-b5c6-5755b451e223 name | unique_name description | This is the description secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= -key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +key_id | nonce | \x9f2d60954ba5eb566445736e0760b0e3 created_at | 2022-12-14 02:34:23.85159+00 updated_at | 2022-12-14 02:34:23.85159+00 @@ -79,31 +71,6 @@ updated_at | 2022-12-14 02:34:23.85159+00 -Alternatively, you can create a secret by inserting data into the `vault.secret` table: - -{/* prettier-ignore */} -```sql -insert into vault.secrets (secret) -values ('s3kre3t_k3y') returning *; -``` - -
-Show Result - -```sql --[ RECORD 1 ]------------------------------------------------------------- -id | d91596b8-1047-446c-b9c0-66d98af6d001 -name | -description | -secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== -key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 -nonce | \x3aa2e92f9808e496aa4163a59304b895 -created_at | 2022-12-14 02:29:21.3625+00 -updated_at | 2022-12-14 02:29:21.3625+00 -``` - -
- ### Viewing secrets If you look in the `vault.secrets` table, you will see that your data is stored encrypted. To decrypt the data, there is an automatically created view `vault.decrypted_secrets`. This view will decrypt secret data on the fly: @@ -126,7 +93,7 @@ name | unique_name description | This is the description secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= decrypted_secret | another_s3kre3t -key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +key_id | nonce | \x9f2d60954ba5eb566445736e0760b0e3 created_at | 2022-12-14 02:34:23.85159+00 updated_at | 2022-12-14 02:34:23.85159+00 @@ -136,7 +103,7 @@ name | description | secret | a1CE4vXwQ53+N9bllJj1D7fasm59ykohjb7K90PPsRFUd9IbBdxIGZNoSQLIXl4= decrypted_secret | another_s3kre3t -key_id | 8c72b05e-b931-4372-abf9-a09cfad18489 +key_id | nonce | \x1d3b2761548c4efb2d29ca11d44aa22f created_at | 2022-12-14 02:32:50.58921+00 updated_at | 2022-12-14 02:32:50.58921+00 @@ -146,7 +113,7 @@ name | description | secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== decrypted_secret | s3kre3t_k3y -key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 +key_id | nonce | \x3aa2e92f9808e496aa4163a59304b895 created_at | 2022-12-14 02:29:21.3625+00 updated_at | 2022-12-14 02:29:21.3625+00 @@ -186,7 +153,7 @@ name | updated_unique_name description | This is the updated description secret | lhb3HBFxF+qJzp/HHCwhjl4QFb5dYDsIQEm35DaZQOovdkgp2iy6UMufTKJGH4ThMrU= decrypted_secret | n3w_upd@ted_s3kret -key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +key_id | nonce | \x9f2d60954ba5eb566445736e0760b0e3 created_at | 2022-12-14 02:34:23.85159+00 updated_at | 2022-12-14 02:51:13.938396+00 @@ -206,7 +173,7 @@ updated_at | 2022-12-14 02:51:13.938396+00 > -As we mentioned, the Vault uses `pgsodium`'s Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details. +As we mentioned, Vault uses Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about. What does authenticated mean? Where is the encryption key stored? This section explains those details. ### Authenticated encryption with associated data @@ -218,34 +185,11 @@ The first important feature of TCE is that it uses an [Authenticated Encryption **Associated Data** means that you can include any other columns from the same row as part of the signature computation. This doesn't encrypt those other columns - rather it ensures that your encrypted value is only associated with columns from that row. If an attacker were to copy an encrypted value from another row to the current one, the signature would be rejected (assuming you used a unique column in the associated data). -Another important feature of `pgsodium` is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_. +Another important feature is that the encryption key is never stored in the database alongside the encrypted data. Even if an attacker can capture a dump of your entire database, they will see only encrypted data, _never the encryption key itself_. This is an important safety precaution - there is little value in storing the encryption key in the database itself as this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue. -Where are the keys stored? Supabase creates and manages the root keys (from which all key IDs are derived) in our secured backend systems. We keep this root key safe and separate from your data. You remain in control of your keys - a separate API endpoint is available that you can use to access the key if you want to decrypt your data outside of Supabase. - -### Internal details - -To encrypt data, you need a _key id_. You can use the default key id created automatically for every project, or create your own key ids Using the `pgsodium.create_key()` function. Key ids are used to internally derive the encryption key used to encrypt secrets in the vault. Vault users typically do not have access to the key itself, only the key id. - -Both `vault.create_secret()` and `vault.update_secret()` take an optional fourth `new_key_id` argument. This argument can be used to store a different key id for the secret instead of the default value. - -{/* prettier-ignore */} -```sql -select vault.create_secret( - 'another_s3kre3t_key', - 'another_unique_name', - 'This is another description', - (pgsodium.create_key()).id -); -``` - -Result: - -```sh --[ RECORD 1 ]-+------------------------------------- -create_secret | cec9e005-a44d-4b19-86e1-febf3cd40619 -``` +Where is the key stored? Supabase creates and manages the encryption key in our secured backend systems. We keep this key safe and separate from your data. You remain in control of your key - a separate API endpoint is available that you can use to access the key if you want to decrypt your data outside of Supabase. Which roles should have access to the `vault.secrets` table should be carefully considered. There are two ways to grant access, the first is that the `postgres` user can explicitly grant access to the vault table itself. diff --git a/apps/docs/content/guides/deployment/branching.mdx b/apps/docs/content/guides/deployment/branching.mdx index a31289fed53..1f4e2a71cb1 100644 --- a/apps/docs/content/guides/deployment/branching.mdx +++ b/apps/docs/content/guides/deployment/branching.mdx @@ -106,14 +106,17 @@ You can use the [Supabase CLI](/docs/guides/cli) to manage changes inside a loca - Pull your database changes using `supabase db pull`. You can find your database URL in your [database settings](https://supabase.com/dashboard/project/_/settings/database), under the URI tab of the Connection String settings panel. Make sure **Use connection pooling** is checked so you can use the IPv4-enabled connection pooler. (Without connection pooling, your database is only accessible over IPv6, which isn't yet supported by all network providers.) + Pull your database changes using `supabase db pull`. To get your database connection string, go to your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) and look for the Session pooler connection string. ```markdown - supabase db pull --db-url + supabase db pull --db-url - # Your Database URL looks something like: + # Your Database connection string will look like this: # postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres ``` + + If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. + @@ -458,8 +461,10 @@ You can disable branching at any time. Navigate to the [Branches](/dashboard/pro ### Persistent branches -Persistent branches are the type of branches that will remain active even after the underlying PR is closed. +Persistent branches are the type of branches that will remain active even after the underlying PR is closed. Any PR based on a persistent branch will also have a corresponding preview branch created automatically. + You can change any branch to be persistent on the [Branches](/dashboard/project/_/branches) page by clicking the triple dots icon next to the branch you want to modify, and selecting "Switch to persistent". + All persistent branches can be toggled back to be an ephemeral branch in the exact same way. ## Migration and seeding behavior diff --git a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx index 22c36d4dec9..134f593cf46 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx @@ -49,13 +49,13 @@ hideToc: true Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. - When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password. + When your project is up and running, navigate to your project dashboard and click on [Connect](https://supabase.com/dashboard/project/_?showConnect=true). - + Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [database settings](https://supabase.com/dashboard/project/_/settings/database) if you do not have it. - If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI. - - + + If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. +
diff --git a/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx b/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx index ffe60f4bf65..482a1ddcd49 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx @@ -31,13 +31,13 @@ hideToc: true Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. - When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password. + When your project is up and running, navigate to your project dashboard and click on [Connect](https://supabase.com/dashboard/project/_?showConnect=true). - + Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [database settings](https://supabase.com/dashboard/project/_/settings/database) if you do not have it. - If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI. - - + + If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. +
diff --git a/apps/docs/content/guides/platform/access-control.mdx b/apps/docs/content/guides/platform/access-control.mdx index 3105797415c..c3e9bb8ba43 100644 --- a/apps/docs/content/guides/platform/access-control.mdx +++ b/apps/docs/content/guides/platform/access-control.mdx @@ -42,7 +42,7 @@ Otherwise, you'll need to invite a user as **Owner**, and they need to accept th -Project scoped roles are only available on the [Enterprise Plan](https://supabase.com/pricing) +Project scoped roles are only available on the [Team and Enterprise plans](https://supabase.com/pricing). @@ -261,7 +261,7 @@ The table below shows the actions each role can take on the resources belonging [^1]: Available on the Team and Enterprise Plans. [^2]: Sending anonymous data to OpenAI is opt in and can improve Studio AI Assistant's responses. [^3]: Invites sent from a SSO account can only be accepted by another SSO account coming from the same identity provider. This is a security measure that prevents accidental invites to accounts not managed by your company's enterprise systems. -[^4]: Available on the Enterprise Plan. +[^4]: Available on the Team and Enterprise Plans. [^5]: Read-Only role is able to access secrets. [^6]: Listed permissions are for the API and Dashboard. [^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html). diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx index d2e62dcaaa6..b8510e7ba3d 100644 --- a/apps/docs/content/guides/platform/ipv4-address.mdx +++ b/apps/docs/content/guides/platform/ipv4-address.mdx @@ -72,7 +72,7 @@ nslookup db..supabase.co ## Identifying your connections -The pooler and direct connection strings can be found in the [database settings](https://supabase.com/dashboard/project/_/settings/database): +The pooler and direct connection strings can be found in the [project connect page](https://supabase.com/dashboard/project/_?showConnect=true): #### Direct connection diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/amazon-rds.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/amazon-rds.mdx index a680e3eaefc..33ff4d07cfd 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/amazon-rds.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/amazon-rds.mdx @@ -23,8 +23,8 @@ Supabase's core is Postgres, enabling the use of row-level security and providin ## Retrieve your Supabase host [#retrieve-supabase-host] 1. If you're new to Supabase, [create a project](https://database.new). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`). +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click on the View parameters under the connect string. Note your Host (`$SUPABASE_HOST`). ![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/amazon-rds/database-settings-host.png) diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx index 980619e82ac..e33c787357e 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx @@ -30,8 +30,8 @@ Supabase provides several [tools](https://github.com/supabase-community/firebase } ``` -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under `Connection parameters`, enable `Use connection pooling` and set the mode to `Session`. Replace the `Host` and `User` fields with the values shown. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click on the View parameters under the connect string. Replace the `Host` and `User` fields with the values shown. 1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file. ## Generate a Firebase private key [#generate-firebase-private-key] diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx index a0d55e9513a..809d4e38b26 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx @@ -29,8 +29,8 @@ The Firestore `collection` is "flattened" and converted to a table with basic co } ``` -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under `Connection parameters`, enable `Use connection pooling` and set the mode to `Session`. Replace the `Host` and `User` fields with the values shown. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click on the View parameters under the connect string. Replace the `Host` and `User` fields with the values shown. 1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file. ## Generate a Firebase private key [#generate-firebase-private-key] diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx index 94860a7d034..1b2610d7ed1 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx @@ -35,8 +35,8 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab ## Retrieve your Supabase connection string [#retrieve-supabase-connection-string] 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password. +1. Get your project's Session pooler connection string from your project dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). +1. Replace [YOUR-PASSWORD] in the connection string with your database password. You can reset your database password on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database) if you do not have it. ## Export your Heroku database to a file [#export-heroku-database] diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/mssql.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/mssql.mdx index 51d7080fb06..14a862acd76 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/mssql.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/mssql.mdx @@ -22,8 +22,8 @@ Before you begin the migration, you need to collect essential information about 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`). +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click on the View parameters under the connect string. Note your Host (`$SUPABASE_HOST`). ![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/mssql/database-settings-host.png) diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/mysql.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/mysql.mdx index 9aba64e0d2a..09a91700aff 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/mysql.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/mysql.mdx @@ -23,8 +23,8 @@ Before you begin the migration, you need to collect essential information about 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`). +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click on the View parameters under the connect string. Note your Host (`$SUPABASE_HOST`). ![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/mysql/database-settings-host.png) diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/neon.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/neon.mdx index 11cb75c29a1..64e54365efb 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/neon.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/neon.mdx @@ -34,9 +34,8 @@ export OLD_DB_URL="postgresql://neondb_owner:xxxxxxxxxxxxxxx-random-word-yyyyyyy 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection string**, select **URI**, make sure **Display connection pooler** is checked, and **Mode: Session** is set. -1. Click the **Copy** button to the right of your connection string to copy it to the clipboard. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click the **Copy** button to the right of your connection string to copy it to the clipboard. ## Set your `NEW_DB_URL` environment variable diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/postgres.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/postgres.mdx index aaf77fc4843..540d18d7ecb 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/postgres.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/postgres.mdx @@ -25,8 +25,13 @@ Example: 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under Session pooler, Copy the connection string and replace the password placeholder with your database password. + + + If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have + the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. + ![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/postgres/database-settings-host.png) diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/render.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/render.mdx index aea3ca0449c..d45299a4f68 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/render.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/render.mdx @@ -29,8 +29,13 @@ Example: 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under Session pooler, Copy the connection string and replace the password placeholder with your database password. + + + If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have + the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. + ## Migrate the database diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx index 94e8a6bb0fd..80631b2a3e4 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/vercel-postgres.mdx @@ -41,9 +41,8 @@ export OLD_DB_URL="postgres://default:xxxxxxxxxxxx@yy-yyyyy-yyyyyy-yyyyyyy.us-we 1. If you're new to Supabase, [create a project](https://supabase.com/dashboard). Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database). -1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard. -1. Under **Connection string**, select **URI**, make sure **Display connection pooler** is checked, and **Mode: Session** is set. -1. Click the **Copy** button to the right of your connection string to copy it to the clipboard. +1. On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) +1. Under the Session pooler, click the **Copy** button to the right of your connection string to copy it to the clipboard. ## Set your `NEW_DB_URL` environment variable diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx index 5331b997d0c..5ebaca89ad6 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx @@ -21,10 +21,10 @@ breadcrumb: 'Migrations' - Go to the [project page](/dashboard/project/_/) and click the "**Connect**" button at the top of the page for the connection string. + On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true). - Use the Session pooler connection string by default. If your ISP supports IPv6, use the direct connection string. + Use the Session pooler connection string by default. If your ISP supports IPv6 or you have the IPv4 add-on enabled, use the direct connection string. Session pooler connection string: @@ -131,7 +131,7 @@ breadcrumb: 'Migrations' - Reset the password in the [Database Settings](/dashboard/project/_/settings/database). + Reset the password in the [project connect page](/dashboard/project/_?showConnect=true). Replace ```[YOUR-PASSWORD]``` in the connection string with the database password. diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx index 9501394fb95..9e367c14ff2 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx @@ -59,20 +59,15 @@ Here are some things that are not stored directly in your database and will requ - Database extensions and settings - Read Replicas -Here is a non-exhaustive list of items that are commonly asked about and will be migrated with your backup restoration: - - - Database triggers - - Database functions - ## Restore backup - Go to the [project page](/dashboard/project/_/) and click the "**Connect**" button at the top of the page for the connection string. + On your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true). - Use the Session pooler connection string by default. If your ISP supports IPv6, use the direct connection string. + Use the Session pooler connection string by default. If your ISP supports IPv6 or you have the IPv4 add-on enabled, use the direct connection string. Session pooler connection string: diff --git a/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx index 50011eff417..1ae2437d733 100644 --- a/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx +++ b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx @@ -33,7 +33,7 @@ sudo apt-get update sudo apt-get install postgresql-client ``` -Once installed, you can find your PSQL string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database). Make sure if you are using the pooler connection that it is in session mode (port 5432). +Once installed, you can find your PSQL string on the dashboard by clicking [connect](https://supabase.com/dashboard/project/_?showConnect=true). Make sure if you are using the pooler connection that it is the Session pooler (port 5432). diff --git a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx index 8334936e7f3..7389fb42ae4 100644 --- a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx +++ b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx @@ -46,7 +46,7 @@ sudo apt-get update sudo apt-get install postgresql-client ``` -Once installed, you can find your PSQL string from the [Database Settings](https://supabase.com/dashboard/project/_/settings/database), which can be executed in the terminal to create a psql session. +Once installed, you can find your PSQL string on the dashboard by clicking [connect](https://supabase.com/dashboard/project/_?showConnect=true). Make sure if you are using the pooler connection that it is the Session pooler (port 5432). If your network can use IPv6, consider using the direct connection string instead of Supavisor. It's not mandatory, but for tasks that run a long time, it's best to reduce network complexity. To check if your network is compatible, use this cURL command to request your IPv6 address: @@ -54,7 +54,7 @@ If your network can use IPv6, consider using the direct connection string instea curl -6 https://ifconfig.co/ip ``` -If an address is returned, you should be able to use your direct connection string found in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database): +If an address is returned, you should use your direct connection string found on the dashboard by clicking [connect](https://supabase.com/dashboard/project/_?showConnect=true). **3. Increase memory for index creation (optional)** @@ -104,7 +104,8 @@ If your task is particularly long, you can speed it up by boosting your computin **7. Consider increasing disk size (optional)** -HSNW indexes can produce temporary files during their construction that may consume a few GBs worth of disk. Consider increasing the disk size in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) to accommodate for short-term disk stress. +HSNW indexes can produce temporary files during their construction that may consume a few GBs worth of disk. +Consider increasing the disk size in the [Compute and Disk settings](https://supabase.com/dashboard/project/_/settings/compute-and-disk) to accommodate for short-term disk stress. "Note uses an IPv6 address by default diff --git a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx index 86bbf33f68b..06952079762 100644 --- a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx +++ b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx @@ -33,7 +33,7 @@ They sit between the database and your client servers. They are solely optimized ### Where are the connection strings -Supabase provides 3 database connection strings that can be used simultaneously if necessary. All can be viewed in the connection string section of the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) +Supabase provides 3 database connection strings that can be used simultaneously if necessary. You can find them on the dashboard by clicking [Connect](https://supabase.com/dashboard/project/_?showConnect=true). 0) { const rewriteUrl = new URL(url) rewriteUrl.pathname = (BASE_PATH ?? '') + '/api/crawlers' - return NextResponse.rewrite(rewriteUrl) } } - } else { - const [, lib, maybeVersion] = url.pathname.replace(REFERENCE_PATH, '').split('/') + } - if (lib === 'cli') { - const rewritePath = [REFERENCE_PATH, 'cli'].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) - } + const [, lib, maybeVersion] = url.pathname.replace(REFERENCE_PATH, '').split('/') - if (lib === 'api') { - const rewritePath = [REFERENCE_PATH, 'api'].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) - } + if (lib === 'cli') { + const rewritePath = [REFERENCE_PATH, 'cli'].join('/') + return NextResponse.rewrite(new URL(rewritePath, request.url)) + } - if (lib?.startsWith('self-hosting-')) { - const rewritePath = [REFERENCE_PATH, lib].join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) - } + if (lib === 'api') { + const rewritePath = [REFERENCE_PATH, 'api'].join('/') + return NextResponse.rewrite(new URL(rewritePath, request.url)) + } - if (clientSdkIds.includes(lib)) { - const version = /v\d+/.test(maybeVersion) ? maybeVersion : null - const rewritePath = [REFERENCE_PATH, lib, version].filter(Boolean).join('/') - return NextResponse.rewrite(new URL(rewritePath, request.url)) - } + if (lib?.startsWith('self-hosting-')) { + const rewritePath = [REFERENCE_PATH, lib].join('/') + return NextResponse.rewrite(new URL(rewritePath, request.url)) + } + + if (clientSdkIds.includes(lib)) { + const version = /v\d+/.test(maybeVersion) ? maybeVersion : null + const rewritePath = [REFERENCE_PATH, lib, version].filter(Boolean).join('/') + return NextResponse.rewrite(new URL(rewritePath, request.url)) } return NextResponse.next() } export const config = { - matcher: '/((?!api|_next|static|public|favicon.ico).*)', + matcher: '/reference/:path*', } diff --git a/apps/docs/public/img/database/database-settings-host.png b/apps/docs/public/img/database/database-settings-host.png index 85c0ddcec14..366cb578616 100644 Binary files a/apps/docs/public/img/database/database-settings-host.png and b/apps/docs/public/img/database/database-settings-host.png differ diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 6a2a6120096..e8781085ddb 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.20.12 + version: 2.22.4 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -871,7 +871,7 @@ commands: title: supabase services summary: Show versions of all Supabase services tags: - - management-api + - local-dev links: [] usage: supabase services subcommands: [] diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml index e7dc26710bc..73c07127349 100644 --- a/apps/docs/spec/supabase_js_v2.yml +++ b/apps/docs/spec/supabase_js_v2.yml @@ -1387,7 +1387,8 @@ functions: examples: - id: update-the-email-for-an-authenticated-user name: Update the email for an authenticated user - description: Sends a "Confirm Email Change" email to the new email address. + description: | + Sends a "Confirm Email Change" email to the new address. If **Secure Email Change** is enabled (default), confirmation is also required from the **old email** before the change is applied. To skip dual confirmation and apply the change after only the new email is verified, disable **Secure Email Change** in the [Email Auth Provider settings](/dashboard/project/_/auth/providers?provider=Email). isSpotlight: false code: | ```js diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 80f0c99015f..09a04f10157 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -7,7 +7,6 @@ import { createPortal } from 'react-dom' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' -import { useUrlState } from 'hooks/ui/useUrlState' import { RoleImpersonationState } from 'lib/role-impersonation' import { EMPTY_ARR } from 'lib/void' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 5a8a0221444..d4d4b770dfa 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -1,5 +1,7 @@ import { noop } from 'lodash' +import { FeatureFlagContext } from 'common' +import { useFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { EMPTY_OBJ } from 'lib/void' import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react' @@ -15,7 +17,7 @@ export const FEATURE_PREVIEWS = [ key: LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW, name: 'Layout Update for Organizations', content: , - discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/33670', isNew: false, isPlatformOnly: true, }, @@ -72,6 +74,19 @@ const FeaturePreviewContext = createContext({ export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext) export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => { + const { hasLoaded } = useContext(FeatureFlagContext) + const enableNewLayoutPreview = useFlag('newLayoutPreview') + + // [Joshen] Similar logic to feature flagging previews, we can use flags to default opt in previews + const isDefaultOptIn = (feature: (typeof FEATURE_PREVIEWS)[number]) => { + switch (feature.key) { + case LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW: + return enableNewLayoutPreview + default: + return false + } + } + const [flags, setFlags] = useState(() => FEATURE_PREVIEWS.reduce((a, b) => { return { ...a, [b.key]: false } @@ -82,11 +97,16 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{} if (typeof window !== 'undefined') { setFlags( FEATURE_PREVIEWS.reduce((a, b) => { - return { ...a, [b.key]: localStorage.getItem(b.key) === 'true' } + const defaultOptIn = isDefaultOptIn(b) + const localStorageValue = localStorage.getItem(b.key) + return { + ...a, + [b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true', + } }, {}) ) } - }, []) + }, [hasLoaded]) const value = { flags, diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 71862a31f93..5f78dde3e6b 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -5,7 +5,6 @@ import { useEffect } from 'react' import { useParams } from 'common' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { removeTabsByEditor } from 'state/tabs' @@ -19,12 +18,9 @@ const FeaturePreviewModal = () => { const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() - const enableNewLayoutPreview = useFlag('newLayoutPreview') - + // [Joshen] Use this if we want to feature flag previews function isReleasedToPublic(feature: (typeof FEATURE_PREVIEWS)[number]) { switch (feature.key) { - case LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW: - return enableNewLayoutPreview default: return true } diff --git a/apps/studio/components/interfaces/App/FeaturePreview/LayoutUpdatePreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/LayoutUpdatePreview.tsx index 0c143b92305..be462dde866 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/LayoutUpdatePreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/LayoutUpdatePreview.tsx @@ -1,5 +1,10 @@ -import { BASE_PATH } from 'lib/constants' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { ExternalLink, X } from 'lucide-react' import Image from 'next/image' +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button } from 'ui' +import { useIsNewLayoutEnabled } from './FeaturePreviewContext' export const LayoutUpdatePreview = () => { return ( @@ -37,3 +42,48 @@ export const LayoutUpdatePreview = () => { ) } + +export const LayoutUpdateBanner = () => { + const newLayoutPreview = useIsNewLayoutEnabled() + const [newLayoutPreviewState] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW, '') + const [newLayoutAcknowledged, setNewLayoutAcknowledged] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.NEW_LAYOUT_NOTICE_ACKNOWLEDGED, + false + ) + const isDefaultOptedInNewLayout = newLayoutPreview && newLayoutPreviewState === '' + + if (!isDefaultOptedInNewLayout || newLayoutAcknowledged) return null + + return ( + + + + NEW + + Dashboard layout has been updated! + + + We've updated the dashboard layout to make it easier to find and manage your organization + settings. + + + + + } + className="absolute top-2 right-2 px-1" + onClick={() => setNewLayoutAcknowledged(true)} + tooltip={{ content: { side: 'bottom', text: 'Dismiss' } }} + /> + + ) +} diff --git a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx index 1447e364f4d..ef14cacedce 100644 --- a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -8,13 +8,15 @@ import { useProjectsQuery } from 'data/projects/projects-query' import useLatest from 'hooks/misc/useLatest' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { DEFAULT_HOME, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' +import { useIsNewLayoutEnabled } from './FeaturePreview/FeaturePreviewContext' // Ideally these could all be within a _middleware when we use Next 12 const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() const { ref, slug, id } = useParams() + const newLayoutPreview = useIsNewLayoutEnabled() const isLoggedIn = useIsLoggedIn() const snap = useAppStateSnapshot() @@ -31,6 +33,12 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { '' ) + const DEFAULT_HOME = IS_PLATFORM + ? newLayoutPreview + ? '/organizations' + : '/projects' + : '/project/default' + /** * Array of urls/routes that should be ignored */ diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx index 7a3285b3045..317f81ccd45 100644 --- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -7,10 +7,10 @@ import Table from 'components/to-be-cleaned/Table' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations-query' -import { Button, Input, SidePanel } from 'ui' -import MigrationsEmptyState from './MigrationsEmptyState' import { Search } from 'lucide-react' +import { Button, Input, SidePanel } from 'ui' import { Admonition } from 'ui-patterns' +import MigrationsEmptyState from './MigrationsEmptyState' const Migrations = () => { const [search, setSearch] = useState('') @@ -55,7 +55,7 @@ const Migrations = () => { > + + + + + + setPublicationPanelVisible(false)} + /> + + ) : ( + <> + + +
+ + New Destination + + + + + + {/* Pricing to be decided yet */} + Enabling replication will cost additional $xx.xx + + + +
+ +
+
+
+
+ + + +
+
+
+ + )} + + ) +} + +export default DestinationPanel diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx new file mode 100644 index 00000000000..465a35cba47 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -0,0 +1,206 @@ +import Table from 'components/to-be-cleaned/Table' +import AlertError from 'components/ui/AlertError' +import { ReplicationPipelinesData } from 'data/replication/pipelines-query' +import { ResponseError } from 'types' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import RowMenu from './RowMenu' +import PipelineStatus from './PipelineStatus' +import { useParams } from 'common' +import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query' +import { useState } from 'react' +import { toast } from 'sonner' +import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation' +import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' +import { useDeleteSinkMutation } from 'data/replication/delete-sink-mutation' +import { useDeletePipelineMutation } from 'data/replication/delete-pipeline-mutation' +import DeleteDestination from './DeleteDestination' +import DestinationPanel from './DestinationPanel' + +export type Pipeline = ReplicationPipelinesData['pipelines'][0] + +interface DestinationRowProps { + sourceId: number | undefined + sinkId: number + sinkName: string + type: string + pipeline: Pipeline | undefined + error: ResponseError | null + isLoading: boolean + isError: boolean + isSuccess: boolean +} + +const DestinationRow = ({ + sourceId, + sinkId, + sinkName, + type, + pipeline, + error: pipelineError, + isLoading: isPipelineLoading, + isError: isPipelineError, + isSuccess: isPipelineSuccess, +}: DestinationRowProps) => { + const { ref: projectRef } = useParams() + const [refetchInterval, setRefetchInterval] = useState(false) + const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false) + const [showEditDestinationPanel, setShowEditDestinationPanel] = useState(false) + + const { + data: pipelineStatusData, + error: pipelineStatusError, + isLoading: isPipelineStatusLoading, + isError: isPipelineStatusError, + isSuccess: isPipelineStatusSuccess, + } = useReplicationPipelineStatusQuery( + { + projectRef, + pipelineId: pipeline?.id, + }, + { refetchInterval } + ) + const [requestStatus, setRequestStatus] = useState< + 'None' | 'EnableRequested' | 'DisableRequested' + >('None') + const { mutateAsync: startPipeline } = useStartPipelineMutation() + const { mutateAsync: stopPipeline } = useStopPipelineMutation() + const pipelineStatus = pipelineStatusData?.status + if ( + (requestStatus === 'EnableRequested' && pipelineStatus === 'Started') || + (requestStatus === 'DisableRequested' && pipelineStatus === 'Stopped') + ) { + setRefetchInterval(false) + setRequestStatus('None') + } + + const onEnableClick = async () => { + if (!projectRef) { + console.error('Project ref is required') + return + } + if (!pipeline) { + toast.error('No pipeline found') + return + } + + try { + await startPipeline({ projectRef, pipelineId: pipeline.id }) + } catch (error) { + toast.error('Failed to enable destination') + } + setRequestStatus('EnableRequested') + setRefetchInterval(5000) + } + const onDisableClick = async () => { + if (!projectRef) { + console.error('Project ref is required') + return + } + if (!pipeline) { + toast.error('No pipeline found') + return + } + + try { + await stopPipeline({ projectRef, pipelineId: pipeline.id }) + } catch (error) { + toast.error('Failed to disable destination') + } + setRequestStatus('DisableRequested') + setRefetchInterval(5000) + } + const { mutateAsync: deleteSink } = useDeleteSinkMutation({}) + const { mutateAsync: deletePipeline } = useDeletePipelineMutation({ + onSuccess: (_res: any) => { + toast.success('Successfully deleted destination') + }, + }) + + const onDeleteClick = async () => { + if (!projectRef) { + console.error('Project ref is required') + return + } + if (!pipeline) { + toast.error('No pipeline found') + return + } + + try { + await stopPipeline({ projectRef, pipelineId: pipeline.id }) + await deletePipeline({ projectRef, pipelineId: pipeline.id }) + await deleteSink({ projectRef, sinkId }) + } catch (error) { + toast.error('Failed to delete destination') + } + } + + return ( + <> + {isPipelineError && ( + + )} + {isPipelineSuccess && ( + + + {isPipelineLoading ? : sinkName} + + {isPipelineLoading ? : type} + + {isPipelineLoading || !pipeline ? ( + + ) : ( + + )} + + + {isPipelineLoading || !pipeline ? ( + + ) : ( + pipeline.publication_name + )} + + + setShowDeleteDestinationForm(true)} + onEditClick={() => setShowEditDestinationPanel(true)} + > + + + )} + + setShowEditDestinationPanel(false)} + sourceId={sourceId} + existingDestination={{ + sourceId, + sinkId, + pipelineId: pipeline?.id, + enabled: pipelineStatusData?.status === 'Started', + }} + /> + + ) +} + +export default DestinationRow diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx new file mode 100644 index 00000000000..a48f109fa11 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -0,0 +1,138 @@ +import { useParams } from 'common' +import Table from 'components/to-be-cleaned/Table' +import AlertError from 'components/ui/AlertError' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useReplicationSinksQuery } from 'data/replication/sinks-query' +import { Plus } from 'lucide-react' +import { Button, cn } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' +import DestinationRow from './DestinationRow' +import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query' +import { FormHeader } from 'components/ui/Forms/FormHeader' +import { useState } from 'react' +import NewDestinationPanel from './DestinationPanel' +import { useReplicationSourcesQuery } from 'data/replication/sources-query' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' + +const Destinations = () => { + const [showNewDestinationPanel, setShowNewDestinationPanel] = useState(false) + const { ref: projectRef } = useParams() + + const { + data: sourcesData, + error: sourcesError, + isLoading: isSourcesLoading, + isError: isSourcesError, + isSuccess: isSourcesSuccess, + } = useReplicationSourcesQuery({ + projectRef, + }) + + let sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id + + const { + data: sinksData, + error: sinksError, + isLoading: isSinksLoading, + isError: isSinksError, + isSuccess: isSinksSuccess, + } = useReplicationSinksQuery({ + projectRef, + }) + + const { + data: pipelinesData, + error: pipelinesError, + isLoading: isPipelinesLoading, + isError: isPipelinesError, + isSuccess: isPipelinesSuccess, + } = useReplicationPipelinesQuery({ + projectRef, + }) + + const anySinks = isSinksSuccess && sinksData.sinks.length > 0 + + return ( + <> + +
+ Destinations + +
+ {(isSourcesLoading || isSinksLoading) && } + + {(isSourcesError || isSinksError) && ( + + )} + + {anySinks ? ( + Name, + Type, + Status, + Publication, + , + ]} + body={sinksData.sinks.map((sink) => { + const pipeline = pipelinesData?.pipelines.find((p) => p.sink_id === sink.id) + return ( + + ) + })} + >
+ ) : ( + !isSourcesLoading && + !isSinksLoading && + !isSourcesError && + !isSinksError && ( +
+

Send data to your first destination

+

+ Use destinations to improve performance or run analysis on your data via + integrations like BigQuery +

+ +
+ ) + )} +
+ + setShowNewDestinationPanel(false)} + > + + ) +} + +export default Destinations diff --git a/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx new file mode 100644 index 00000000000..2d06e06b4c0 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/NewPublicationPanel.tsx @@ -0,0 +1,179 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useParams } from 'common' +import { useCreatePublicationMutation } from 'data/replication/create-publication-mutation' +import { useReplicationTablesQuery } from 'data/replication/tables-query' +import { X } from 'lucide-react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetClose, + cn, + Button, + SheetFooter, + SheetSection, + Form_Shadcn_, + FormLabel_Shadcn_, + FormField_Shadcn_, + FormItem_Shadcn_, + FormControl_Shadcn_, + Input_Shadcn_, + FormMessage_Shadcn_, + Card, + CardContent, + SheetDescription, +} from 'ui' +import { MultiSelector } from 'ui-patterns/multi-select' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' + +interface NewPublicationPanelProps { + visible: boolean + sourceId?: number + onClose: () => void +} + +const NewPublicationPanel = ({ visible, sourceId, onClose }: NewPublicationPanelProps) => { + const { ref: projectRef } = useParams() + const { mutateAsync: createPublication, isLoading: creatingPublication } = + useCreatePublicationMutation() + const { data: tables } = useReplicationTablesQuery({ + projectRef, + sourceId, + }) + const formId = 'publication-editor' + const FormSchema = z.object({ + name: z.string().min(1, 'Name is required'), + tables: z.array(z.string()).min(1, 'At least one table is required'), + }) + const defaultValues = { + name: '', + tables: [], + } + const form = useForm>({ + mode: 'onBlur', + reValidateMode: 'onBlur', + resolver: zodResolver(FormSchema), + defaultValues, + }) + + const onSubmit = async (data: z.infer) => { + if (!projectRef) return console.error('Project ref is required') + if (!sourceId) return console.error('Source id is required') + try { + await createPublication({ + projectRef, + sourceId, + name: data.name, + tables: data.tables.map((table) => { + const [schema, name] = table.split('.') + return { schema, name } + }), + }) + toast.success('Successfully created publication') + onClose() + } catch (error) { + toast.error('Failed to create publication') + } + form.reset(defaultValues) + } + + return ( + <> + + +
+ +
+
+ New Publication + + Create a new publication to share table changes for replication + +
+ + + Close + +
+
+ + +
+ ( + + + + + + )} + /> + ( + + + + + + + + + {tables?.tables.map((table) => ( + + {`${table.schema}.${table.name}`} + + ))} + + + + + + )} + /> + +
+
+ + + + +
+
+
+ + ) +} + +export default NewPublicationPanel diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx new file mode 100644 index 00000000000..2569c32cd14 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatus.tsx @@ -0,0 +1,57 @@ +import AlertError from 'components/ui/AlertError' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { cn } from 'ui' +import { ResponseError } from 'types' +import { Loader2 } from 'lucide-react' + +interface PipelineStatusProps { + pipelineStatus: string | undefined + error: ResponseError | null + isLoading: boolean + isError: boolean + isSuccess: boolean + requestStatus: 'None' | 'EnableRequested' | 'DisableRequested' +} + +const PipelineStatus = ({ + pipelineStatus, + error, + isLoading, + isError, + isSuccess, + requestStatus, +}: PipelineStatusProps) => { + const pipelineEnabled = pipelineStatus === 'Stopped' ? false : true + const requestInFlight = requestStatus !== 'None' + const status = + requestStatus === 'EnableRequested' + ? 'Enabling' + : requestStatus === 'DisableRequested' + ? 'Disabling' + : pipelineStatus === 'Stopped' + ? 'Disabled' + : 'Enabled' + return ( + <> + {isLoading && } + {isError && } + {isSuccess && ( +
+ {requestInFlight ? ( + + ) : ( +
+ )} + {status} +
+ )} + + ) +} + +export default PipelineStatus diff --git a/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx b/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx new file mode 100644 index 00000000000..7ed6599795b --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/PublicationsComboBox.tsx @@ -0,0 +1,132 @@ +import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react' +import { useState } from 'react' +import { + Button, + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + CommandSeparator_Shadcn_, + Popover_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + ScrollArea, +} from 'ui' +import { ControllerRenderProps } from 'react-hook-form' + +interface PublicationsComboBoxProps { + publications: string[] + loading: boolean + onNewPublicationClick: () => void + field: ControllerRenderProps +} + +const PublicationsComboBox = ({ + publications, + loading, + onNewPublicationClick, + field, +}: PublicationsComboBoxProps) => { + const [dropdownOpen, setDropdownOpen] = useState(false) + const [selectedPublication, setSelectedPublication] = useState(field?.value || '') + const [searchTerm, setSearchTerm] = useState('') + + function handleSearchChange(value: string) { + setSearchTerm(value) + } + + function handlePublicationSelect(pub: string) { + setSelectedPublication(pub) + setDropdownOpen(false) + field.onChange(pub) + } + + return ( + { + setDropdownOpen(open) + if (!open && field?.onBlur) { + field.onBlur() + } + }} + > + + + + + + + + + {loading ? ( +
+ + Loading... +
+ ) : ( + 'No publications found' + )} +
+ + 7 ? 'h-[210px]' : ''}> + {publications.map((pub) => ( + { + handlePublicationSelect(pub) + }} + onClick={() => { + handlePublicationSelect(pub) + }} + > + {pub} + {selectedPublication === pub && ( + + )} + + ))} + + + + + + +

New publication

+
+
+
+
+
+
+ ) +} + +export default PublicationsComboBox diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx new file mode 100644 index 00000000000..2852aea3577 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -0,0 +1,72 @@ +import AlertError from 'components/ui/AlertError' +import { Edit, MoreVertical, Pause, Play, Trash } from 'lucide-react' +import { ResponseError } from 'types' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' + +interface RowMenuProps { + pipelineStatus: string | undefined + error: ResponseError | null + isLoading: boolean + isError: boolean + onEnableClick: () => void + onDisableClick: () => void + onEditClick: () => void + onDeleteClick: () => void +} + +const RowMenu = ({ + pipelineStatus, + error, + isLoading, + isError, + onEnableClick, + onDisableClick, + onEditClick, + onDeleteClick, +}: RowMenuProps) => { + const pipelineEnabled = pipelineStatus === 'Stopped' ? false : true + return ( +
+ {isLoading && } + {isError && } + + + +
+ ) +} + +export default RowMenu diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx index 4f1cf7abdff..392804719b8 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -292,7 +292,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { )} label={
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx index 294a65efe43..879ffd756e4 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx @@ -9,6 +9,7 @@ import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query' import { useCronJobRunQuery } from 'data/database-cron-jobs/database-cron-jobs-run-query' import { useDatabaseCronJobToggleMutation } from 'data/database-cron-jobs/database-cron-jobs-toggle-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { Badge, Button, @@ -26,7 +27,6 @@ import { TimestampInfo } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { convertCronToString, getNextRun } from './CronJobs.utils' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' interface CronJobCardProps { job: CronJob diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 76fd804bd7a..528a6438755 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -187,10 +187,6 @@ const supabaseIntegrations: IntegrationDefinition[] = [ route: 'overview', label: 'Overview', }, - { - route: 'keys', - label: 'Keys', - }, { route: 'secrets', label: 'Secrets', @@ -208,16 +204,6 @@ const supabaseIntegrations: IntegrationDefinition[] = [ loading: Loading, } ) - case 'keys': - return dynamic( - () => - import('../Vault/Keys/EncryptionKeysManagement').then( - (mod) => mod.EncryptionKeysManagement - ), - { - loading: Loading, - } - ) case 'secrets': return dynamic( () => import('../Vault/Secrets/SecretsManagement').then((mod) => mod.SecretsManagement), diff --git a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeySelector.tsx b/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeySelector.tsx deleted file mode 100644 index 071eae7ef06..00000000000 --- a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeySelector.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { noop } from 'lodash' -import { Plus } from 'lucide-react' -import { - Input, - SelectContent_Shadcn_, - SelectGroup_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - Select_Shadcn_, -} from 'ui' - -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { usePgSodiumKeysQuery } from 'data/pg-sodium-keys/pg-sodium-keys-query' - -interface EncryptionKeySelectorProps { - nameId?: string - label?: string - labelOptional?: string - selectedKeyId?: any - error?: string - onSelectKey: (keyId: string) => void - onUpdateDescription?: (desc: string) => void -} - -const EncryptionKeySelector = ({ - nameId = 'keyName', - label = 'Encryption key', - labelOptional, - selectedKeyId, - error, - onSelectKey = noop, - onUpdateDescription = noop, -}: EncryptionKeySelectorProps) => { - const { project } = useProjectContext() - - const { data } = usePgSodiumKeysQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const keys = data || [] - - return ( - <> -
- {label && ( -
- - {labelOptional && ( - {labelOptional} - )} -
- )} - - - {keys.find((key) => key.id === selectedKeyId)?.name || 'Select an encryption key'} - - - - - - - Create a new Encryption Key - - - {keys.length > 0 && ( - <> -
- {keys.map((key) => ( - -
-

{key.name || 'No name provided'}

-

- ID: {key.id} -

-
-
- ))} - - )} - - - -
- {selectedKeyId === 'create-new' && ( - onUpdateDescription(event.target.value)} - descriptionText="Provide a name for your key for easier identification" - /> - )} - - ) -} - -export default EncryptionKeySelector diff --git a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx deleted file mode 100644 index ef7a070cb40..00000000000 --- a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import dayjs from 'dayjs' -import { sortBy } from 'lodash' -import { Fragment, useEffect, useState } from 'react' -import { toast } from 'sonner' - -import { useParams } from 'common' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { DocsButton } from 'components/ui/DocsButton' -import { usePgSodiumKeyCreateMutation } from 'data/pg-sodium-keys/pg-sodium-key-create-mutation' -import { usePgSodiumKeyDeleteMutation } from 'data/pg-sodium-keys/pg-sodium-key-delete-mutation' -import { usePgSodiumKeysQuery } from 'data/pg-sodium-keys/pg-sodium-keys-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Key, Loader, Search, Trash, X } from 'lucide-react' -import { Button, Form, Input, Listbox, Modal, Separator } from 'ui' -import { Admonition } from 'ui-patterns' - -const DEFAULT_KEY_NAME = 'No description provided' - -export const EncryptionKeysManagement = () => { - const { search } = useParams() - const { project } = useProjectContext() - - const [searchValue, setSearchValue] = useState('') - const [selectedSort, setSelectedSort] = useState<'name' | 'created'>('created') - const [showAddKeyModal, setShowAddKeyModal] = useState(false) - const [selectedKeyToRemove, setSelectedKeyToRemove] = useState() - const canManageKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - - useEffect(() => { - if (search !== undefined) setSearchValue(search) - }, [search]) - - const { data, isLoading } = usePgSodiumKeysQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const { mutate: addKeyMutation, isLoading: isCreating } = usePgSodiumKeyCreateMutation({ - onSuccess: () => { - toast.success('Successfully added new key') - setShowAddKeyModal(false) - }, - }) - const { mutate: deleteKeyMutation, isLoading: isDeleting } = usePgSodiumKeyDeleteMutation({ - onSuccess: () => { - toast.success(`Successfully deleted encryption key`) - setSelectedKeyToRemove(undefined) - }, - }) - - const allKeys = data || [] - const keys = sortBy( - searchValue - ? allKeys.filter( - (key) => - (key?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) || - key.id.toLowerCase().includes(searchValue.toLowerCase()) - ) - : allKeys, - (k) => { - if (selectedSort === 'created') { - return Number(new Date(k.created)) - } else { - return k[selectedSort] - } - } - ) - - const addKey = async (values: any, { setSubmitting }: any) => { - if (!project) return console.error('Project is required') - - addKeyMutation({ - projectRef: project.ref, - connectionString: project.connectionString, - name: values.name, - }) - } - - const confirmDeleteKey = async () => { - if (!selectedKeyToRemove) return - if (!project) return console.error('Project is required') - - deleteKeyMutation({ - projectRef: project.ref, - connectionString: project.connectionString, - id: selectedKeyToRemove.id, - }) - } - - return ( - <> -
-
-
- setSearchValue(event.target.value)} - icon={} - actions={ - searchValue.length > 0 - ? [ -
-
- - setShowAddKeyModal(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canManageKeys ? 'You need additional permissions to add keys' : undefined, - }, - }} - > - Add new key - -
-
- - {/* Table of keys */} -
- {isLoading ? ( -
- -

Loading keys from the Vault

-
- ) : ( - <> - {keys.map((key, idx) => { - return ( - -
- -
-

- {key.name || DEFAULT_KEY_NAME} -

-

- ID: {key.id} -

-
-
-

- Added on {dayjs(key.created).format('MMM D, YYYY')} -

- } - disabled={!canManageKeys} - onClick={() => setSelectedKeyToRemove(key)} - tooltip={{ - content: { - side: 'bottom', - text: !canManageKeys - ? 'You need additional permissions to delete keys' - : undefined, - }, - }} - /> -
-
- {idx !== keys.length - 1 && } -
- ) - })} - {keys.length === 0 && ( - <> - {searchValue.length === 0 ? ( -
-

No encryption keys added yet

-

- Encryption keys are created by the pgsodium extension and can be used to - encrypt your columns and secrets -

-
- ) : ( -
-

No results found

-

- Your search for "{searchValue}" did not return any results -

-
- )} - - )} - - )} -
-
- - setSelectedKeyToRemove(undefined)} - onConfirm={confirmDeleteKey} - loading={isDeleting} - header="Confirm to delete key" - > - - -

- The following key will be permanently removed and cannot be recovered. -

-
-

- {selectedKeyToRemove?.name ?? DEFAULT_KEY_NAME} -

-

- ID: {selectedKeyToRemove?.id} -

-
-
-
- - setShowAddKeyModal(false)} - header="Add a new key" - > -
{ - const errors: any = {} - if (values.name.length === 0) errors.name = 'Please provide a name for your key' - return errors - }} - onSubmit={addKey} - > - {() => { - return ( - <> - -

Provide a name for your key for easier identification.

- -
- - - - - - - ) - }} - -
- - ) -} diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx index 4b005caa51e..0f9996d60da 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx @@ -4,12 +4,9 @@ import { toast } from 'sonner' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import InformationBox from 'components/ui/InformationBox' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { usePgSodiumKeyCreateMutation } from 'data/pg-sodium-keys/pg-sodium-key-create-mutation' -import { usePgSodiumKeysQuery } from 'data/pg-sodium-keys/pg-sodium-keys-query' import { useVaultSecretCreateMutation } from 'data/vault/vault-secret-create-mutation' import { Eye, EyeOff, HelpCircle } from 'lucide-react' import { Button, Form, Input, Modal } from 'ui' -import EncryptionKeySelector from '../Keys/EncryptionKeySelector' interface AddNewSecretModalProps { visible: boolean @@ -18,35 +15,20 @@ interface AddNewSecretModalProps { const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { const [showSecretValue, setShowSecretValue] = useState(false) - const [selectedKeyId, setSelectedKeyId] = useState() const { project } = useProjectContext() - const { mutateAsync: addKeyMutation } = usePgSodiumKeyCreateMutation() const { mutateAsync: addSecret } = useVaultSecretCreateMutation() - const { data: keys, isLoading } = usePgSodiumKeysQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - }, - { - enabled: visible, - } - ) - useEffect(() => { - if (visible && keys) { + if (visible) { setShowSecretValue(false) - setSelectedKeyId(keys[0]?.id ?? 'create-new') } - }, [visible, keys]) + }, [visible]) const validate = (values: any) => { const errors: any = {} if (values.name.length === 0) errors.name = 'Please provide a name for your secret' if (values.secret.length === 0) errors.secret = 'Please enter your secret value' - if (selectedKeyId === 'create-new' && values.keyName.length === 0) - errors.keyName = 'Please provide a name for your new key' return errors } @@ -54,18 +36,9 @@ const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { if (!project) return console.error('Project is required') setSubmitting(true) - let encryptionKeyId = selectedKeyId try { setSubmitting(true) - if (selectedKeyId === 'create-new') { - const addKeyRes = await addKeyMutation({ - projectRef: project?.ref!, - connectionString: project?.connectionString, - name: values.keyName || undefined, - }) - encryptionKeyId = addKeyRes[0].id - } await addSecret({ projectRef: project.ref, @@ -73,7 +46,6 @@ const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { name: values.name, description: values.description, secret: values.secret, - key_id: encryptionKeyId, }) toast.success(`Successfully added new secret ${values.name}`) onClose() @@ -86,81 +58,47 @@ const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { return ( - {isLoading ? ( - - - - ) : ( -
- {({ isSubmitting }: any) => { - return ( - <> - - - - -
- } - /> - - - - - } - url="https://github.com/supabase/vault" - urlLabel="Vault documentation" - title="What is a key?" - description={ -
-

- Keys are used to encrypt data inside your database, and every secret in - the Vault is encrypted with a key. -

-

- You may create different keys for different purposes, such as one for - encrypting user data, and another for application data. -

-
- } - /> -
- - - - - - - ) - }} - - )} +
+ {({ isSubmitting }: any) => { + return ( + <> + + + + +
+ } + /> + + + + + + + + ) + }} + ) } diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/DeleteSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/DeleteSecretModal.tsx index 45643cb06ec..c918cc9c8e2 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/DeleteSecretModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/DeleteSecretModal.tsx @@ -50,7 +50,7 @@ const DeleteSecretModal = ({ selectedSecret, onClose }: DeleteSecretModalProps)

{selectedSecret?.description}

- ID: {selectedSecret?.key_id} + ID: {selectedSecret?.id}

diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx index f0ffbcf03f8..5c19268657a 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx @@ -4,12 +4,10 @@ import { toast } from 'sonner' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { usePgSodiumKeyCreateMutation } from 'data/pg-sodium-keys/pg-sodium-key-create-mutation' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' import { useVaultSecretUpdateMutation } from 'data/vault/vault-secret-update-mutation' import type { VaultSecret } from 'types' import { Button, Form, Input, Modal } from 'ui' -import EncryptionKeySelector from '../Keys/EncryptionKeySelector' import { EyeOff, Eye } from 'lucide-react' interface EditSecretModalProps { @@ -18,11 +16,9 @@ interface EditSecretModalProps { } const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => { - const [selectedKeyId, setSelectedKeyId] = useState() const [showSecretValue, setShowSecretValue] = useState(false) const { project } = useProjectContext() - const { mutateAsync: addKeyMutation } = usePgSodiumKeyCreateMutation() const { mutateAsync: updateSecret } = useVaultSecretUpdateMutation() let INITIAL_VALUES = { @@ -34,7 +30,6 @@ const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => { useEffect(() => { if (selectedSecret !== undefined) { setShowSecretValue(false) - setSelectedKeyId(selectedSecret.key_id) } }, [selectedSecret]) @@ -53,23 +48,6 @@ const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => { if (values.name !== selectedSecret?.name) payload.name = values.name if (values.description !== selectedSecret?.description) payload.description = values.description - if (selectedKeyId !== selectedSecret?.key_id) { - let encryptionKeyId = selectedKeyId - if (encryptionKeyId === 'create-new') { - const addKeyRes = await addKeyMutation({ - projectRef: project?.ref!, - connectionString: project?.connectionString, - name: values.keyName || undefined, - }) - if (addKeyRes.error) { - return toast.error(`Failed to create new key: ${addKeyRes.error.message}`) - } else { - encryptionKeyId = addKeyRes[0].id - } - } - - payload.key_id = encryptionKeyId - } payload.secret = values.secret if (!isEmpty(payload) && selectedSecret) { @@ -162,16 +140,6 @@ const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => { /> - - - -
- - - {secret.key_id} - + +

+ {secret.id} +

diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx index c88dd085827..2faf363d538 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx @@ -39,10 +39,8 @@ export const SecretsManagement = () => { const allSecrets = data || [] const secrets = sortBy( searchValue.length > 0 - ? allSecrets.filter( - (secret) => - (secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) || - secret.key_id.toLowerCase().includes(searchValue.toLowerCase()) + ? allSecrets.filter((secret) => + (secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) ) : allSecrets, (s) => { @@ -99,14 +97,6 @@ export const SecretsManagement = () => { > Name - - Key ID -
diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx index da2876a39dc..923e57e8a0e 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx @@ -6,7 +6,9 @@ import { toast } from 'sonner' import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useOrganizationDeleteMutation } from 'data/organizations/organization-delete-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { Button, Form, Input, Modal } from 'ui' const DeleteOrganizationButton = () => { @@ -18,11 +20,21 @@ const DeleteOrganizationButton = () => { const [isOpen, setIsOpen] = useState(false) const [value, setValue] = useState('') + const [_, setLastVisitedOrganization] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, + '' + ) + const canDeleteOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') const { mutate: deleteOrganization, isLoading: isDeleting } = useOrganizationDeleteMutation({ onSuccess: () => { toast.success(`Successfully deleted ${orgName}`) - router.push(newLayoutPreview ? '/organizations' : '/projects') + if (newLayoutPreview) { + setLastVisitedOrganization('') + router.push('/organizations') + } else { + router.push('/projects') + } }, }) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index 27ad91f1448..4426b4af278 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -1,4 +1,5 @@ import { Search } from 'lucide-react' +import { useRouter } from 'next/router' import { useState } from 'react' import { toast } from 'sonner' @@ -16,10 +17,12 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { BASE_PATH } from 'lib/constants' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { useProfile } from 'lib/profile' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -29,6 +32,10 @@ import { hasMultipleOwners, useGetRolesManagementPermissions } from './TeamSetti const TeamSettings = () => { const newLayoutPreview = useIsNewLayoutEnabled() + const [_, setLastVisitedOrganization] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, + '' + ) const { organizationMembersCreate: organizationMembersCreationEnabled, @@ -36,11 +43,13 @@ const TeamSettings = () => { } = useIsFeatureEnabled(['organization_members:create', 'organization_members:delete']) const { slug } = useParams() + const router = useRouter() const { profile } = useProfile() const selectedOrganization = useSelectedOrganization() const isOwner = selectedOrganization?.is_owner const { data: permissions } = usePermissionsQuery() + const { refetch: refetchOrganizations } = useOrganizationsQuery() const { data: rolesData } = useOrganizationRolesV2Query({ slug }) const { data: members } = useOrganizationMembersQuery({ slug }) @@ -59,10 +68,19 @@ const TeamSettings = () => { const canLeave = !isOwner || (isOwner && hasMultipleOwners(members, roles)) const { mutate: deleteMember } = useOrganizationMemberDeleteMutation({ - onSuccess: () => { + onSuccess: async () => { setIsLeaving(false) setIsLeaveTeamModalOpen(false) - window?.location.replace(BASE_PATH) // Force reload to clear Store + + await refetchOrganizations() + toast.success(`Successfully left ${selectedOrganization?.name}`) + + if (newLayoutPreview) { + setLastVisitedOrganization('') + router.push('/organizations') + } else { + router.push('/projects') + } }, onError: (error) => { setIsLeaving(false) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx index 4782245ccd3..186beb2ea4d 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx @@ -142,14 +142,12 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP if (isApplyAllProjects) { if (originalConfigurationType === 'org-scope') { - console.log('Reset: Org scoped') setProjectsRoleConfiguration(originalConfiguration) } else { setProjectsRoleConfiguration([{ ref: undefined, roleId: roleIdToApply }]) } } else { if (originalConfigurationType === 'project-scope') { - console.log('Reset: Project scoped') setProjectsRoleConfiguration(originalConfiguration) } else { setProjectsRoleConfiguration( diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx index ff05812f689..936ed8e0b8a 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.tsx @@ -36,6 +36,8 @@ import SecondLevelNav from './SecondLevelNav' const ProjectAPIDocs = () => { const { ref } = useParams() const snap = useAppStateSnapshot() + const isIntroduction = + snap.activeDocsSection.length === 1 && snap.activeDocsSection[0] === 'introduction' const isEntityDocs = snap.activeDocsSection.length === 2 && snap.activeDocsSection[0] === 'entities' @@ -76,9 +78,11 @@ const ProjectAPIDocs = () => {

API Docs

{!isEntityDocs && } - + {isIntroduction && ( + + )}
diff --git a/apps/studio/components/interfaces/Reports/Reports.tsx b/apps/studio/components/interfaces/Reports/Reports.tsx index d0c239a991b..8d7e598fcaa 100644 --- a/apps/studio/components/interfaces/Reports/Reports.tsx +++ b/apps/studio/components/interfaces/Reports/Reports.tsx @@ -482,7 +482,7 @@ const Reports = () => { )} ) : ( -
+
{config && startDate && endDate && ( void onHasSelection: (value: boolean) => void + onMount?: (editor: IStandaloneCodeEditor) => void onPrompt?: (value: { selection: string beforeSelection: string @@ -43,6 +44,7 @@ const MonacoEditor = ({ executeQuery, onHasSelection, onPrompt, + onMount, }: MonacoEditorProps) => { const router = useRouter() const { profile } = useProfile() @@ -143,6 +145,8 @@ const MonacoEditor = ({ if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 }) editor.focus() } + + onMount?.(editor) } const debouncedSetSql = debounce((id, value) => snapV2.setSql(id, value), 1000) diff --git a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx index 5956e1e1034..591f8a63c04 100644 --- a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx +++ b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx @@ -13,7 +13,7 @@ import type { SqlSnippet } from 'data/content/sql-snippets-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' -import { createTabId, renameTab } from 'state/tabs' +import { createTabId, updateTab } from 'state/tabs' import { AiIconAnimation, Button, Form, Input, Modal } from 'ui' import { useIsSQLEditorTabsEnabled } from '../App/FeaturePreview/FeaturePreviewContext' import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' @@ -111,7 +111,7 @@ const RenameQueryModal = ({ snapV2.renameSnippet({ id, name: nameInput, description: descriptionInput }) if (isSQLEditorTabsEnabled && ref) { const tabId = createTabId('sql', { id }) - renameTab(ref, tabId, nameInput) + updateTab(ref, tabId, { label: nameInput }) } toast.success('Successfully renamed snippet!') if (onComplete) onComplete() diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 8dc8ee9ef81..e518e86627f 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -36,6 +36,7 @@ import { useGetImpersonatedRoleState, } from 'state/role-impersonation-state' import { getSqlEditorV2StateSnapshot, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' +import { createTabId, getTabsStore, updateTab } from 'state/tabs' import { Button, DropdownMenu, @@ -51,6 +52,7 @@ import { TooltipTrigger, cn, } from 'ui' +import { useSnapshot } from 'valtio' import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' import { RunQueryWarningModal } from './RunQueryWarningModal' @@ -81,11 +83,14 @@ export const SQLEditor = () => { const os = detectOS() const router = useRouter() const { ref, id: urlId } = useParams() - const org = useSelectedOrganization() + const { profile } = useProfile() - const queryClient = useQueryClient() const project = useSelectedProject() - const organization = useSelectedOrganization() + const org = useSelectedOrganization() + + const queryClient = useQueryClient() + const store = getTabsStore(ref) + const tabs = useSnapshot(store) const aiSnap = useAiAssistantStateSnapshot() const snapV2 = useSqlEditorV2StateSnapshot() const getImpersonatedRoleState = useGetImpersonatedRoleState() @@ -111,6 +116,8 @@ export const SQLEditor = () => { const editorRef = useRef(null) const monacoRef = useRef(null) const diffEditorRef = useRef(null) + const scrollTopRef = useRef(0) + const [hasSelection, setHasSelection] = useState(false) const [lineHighlights, setLineHighlights] = useState([]) const [isDiffEditorMounted, setIsDiffEditorMounted] = useState(false) @@ -136,7 +143,7 @@ export const SQLEditor = () => { useAddDefinitions(id, monacoRef.current) /** React query data fetching */ - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery({ @@ -362,6 +369,19 @@ export const SQLEditor = () => { [profile?.id, project?.id, ref, router, snapV2] ) + const onMount = (editor: IStandaloneCodeEditor) => { + const tabId = createTabId('sql', { id }) + const tabData = tabs.tabsMap[tabId] + + // [Joshen] Tiny timeout to give a bit of time for the content to load before scrolling + setTimeout(() => { + if (tabData?.metadata?.scrollTop) { + editor.setScrollTop(tabData.metadata.scrollTop) + } + }, 20) + editor.onDidScrollChange((e) => (scrollTopRef.current = e.scrollTop)) + } + const onDebug = useCallback(async () => { try { const snippet = snapV2.snippets[id] @@ -511,6 +531,12 @@ export const SQLEditor = () => { closeDiff() setPromptState((prev) => ({ ...prev, isOpen: false })) } + return () => { + if (ref) { + const tabId = createTabId('sql', { id }) + updateTab(ref, tabId, { scrollTop: scrollTopRef.current }) + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [closeDiff, id]) @@ -718,6 +744,7 @@ export const SQLEditor = () => { monacoRef={monacoRef} executeQuery={executeQuery} onHasSelection={setHasSelection} + onMount={onMount} onPrompt={({ selection, beforeSelection, diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 5ac63e55595..16cc9ddf870 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -479,7 +479,7 @@ const Addons = () => {

+ +
+
+
+
+
+
+ +
+
+

+ Hi! I'm your AI assistant. How can I help you today? +

+
+
+
+
+

+ I can help you with database queries, API endpoints, or any other technical + questions you might have. +

+
+
+
+
+
+
+ + )} + + ) +} diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx index 71c6976699f..d1adafc13dd 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV2.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -58,16 +58,21 @@ const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive'] const CONTAINER_CLASSES = 'px-6' interface SupportFormV2Props { + onProjectSelected: (value: string) => void + onOrganizationSelected: (value: string) => void setSentCategory: (value: string) => void - setSelectedProject: (value: string) => void } // [Joshen] Just naming it as V2 for now for PR review purposes so its easier to view // This is a rewrite of the old SupportForm to use the new form components -export const SupportFormV2 = ({ setSentCategory, setSelectedProject }: SupportFormV2Props) => { +export const SupportFormV2 = ({ + onProjectSelected: setSelectedProject, + onOrganizationSelected: setSelectedOrganization, + setSentCategory, +}: SupportFormV2Props) => { const { profile } = useProfile() const { - ref, + projectRef: ref, slug, category: urlCategory, subject: urlSubject, @@ -295,6 +300,18 @@ export const SupportFormV2 = ({ setSentCategory, setSelectedProject }: SupportFo if (urlMessage) form.setValue('message', urlMessage) }, [urlMessage]) + // Sync organization selection with parent state + // Initialized as 'no-org' in parent if no org is selected + useEffect(() => { + setSelectedOrganization(organizationSlug) + }, [organizationSlug, setSelectedOrganization]) + + // Sync project selection with parent state + // Initialized as 'no-project' in parent if no project is selected + useEffect(() => { + setSelectedProject(projectRef) + }, [projectRef, setSelectedProject]) + return (
{ - return ( -
-
- } - title={`Unable to find your table with ID ${id}`} - /> -
-
- ) -} - -export default NotFoundState diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 38661ec6a5c..27d6664959e 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -25,7 +25,7 @@ import { getTables } from 'data/tables/tables-query' import { useUrlState } from 'hooks/ui/useUrlState' import { useGetImpersonatedRoleState } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { createTabId, renameTab } from 'state/tabs' +import { createTabId, updateTab } from 'state/tabs' import type { Dictionary } from 'types' import { SonnerProgress } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -489,7 +489,7 @@ const SidePanelEditor = ({ if (isTableEditorTabsEnabled && ref && payload.name) { // [Joshen] Only table entities can be updated via the dashboard const tabId = createTabId(ENTITY_TYPE.TABLE, { id: selectedTable.id }) - renameTab(ref, tabId, payload.name) + updateTab(ref, tabId, { label: payload.name }) } toast.success(`Successfully updated ${table.name}!`, { id: toastId }) } diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 7530dea458d..df3d071c0cb 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -6,6 +6,7 @@ import { useCallback } from 'react' import { useParams } from 'common' import { SupabaseGrid } from 'components/grid/SupabaseGrid' import { useLoadTableEditorStateFromLocalStorageIntoUrl } from 'components/grid/SupabaseGrid.utils' +import { useEditorType } from 'components/layouts/editors/EditorsLayout.hooks' import { Entity, isMaterializedView, @@ -16,10 +17,12 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' +import { useAppStateSnapshot } from 'state/app-state' import { TableEditorTableStateContextProvider } from 'state/table-editor-table' -import { makeActiveTabPermanent } from 'state/tabs' -import { GenericSkeletonLoader } from 'ui-patterns' -import NotFoundState from './NotFoundState' +import { handleTabClose, makeActiveTabPermanent, useTabsStore } from 'state/tabs' +import { Button } from 'ui' +import { Admonition, GenericSkeletonLoader } from 'ui-patterns' +import { useIsTableEditorTabsEnabled } from '../App/FeaturePreview/FeaturePreviewContext' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' import TableDefinition from './TableDefinition' @@ -28,13 +31,17 @@ export interface TableGridEditorProps { selectedTable?: Entity } -const TableGridEditor = ({ +export const TableGridEditor = ({ isLoadingSelectedTable = false, selectedTable, }: TableGridEditorProps) => { const router = useRouter() + const editor = useEditorType() const project = useSelectedProject() + const appSnap = useAppStateSnapshot() const { ref: projectRef, id } = useParams() + const tabs = useTabsStore(projectRef) + const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() useLoadTableEditorStateFromLocalStorageIntoUrl({ projectRef, @@ -46,6 +53,7 @@ const TableGridEditor = ({ const canEditTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const canEditColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') const isReadOnly = !canEditTables && !canEditColumns + const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined const onTableCreated = useCallback( (table: { id: number }) => { @@ -54,6 +62,12 @@ const TableGridEditor = ({ [projectRef, router] ) + const onClearDashboardHistory = () => { + if (projectRef && editor) { + appSnap.setDashboardHistory(projectRef, editor === 'table' ? 'editor' : editor, undefined) + } + } + // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isLoadingSelectedTable || !projectRef) { return ( @@ -68,7 +82,37 @@ const TableGridEditor = ({ } if (isUndefined(selectedTable)) { - return + return ( +
+
+ + {isTableEditorTabsEnabled && ( + + )} + +
+
+ ) } const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable) @@ -121,5 +165,3 @@ const TableGridEditor = ({ ) } - -export default TableGridEditor diff --git a/apps/studio/components/interfaces/TableGridEditor/index.ts b/apps/studio/components/interfaces/TableGridEditor/index.ts index 86131e01172..50e1839b986 100644 --- a/apps/studio/components/interfaces/TableGridEditor/index.ts +++ b/apps/studio/components/interfaces/TableGridEditor/index.ts @@ -1,3 +1,2 @@ -export { default as TableGridEditor } from './TableGridEditor' -export { default as SidePanelEditor } from './SidePanelEditor/SidePanelEditor' export { default as EmptyState } from './EmptyState' +export { default as SidePanelEditor } from './SidePanelEditor/SidePanelEditor' diff --git a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index 13c659f29ec..ac094bfc606 100644 --- a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -62,7 +62,7 @@ const OrganizationDropdown = () => { {newLayoutPreview ? (