diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index d56648eb1f263..2a5086cd2d922 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -181,6 +181,8 @@ Line numbers are 1-indexed and inclusive. lines={[[1, 3], [5, -1]]} {/* Optional, displays as a file name on the code block */} meta="name=display/path.js" +{/* Optional, strips TypeScript types to produce JavaScript, which you also include with another */} +convertToJs={true} /> ``` @@ -195,11 +197,31 @@ commit="1623aa9b95ec90e21c5bae5a0d50dcf272abe92f" path="/relative/path/from/root.js" lines={[[1, 3], [5, -1]]} meta="name=display/path.js" +convertToJs={true} /> ``` The repo must be public, the org must be on the allow list, and the commit must be an immutable SHA (not a mutable tag or branch name). +#### Converting TypeScript to JavaScript + +You can automatically strip TypeScript types from code samples to produce JavaScript using the `convertToJs` option: + +```mdx +<$CodeSample +path="/path/to/typescript-file.ts" +lines={[[1, -1]]} +convertToJs={true} +/> +``` + +This is useful when you want to show JavaScript examples from TypeScript source files. The conversion: + +- Removes all TypeScript type annotations, interfaces, and type definitions +- Converts the language identifier from `typescript` to `javascript` (or `tsx` to `jsx`) +- Happens before any line selection or elision processing +- Defaults to `false` to preserve TypeScript code when not specified + #### Multi-file code samples Multi-file code samples use the `<$CodeTabs>` annotation: diff --git a/apps/docs/content/guides/platform/sso.mdx b/apps/docs/content/guides/platform/sso.mdx index a4df878167814..6553a1f39f2ff 100644 --- a/apps/docs/content/guides/platform/sso.mdx +++ b/apps/docs/content/guides/platform/sso.mdx @@ -13,11 +13,11 @@ Supabase offers single sign-on (SSO) as a login option to provide additional acc -Supabase currently provides SAML SSO for [Team and Enterprise Plan customers](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. Once completed, [contact us](https://supabase.com/dashboard/support/new?category=Login_issues&subject=Enquiry%20about%20setting%20up%20SSO&message=I%20would%20like%20to%20set%20up%20SAML%20SSO%20for%20my%20team%20and%20have%20followed%20https://supabase.com/docs/guides/platform/sso%20and%20configured%20my%20provider%20%0A%0APlease%20attach%20the%20IDP%20metadata%20in%20the%20attachments%20below) to enable SSO for your team. +Supabase currently provides SAML SSO for [Team and Enterprise Plan customers](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. -## Setup and limitations +## Supported providers Supabase supports practically all identity providers that support the SAML 2.0 SSO protocol. We've prepared these guides for commonly used identity providers to help you get started. If you use a different provider, our support stands ready to support you. @@ -25,18 +25,33 @@ Supabase supports practically all identity providers that support the SAML 2.0 S - [Azure Active Directory](/docs/guides/platform/sso/azure) - [Okta](/docs/guides/platform/sso/okta) -Accounts signing in with SSO have certain limitations. -The following sections outline the limitations when SSO is enabled or disabled for your team. +Once configured, you can update your settings anytime via the [SSO tab](https://supabase.com/dashboard/org/_/sso) under **Organization Settings**. -### Enable SSO for your team [#enable-sso] +![SSO Example](/docs/img/sso-dashboard-enabled.png) + +## Key configuration options + +- **Multiple domains** - You can associate one or more email domains with your SSO provider. Users with email addresses matching these domains are eligible to sign in via SSO. +- **Auto-join** - Optionally allow users with a matching domain to be added to your organization automatically when they first sign in, without an invitation. +- **Default role for auto-joined users** - Choose the role (e.g., `Read-only`, `Developer`, `Administrator`, `Owner`) that automatically joined users receive. Refer to [access control](/docs/guides/platform/access-control) for more information about roles. + +## How SSO works in Supabase + +When SSO is enabled for an organization: - Organization invites are restricted to company members belonging to the same identity provider. - Every user has an organization created by default. They can create as many projects as they want. - An SSO user will not be able to update or reset their password since the company administrator manages their access via the identity provider. - If an SSO user with the following email of `alice@foocorp.com` attempts to sign in with a GitHub account that uses the same email, a separate Supabase account is created and will not be linked to the SSO user's account. -- An SSO user will not be able to see all organizations/projects created under the same identity provider. They will need to be invited to the Supabase organization first. Refer to [access control](/docs/guides/platform/access-control) for more information. +- SSO users will only see organizations/projects they've been invited to or auto-joined into. See [access control](/docs/guides/platform/access-control) for more details. + +## Disabling SSO for an organization + +If you disable the SSO provider for an organization, **all SSO users will immediately be unable to sign in**. Before disabling SSO, ensure you have at least one non-SSO owner account to prevent being locked out. + +## Removing an individual SSO user's access -### Disable SSO for your team [#disable-sso] +To revoke access for a specific SSO user without disabling the provider entirely you may: -- You can prevent a user's account from further access to Supabase by removing or disabling their account in your identity provider. -- You should also remove or downgrade their permissions from any organizations inside Supabase. +- Remove or disable the user's account in your identity provider +- Downgrade or remove their permissions for any organizations in Supabase. diff --git a/apps/docs/content/guides/platform/sso/azure.mdx b/apps/docs/content/guides/platform/sso/azure.mdx index 3f3e93888ecc3..1ced76f50ddd4 100644 --- a/apps/docs/content/guides/platform/sso/azure.mdx +++ b/apps/docs/content/guides/platform/sso/azure.mdx @@ -5,7 +5,7 @@ description: 'Configure single sign-on with Azure AD (Microsoft Entra).' -This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. Once completed, [contact us](https://supabase.com/dashboard/support/new?category=Login_issues&subject=Enquiry%20about%20setting%20up%20SSO&message=I%20would%20like%20to%20set%20up%20SAML%20SSO%20for%20my%20team%20and%20have%20followed%20https://supabase.com/docs/guides/platform/sso%20and%20configured%20my%20provider%20%0A%0APlease%20attach%20the%20IDP%20metadata%20in%20the%20attachments%20below) to enable SSO for your team. +This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. @@ -33,7 +33,7 @@ You'll be using the custom enterprise application setup for Supabase. ## Step 3: Fill in application details [#add-application-details] -In the modal titled _Create your own application_, enter a display name for Supabase. This is the name your Azure AD users see when signing in to Supabase from Azure. `Supabase` works in most cases. +In the modal titled _Create your own application_, enter a display name for Supabase. This is the name your Azure AD users will see when signing in to Supabase from Azure. `Supabase` works in most cases. Make sure to choose the third option: _Integrate any other application you don't find in the gallery (Non-gallery)_. @@ -81,24 +81,73 @@ All of the correct information should automatically populate the _Basic SAML Con Finally, click the _Save_ button to save the configuration. -## Step 7: Obtain metadata URL and send to Supabase [#send-metadata-url] +## Step 7: Obtain metadata URL [#idp-metadata-url] -Supabase needs to finalize enabling single sign-on with your Azure AD application. To do this, copy and send the link under **App Federation Metadata URL** in \*section 3 **SAML Certificates\*** to your support contact and await further instructions. If you're not clear who to send this link to or need further assistance, reach out to [Supabase Support](https://supabase.help). - -**Do not test the login until you have heard back from the support contact.** +Save the link under **App Federation Metadata URL** in \*section 3 **SAML Certificates\***. You will need to enter this URL later in [Step 10](#dashboard-configure-metadata). ![Azure AD console: Supabase application, SAML Certificates card shown, App Federation Metadata Url highlighted](/docs/img/sso-azure-step-07.png) -## Step 8: Wait for confirmation [#confirmation] +## Step 8: Enable SSO in the Dashboard [#dashboard-enable-sso] + +1. Visit the [SSO tab](https://supabase.com/dashboard/org/_/sso) under the Organization Settings page. ![SSO disabled](/docs/img/sso-dashboard-disabled.png) + +2. Toggle **Enable Single Sign-On** to begin configuration. Once enabled, the configuration form appears. ![SSO enabled](/docs/img/sso-dashboard-enabled.png) + +## Step 9: Configure domains [#dashboard-configure-domain] + +Enter one or more domains associated with your users email addresses (e.g., `supabase.com`). +These domains determine which users are eligible to sign in via SSO. + +![Domain configuration](/docs/img/sso-dashboard-configure-domain.png) + +If your organization uses more than one email domain - for example, `supabase.com` for staff and `supabase.io` for contractors - you can add multiple domains here. All listed domains will be authorized for SSO sign-in. + +![Domain configuration with multiple domains](/docs/img/sso-dashboard-configure-domain-multi.png) + + + +We do not permit use of public domains like `gmail.com`, `yahoo.com`. + + + +## Step 10: Configure metadata [#dashboard-configure-metadata] + +Enter the metadata URL you obtained from [Step 7](#idp-metadata-url) into the Metadata URL field: -Wait for confirmation or further instructions from your support contact at Supabase before proceeding to the next step. It usually takes us 1 business day to configure SSO for you. +![Metadata configuration with Azure AD](/docs/img/sso-dashboard-configure-metadata-azure.png) -## Step 9: Test single sign-on [#testing] +## Step 11: Configure attribute mapping [#dashboard-configure-attributes] + +Fill out the Attribute Mapping section using the **Azure** preset. + +![Attribute mapping configuration](/docs/img/sso-dashboard-configure-attributes-azure.png) + +## Step 12: Join organization on signup (optional) [#dashboard-configure-autojoin] + +By default this setting is disabled, users logging in via SSO will not be added to your organization automatically. + +![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) + +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. + +![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) + +When auto-join is enabled, you can choose the **default role** for new users: + +![Auto-join role selection](/docs/img/sso-dashboard-configure-autojoin-enabled-role.png) + +Choose a role that fits the level of access you want to grant to new members. + + + +Visit [access-control](/docs/guides/platform/access-control) documentation for details about each role. + + -_Testing sign-on before your Azure AD has been registered with Supabase will not work. Make sure you've received confirmation from your support contact at Supabase as laid out in the [confirmation](#confirmation) step._ +## Step 13: Save changes and test single sign-on [#dashboard-configure-save] -Once you’ve received confirmation from your support contact at Supabase that SSO setup has been completed for your enterprise, you can ask some of your users to sign in via their Azure AD account. +When you click **Save changes**, your new SSO configuration is applied immediately. From that moment, any user with an email address matching one of your configured domains who visits your organization's sign-in URL will be routed through the SSO flow. -You ask them to enter their email address on the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. +We recommend asking a few users to test signing in via their Azure AD account. They can do this by entering their email address on the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. -If sign in is not working correctly, reach out to your support contact at Supabase for further guidance. +If SSO sign-in doesn't work as expected, contact your Supabase support representative for assistance. diff --git a/apps/docs/content/guides/platform/sso/gsuite.mdx b/apps/docs/content/guides/platform/sso/gsuite.mdx index 2eed8215f20a3..cfa2960348597 100644 --- a/apps/docs/content/guides/platform/sso/gsuite.mdx +++ b/apps/docs/content/guides/platform/sso/gsuite.mdx @@ -5,7 +5,7 @@ description: 'Configure single sign-on with Google Workspace (G Suite).' -This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. Once completed, [contact us](https://supabase.com/dashboard/support/new?category=Login_issues&subject=Enquiry%20about%20setting%20up%20SSO&message=I%20would%20like%20to%20set%20up%20SAML%20SSO%20for%20my%20team%20and%20have%20followed%20https://supabase.com/docs/guides/platform/sso%20and%20configured%20my%20provider%20%0A%0APlease%20attach%20the%20IDP%20metadata%20in%20the%20attachments%20below) to enable SSO for your team. +This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. @@ -35,12 +35,10 @@ The information you enter here is for visibility into your Google Workspace. You ## Step 4: Download IdP metadata [#download-idp-metadata] -This is a very important step. Click on _DOWNLOAD METADATA_ and save the file that was downloaded. +This is a very important step. Click on _DOWNLOAD METADATA_ and save the file that was downloaded. You will need to upload this file later in [Step 10](#dashboard-configure-metadata). ![Google Workspace: Web and mobile apps admin console, Add custom SAML, Google Identity Provider details screen](/docs/img/sso-gsuite-step-04.png) -It's very important to send this file to your support contact at Supabase to complete the SSO setup process. If you're not sure where to send this file, you can always reach out to [Supabase Support](https://supabase.help). - **Important: Make sure the certificate as shown on screen has at least 1 year before it expires. Mark down this date in your calendar so you will be reminded that you need to update the certificate without any downtime for your users.** ## Step 5: Add service provider details [#add-service-provider-details] @@ -63,34 +61,91 @@ Attribute mappings allow Supabase to get information about your Google Workspace **A _Primary email_ to `email` mapping is required.** Other mappings shown below are optional and configurable depending on your Google Workspace setup. If in doubt, replicate the same config as shown. -Share any changes, if any, from this screen with your Supabase support contact. +Any changes you make from this screen will be used later in [Step 10: Configure Attribute Mapping](#dashboard-configure-attributes). ![Google Workspace: Web and mobile apps admin console, Add custom SAML, Attribute mapping](/docs/img/sso-gsuite-step-06.png) -## Step 7: Wait for confirmation [#confirmation] +## Step 7: Configure user access [#configure-user-access] -Once you’ve configured the Google Workspace app as shown above, make sure you send the [metadata file you downloaded](#download-idp-metadata) and information regarding the [attribute mapping](#configure-attribute-mappings) (if any changes are applicable) to your support contact at Supabase. +You can configure which Google Workspace user accounts will get access to Supabase. This is important if you wish to limit access to your software engineering teams. -This information needs to be entered into Supabase before SSO is activated end-to-end. +You can configure this access by clicking on the _User access_ card (or down-arrow). Follow the instructions on screen. -Wait for confirmation that this information has successfully been added to Supabase. It usually takes us 1 business day to configure this information for you. +![Google Workspace: Web and mobile apps admin console, Supabase app screen](/docs/img/sso-gsuite-step-08.png) -## Step 8: Configure user access [#configure-user-access] + -You can configure which Google Workspace user accounts will get access to Supabase. This is important if you wish to limit access to your software engineering teams. +Changes from this step sometimes take a while to propagate across Google's systems. Wait at least 15 minutes before testing your changes. -You can configure this access by clicking on the _User access_ card (or down-arrow). Follow the instructions on screen. + -Changes from this step sometimes take a while to propagate across Google’s systems. Wait at least 15 minutes before proceeding to the next step. +## Step 8: Enable SSO in the Dashboard [#dashboard-enable-sso] -![Google Workspace: Web and mobile apps admin console, Supabase app screen](/docs/img/sso-gsuite-step-08.png) +1. Visit the [SSO tab](https://supabase.com/dashboard/org/_/sso) under the Organization Settings page. ![SSO disabled](/docs/img/sso-dashboard-disabled.png) + +2. Toggle **Enable Single Sign-On** to begin configuration. Once enabled, the configuration form appears. ![SSO enabled](/docs/img/sso-dashboard-enabled.png) + +## Step 9: Configure domains [#dashboard-configure-domain] + +Enter one or more domains associated with your users email addresses (e.g., `supabase.com`). +These domains determine which users are eligible to sign in via SSO. + +![Domain configuration](/docs/img/sso-dashboard-configure-domain.png) + +If your organization uses more than one email domain - for example, `supabase.com` for staff and `supabase.io` for contractors - you can add multiple domains here. All listed domains will be authorized for SSO sign-in. + +![Domain configuration with multiple domains](/docs/img/sso-dashboard-configure-domain-multi.png) + + + +We do not permit use of public domains like `gmail.com`, `yahoo.com`. + + + +## Step 10: Configure metadata [#dashboard-configure-metadata] + +Upload the metadata file you downloaded in [Step 6](#download-idp-metadata) into the Metadata Upload File field. -## Step 9: Test single sign-on [#testing] +![Metadata configuration with Google Workspace](/docs/img/sso-dashboard-configure-metadata-gsuite.png) + +## Step 11: Configure attribute mapping [#dashboard-configure-attributes] + +Enter the SAML attributes you filled out in [Step 6](#configure-attribute-mapping) into the Attribute Mapping section. + +![Attribute mapping configuration](/docs/img/sso-dashboard-configure-attributes-generic.png) + + + +If you did not customize your settings you may save some time by clicking the **G Suite** preset. + + + +## Step 12: Join organization on signup (optional) [#dashboard-configure-autojoin] + +By default this setting is disabled, users logging in via SSO will not be added to your organization automatically. + +![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) + +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. + +![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) + +When auto-join is enabled, you can choose the **default role** for new users: + +![Auto-join role selection](/docs/img/sso-dashboard-configure-autojoin-enabled-role.png) + +Choose a role that fits the level of access you want to grant to new members. + + + +Visit [access-control](/docs/guides/platform/access-control) documentation for details about each role. + + -Once you’ve turned on access to Supabase for users in your organization, ask one of those users to help you out in testing the setup. +## Step 13: Save changes and test single sign-on [#dashboard-configure-save] -It often helps to ask them to log out of their Google account and log back in. +When you click **Save changes**, your new SSO configuration is applied immediately. From that moment, any user with an email address matching one of your configured domains who visits your organization's sign-in URL will be routed through the SSO flow. -Ask them to enter their email address in the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. +We recommend asking a few users to test signing in via their Google Workspace account. They can do this by entering their email address on the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. -If sign in is not working correctly, reach out to your support contact at Supabase. +If SSO sign-in doesn't work as expected, contact your Supabase support representative for assistance. diff --git a/apps/docs/content/guides/platform/sso/okta.mdx b/apps/docs/content/guides/platform/sso/okta.mdx index d86e95d6711bd..22bf180e98785 100644 --- a/apps/docs/content/guides/platform/sso/okta.mdx +++ b/apps/docs/content/guides/platform/sso/okta.mdx @@ -5,7 +5,7 @@ description: 'Configure single sign-on with Okta.' -This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. Once completed, [contact us](https://supabase.com/dashboard/support/new?category=Login_issues&subject=Enquiry%20about%20setting%20up%20SSO&message=I%20would%20like%20to%20set%20up%20SAML%20SSO%20for%20my%20team%20and%20have%20followed%20https://supabase.com/docs/guides/platform/sso%20and%20configured%20my%20provider%20%0A%0APlease%20attach%20the%20IDP%20metadata%20in%20the%20attachments%20below) to enable SSO for your team. +This feature is only available on the [Team and Enterprise Plans](https://supabase.com/pricing). If you are an existing Team or Enterprise Plan customer, continue with the setup below. @@ -37,7 +37,7 @@ The information you enter here is for visibility into your Okta applications men ## Step 4: Fill out SAML settings [#add-saml-settings] -These settings let Supabase use SAML 2.0 properly with your Okta application. Make sure you enter this information exactly as shown on in this table and screenshot. +These settings let Supabase use SAML 2.0 properly with your Okta application. Make sure you enter this information exactly as shown on in this table. | Setting | Value | | ---------------------------------------------- | --------------------------------------------------- | @@ -55,9 +55,7 @@ These settings let Supabase use SAML 2.0 properly with your Okta application. Ma Attribute Statements allow Supabase to get information about your Okta users on each login. -**A `email` to `user.email` statement is required.** Other mappings shown below are optional and configurable depending on your Okta setup. If in doubt, replicate the same config as shown. - -Share any changes, if any, from this screen with your Supabase support contact. +**A `email` to `user.email` statement is required.** Other mappings shown below are optional and configurable depending on your Okta setup. If in doubt, replicate the same config as shown. You will use this mapping later in [Step 10](#dashboard-configure-attributes). ![Okta dashboard: Attribute Statements configuration screen](/docs/img/sso-okta-step-05.png) @@ -67,22 +65,79 @@ Supabase needs to finalize enabling single sign-on with your Okta application. To do this scroll down to the _SAML Signing Certificates_ section on the _Sign On_ tab of the _Supabase_ application. Pick the the _SHA-2_ row with an _Active_ status. Click on the _Actions_ dropdown button and then on the _View IdP Metadata_. -This will open up the SAML 2.0 Metadata XML file in a new tab in your browser. Copy this URL and send it to your support contact and await further instructions. If you're not clear who to send this link to or need further assistance, contact [Supabase Support](https://supabase.help). +This will open up the SAML 2.0 Metadata XML file in a new tab in your browser. You will need to enter this URL later in [Step 9](#dashboard-configure-metadata). The link usually has this structure: `https://.okta.com/apps//sso/saml/metadata` ![Okta dashboard: SAML Signing Certificates, Actions button highlighted](/docs/img/sso-okta-step-06.png) -## Step 7: Wait for confirmation [#confirmation] +## Step 7: Enable SSO in the Dashboard [#dashboard-enable-sso] + +1. Visit the [SSO tab](https://supabase.com/dashboard/org/_/sso) under the Organization Settings page. ![SSO disabled](/docs/img/sso-dashboard-disabled.png) + +2. Toggle **Enable Single Sign-On** to begin configuration. Once enabled, the configuration form appears. ![SSO enabled](/docs/img/sso-dashboard-enabled.png) + +## Step 8: Configure domains [#dashboard-configure-domain] + +Enter one or more domains associated with your users email addresses (e.g., `supabase.com`). +These domains determine which users are eligible to sign in via SSO. + +![Domain configuration](/docs/img/sso-dashboard-configure-domain.png) + +If your organization uses more than one email domain - for example, `supabase.com` for staff and `supabase.io` for contractors - you can add multiple domains here. All listed domains will be authorized for SSO sign-in. + +![Domain configuration with multiple domains](/docs/img/sso-dashboard-configure-domain-multi.png) + + + +We do not permit use of public domains like `gmail.com`, `yahoo.com`. + + + +## Step 9: Configure metadata [#dashboard-configure-metadata] + +Enter the metadata URL you obtained from [Step 6](#idp-metadata-url) into the Metadata URL field: + +![Metadata configuration with Okta](/docs/img/sso-dashboard-configure-metadata-okta.png) + +## Step 10: Configure attribute mapping [#dashboard-configure-attributes] + +Enter the SAML attributes you filled out in [Step 5](#add-attribute-statements) into the Attribute Mapping section. -Once you’ve configured the Okta app as shown above, make sure you send the [metadata URL](#idp-metadata-url) and information regarding the [attribute statements](#add-attribute-statements) (if any changes are applicable) to your support contact at Supabase. +![Attribute mapping configuration](/docs/img/sso-dashboard-configure-attributes.png) -Wait for confirmation that this information has successfully been added to Supabase. It usually takes us 1 business day to configure this information for you. + + +If you did not customize your settings you may save some time by clicking the **Okta** preset. + + + +## Step 11: Join organization on signup (optional) [#dashboard-configure-autojoin] + +By default this setting is disabled, users logging in via SSO will not be added to your organization automatically. + +![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) + +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. + +![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) + +When auto-join is enabled, you can choose the **default role** for new users: + +![Auto-join role selection](/docs/img/sso-dashboard-configure-autojoin-enabled-role.png) + +Choose a role that fits the level of access you want to grant to new members. + + + +Visit [access-control](/docs/guides/platform/access-control) documentation for details about each role. + + -## Step 8: Test single sign-on [#testing] +## Step 12: Save changes and test single sign-on [#dashboard-configure-save] -Once you’ve received confirmation from your support contact at Supabase that SSO setup has been completed for your enterprise, you can ask some of your users to sign in via their Okta account. +When you click **Save changes**, your new SSO configuration is applied immediately. From that moment, any user with an email address matching one of your configured domains who visits your organization's sign-in URL will be routed through the SSO flow. -You ask them to enter their email address on the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. +We recommend asking a few users to test signing in via their Okta account. They can do this by entering their email address on the [Sign in with SSO](https://supabase.com/dashboard/sign-in-sso) page. -If sign in is not working correctly, reach out to your support contact at Supabase for further guidance. +If SSO sign-in doesn't work as expected, contact your Supabase support representative for assistance. diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 63b3ace9babbf..f6c2f3089b370 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -11,7 +11,6 @@ import { QueryClientProvider } from './data/queryClient.client' import { PageTelemetry } from './telemetry/telemetry.client' import { ScrollRestoration } from './ui/helpers.scroll.client' import { ThemeSandbox } from './ui/theme.client' -import { PromoToast } from 'ui-patterns' /** * Global providers that wrap the entire app @@ -28,7 +27,6 @@ function GlobalProviders({ children }: PropsWithChildren) {
- {children} diff --git a/apps/docs/features/directives/CodeSample.test.ts b/apps/docs/features/directives/CodeSample.test.ts index a3ae68b0e1ec2..3c8e83df2fb3f 100644 --- a/apps/docs/features/directives/CodeSample.test.ts +++ b/apps/docs/features/directives/CodeSample.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, it, expect, vi } from 'vitest' +import { stripIndent } from 'common-tags' import { fromMarkdown } from 'mdast-util-from-markdown' import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx' import { toMarkdown } from 'mdast-util-to-markdown' @@ -18,6 +19,42 @@ vi.mock('~/lib/constants', () => ({ IS_PLATFORM: true, })) +/** + * Checks if str1 contains str2, ignoring leading whitespace on each line. + * Lines are matched if they have the same content after trimming leading whitespace. + * + * @param str1 - The string to search in + * @param str2 - The string to search for + * @returns true if str1 contains str2 modulo leading whitespace, false otherwise + */ +export function containsStringIgnoringLeadingWhitespace(str1: string, str2: string): boolean { + const lines1 = str1.split('\n').map((line) => line.trimStart()) + const lines2 = str2.split('\n').map((line) => line.trimStart()) + + if (lines2.length === 0) { + return true + } + + if (lines2.length > lines1.length) { + return false + } + + for (let i = 0; i <= lines1.length - lines2.length; i++) { + let matches = true + for (let j = 0; j < lines2.length; j++) { + if (lines1[i + j] !== lines2[j]) { + matches = false + break + } + } + if (matches) { + return true + } + } + + return false +} + describe('$CodeSample', () => { beforeAll(() => { env = process.env @@ -533,6 +570,169 @@ Some more text. expect(output).toEqual(expected) }) + + describe('convertToJs option', () => { + it('should convert TypeScript to JavaScript when convertToJs is true', async () => { + const markdown = ` +# Embed code sample + +<$CodeSample path="/_internal/fixtures/typescript.ts" lines={[[1, -1]]} convertToJs={true} /> + +Some more text. +`.trim() + + const mdast = fromMarkdown(markdown, { + mdastExtensions: [mdxFromMarkdown()], + extensions: [mdxjs()], + }) + const transformed = await transformWithMock(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = stripIndent` + \`\`\`javascript + const users = [ + { id: 1, name: 'John', email: 'john@example.com' }, + { id: 2, name: 'Jane' }, + ]; + + function getUserById(id) { + return users.find((user) => user.id === id); + } + + function createUser(name, email) { + const newId = Math.max(...users.map((u) => u.id)) + 1; + const newUser = { id: newId, name }; + if (email) { + newUser.email = email; + } + users.push(newUser); + return newUser; + } + + class UserManager { + users = []; + + constructor(initialUsers = []) { + this.users = initialUsers; + } + + addUser(user) { + this.users.push(user); + } + + getUsers() { + return [...this.users]; + } + } + \`\`\` + `.trim() + + expect(containsStringIgnoringLeadingWhitespace(output, expected)).toBe(true) + }) + + it('should preserve TypeScript when convertToJs is false', async () => { + const markdown = ` +# Embed code sample + +<$CodeSample path="/_internal/fixtures/typescript.ts" lines={[[1, -1]]} convertToJs={false} /> + +Some more text. +`.trim() + + const mdast = fromMarkdown(markdown, { + mdastExtensions: [mdxFromMarkdown()], + extensions: [mdxjs()], + }) + const transformed = await transformWithMock(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + // The output should contain TypeScript types + expect(output).toContain('```typescript') + expect(output).toContain('interface User') + expect(output).toContain('type Status') + expect(output).toContain(': User') + expect(output).toContain(': number') + expect(output).toContain(': string') + }) + + it('should preserve TypeScript when convertToJs is not specified (default)', async () => { + const markdown = ` +# Embed code sample + +<$CodeSample path="/_internal/fixtures/typescript.ts" lines={[[1, -1]]} /> + +Some more text. +`.trim() + + const mdast = fromMarkdown(markdown, { + mdastExtensions: [mdxFromMarkdown()], + extensions: [mdxjs()], + }) + const transformed = await transformWithMock(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + // The output should contain TypeScript types by default + expect(output).toContain('```typescript') + expect(output).toContain('interface User') + expect(output).toContain('type Status') + }) + + it('should convert types but preserve line selection and elision', async () => { + const markdown = ` +# Embed code sample + +<$CodeSample path="/_internal/fixtures/typescript.ts" lines={[[1, 4], [10, -1]]} convertToJs={true} /> + +Some more text. +`.trim() + + const mdast = fromMarkdown(markdown, { + mdastExtensions: [mdxFromMarkdown()], + extensions: [mdxjs()], + }) + const transformed = await transformWithMock(mdast) + const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] }) + + const expected = ` + \`\`\`javascript + const users = [ + { id: 1, name: 'John', email: 'john@example.com' }, + { id: 2, name: 'Jane' }, + ]; + + // ... + + function createUser(name, email) { + const newId = Math.max(...users.map((u) => u.id)) + 1; + const newUser = { id: newId, name }; + if (email) { + newUser.email = email; + } + users.push(newUser); + return newUser; + } + + class UserManager { + users = []; + + constructor(initialUsers = []) { + this.users = initialUsers; + } + + addUser(user) { + this.users.push(user); + } + + getUsers() { + return [...this.users]; + } + } + \`\`\` + `.trim() + + expect(containsStringIgnoringLeadingWhitespace(output, expected)).toBe(true) + }) + }) }) describe('_createElidedLine', () => { diff --git a/apps/docs/features/directives/CodeSample.ts b/apps/docs/features/directives/CodeSample.ts index d962ca8486a63..dc505c0bda1ba 100644 --- a/apps/docs/features/directives/CodeSample.ts +++ b/apps/docs/features/directives/CodeSample.ts @@ -11,6 +11,7 @@ * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block * hideElidedLines={true} // Optional, for hiding elided lines in the code block + * convertToJs={true} // Optional, strips TypeScript types to produce JavaScript * /> * ``` * @@ -26,6 +27,7 @@ * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block * hideElidedLines={true} // Optional, for hiding elided lines in the code block + * convertToJs={true} // Optional, strips TypeScript types to produce JavaScript * /> */ @@ -33,8 +35,10 @@ import * as acorn from 'acorn' import tsPlugin from 'acorn-typescript' import { type DefinitionContent, type BlockContent, type Code, type Root } from 'mdast' import type { MdxJsxAttributeValueExpression, MdxJsxFlowElement } from 'mdast-util-mdx-jsx' +import assert from 'node:assert' import { readFile } from 'node:fs/promises' import { join } from 'node:path' +import { removeTypes } from 'remove-types' import { type Parent } from 'unist' import { visitParents } from 'unist-util-visit-parents' import { z, type SafeParseError } from 'zod' @@ -69,6 +73,12 @@ type AdditionalMeta = { codeHikeAncestorParent: Parent | null } +const booleanValidator = z.union([z.boolean(), z.string(), z.undefined()]).transform((v) => { + if (typeof v === 'boolean') return v + if (typeof v === 'string') return v === 'true' + return false +}) + const codeSampleExternalSchema = z.object({ external: z.coerce.boolean().refine((v) => v === true), org: z.enum(ALLOW_LISTED_GITHUB_ORGS, { @@ -80,6 +90,7 @@ const codeSampleExternalSchema = z.object({ lines: linesValidator, meta: z.string().optional(), hideElidedLines: z.coerce.boolean().default(false), + convertToJs: booleanValidator, }) type ICodeSampleExternal = z.infer & AdditionalMeta @@ -92,6 +103,7 @@ const codeSampleInternalSchema = z.object({ lines: linesValidator, meta: z.string().optional(), hideElidedLines: z.coerce.boolean().default(false), + convertToJs: booleanValidator, }) type ICodeSampleInternal = z.infer & AdditionalMeta @@ -114,7 +126,7 @@ interface Dependencies { export function codeSampleRemark(deps: Dependencies) { return async function transform(tree: Root) { const contentMap = await fetchSourceCodeContent(tree, deps) - rewriteNodes(contentMap) + await rewriteNodes(contentMap) return tree } @@ -154,6 +166,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const hideElidedLines = getAttributeValueExpression( getAttributeValue(node, 'hideElidedLines') ) + const convertToJs = getAttributeValueExpression(getAttributeValue(node, 'convertToJs')) const result = codeSampleExternalSchema.safeParse({ external: isExternal, @@ -164,6 +177,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { lines, meta, hideElidedLines, + convertToJs, }) if (!result.success) { @@ -197,6 +211,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const hideElidedLines = getAttributeValueExpression( getAttributeValue(node, 'hideElidedLines') ) + const convertToJs = getAttributeValueExpression(getAttributeValue(node, 'convertToJs')) const result = codeSampleInternalSchema.safeParse({ external: isExternal, @@ -204,6 +219,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { lines, meta, hideElidedLines, + convertToJs, }) if (!result.success) { @@ -234,15 +250,30 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { return nodeContentMap } -function rewriteNodes(contentMap: Map) { +async function rewriteNodes(contentMap: Map) { for (const [node, [meta, content]] of contentMap) { - const lang = matchLang(meta.path.split('.').pop() || '') + let lang = matchLang(meta.path.split('.').pop() || '') const source = isExternalSource(meta) ? `https://github.com/${meta.org}/${meta.repo}/blob/${meta.commit}${meta.path}` : `https://github.com/supabase/supabase/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA ?? 'master'}/examples${meta.path}` - const elidedContent = redactLines(content, meta.lines, lang, meta.hideElidedLines) + let processedContent = content + if (meta.convertToJs) { + processedContent = await removeTypes(content) + // Convert TypeScript/TSX language to JavaScript/JSX when converting types + assert( + lang === 'typescript' || lang === 'tsx', + 'Type stripping to JS is only supported for TypeScript and TSX' + ) + if (lang === 'typescript') { + lang = 'javascript' + } else if (lang === 'tsx') { + lang = 'jsx' + } + } + + const elidedContent = redactLines(processedContent, meta.lines, lang, meta.hideElidedLines) const replacementContent: MdxJsxFlowElement | Code = meta.codeHikeAncestor ? { diff --git a/apps/docs/package.json b/apps/docs/package.json index 96c1cdb065a5c..c3a310ff02699 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -107,6 +107,7 @@ "remark-emoji": "^3.1.2", "remark-gfm": "^3.0.1", "remark-math": "^6.0.0", + "remove-types": "1.0.0", "server-only": "^0.0.1", "shared-data": "workspace:*", "ui": "workspace:*", diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 5024bf43949fa..1d2fe38b68bdf 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -97,6 +97,7 @@ Pavel Borisov Paweł Gulbinowicz Peter Lyn Qiao Han +Rafael Chacón Raminder Singh Riccardo Busetti Rodrigo Mansueli diff --git a/apps/docs/public/img/sso-dashboard-configure-attributes-azure.png b/apps/docs/public/img/sso-dashboard-configure-attributes-azure.png new file mode 100644 index 0000000000000..09fa6ebb70eef Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-attributes-azure.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-attributes-generic.png b/apps/docs/public/img/sso-dashboard-configure-attributes-generic.png new file mode 100644 index 0000000000000..761a6ecc42724 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-attributes-generic.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-attributes.png b/apps/docs/public/img/sso-dashboard-configure-attributes.png new file mode 100644 index 0000000000000..d7b2749cc389d Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-attributes.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-autojoin-disabled.png b/apps/docs/public/img/sso-dashboard-configure-autojoin-disabled.png new file mode 100644 index 0000000000000..513b42d86c9d4 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-autojoin-disabled.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled-role.png b/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled-role.png new file mode 100644 index 0000000000000..fc0e8e8bc8e0b Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled-role.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled.png b/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled.png new file mode 100644 index 0000000000000..4a9c3e2e2d33c Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-autojoin-enabled.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-domain-multi.png b/apps/docs/public/img/sso-dashboard-configure-domain-multi.png new file mode 100644 index 0000000000000..c837a27161979 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-domain-multi.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-domain.png b/apps/docs/public/img/sso-dashboard-configure-domain.png new file mode 100644 index 0000000000000..f7ca8ca955911 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-domain.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-metadata-azure.png b/apps/docs/public/img/sso-dashboard-configure-metadata-azure.png new file mode 100644 index 0000000000000..e81e5b1b6a7bf Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-metadata-azure.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-metadata-gsuite.png b/apps/docs/public/img/sso-dashboard-configure-metadata-gsuite.png new file mode 100644 index 0000000000000..675dc596da46d Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-metadata-gsuite.png differ diff --git a/apps/docs/public/img/sso-dashboard-configure-metadata-okta.png b/apps/docs/public/img/sso-dashboard-configure-metadata-okta.png new file mode 100644 index 0000000000000..5a633113abbb5 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-configure-metadata-okta.png differ diff --git a/apps/docs/public/img/sso-dashboard-disabled.png b/apps/docs/public/img/sso-dashboard-disabled.png new file mode 100644 index 0000000000000..0a5df2efc10fd Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-disabled.png differ diff --git a/apps/docs/public/img/sso-dashboard-enabled.png b/apps/docs/public/img/sso-dashboard-enabled.png new file mode 100644 index 0000000000000..9a911494b5464 Binary files /dev/null and b/apps/docs/public/img/sso-dashboard-enabled.png differ diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.constants.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.constants.ts index 41bf5a5864478..505a373fa6f9c 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.constants.ts +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.constants.ts @@ -12,7 +12,7 @@ export const INVOCATION_TABS: InvocationTab[] = [ label: 'cURL', language: 'bash', code: (functionUrl, _, apiKey) => `curl -L -X POST '${functionUrl}' \\ - -H 'Authorization: Bearer ${apiKey}' \\ ${apiKey.includes('publishable') ? `\n -H 'apikey: ${apiKey}' \\` : ''} + -H 'Authorization: Bearer ${apiKey}' \\${apiKey.includes('publishable') ? `\n -H 'apikey: ${apiKey}' \\` : ''} -H 'Content-Type: application/json' \\ --data '{"name":"Functions"}'`, }, diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx index 441aead5490d6..fca0edd2a2404 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx @@ -20,38 +20,50 @@ export interface UpcomingInvoiceProps { const usageBillingDocsLink: { [K in PricingMetric]?: string } = { [PricingMetric.MONTHLY_ACTIVE_USERS]: - '/docs/guides/platform/manage-your-usage/monthly-active-users', + 'https://supabase.com/docs/guides/platform/manage-your-usage/monthly-active-users', [PricingMetric.MONTHLY_ACTIVE_SSO_USERS]: - '/docs/guides/platform/manage-your-usage/monthly-active-users-sso', + 'https://supabase.com/docs/guides/platform/manage-your-usage/monthly-active-users-sso', [PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS]: - '/docs/guides/platform/manage-your-usage/monthly-active-users-third-party', - [PricingMetric.AUTH_MFA_PHONE]: '/docs/guides/platform/manage-your-usage/advanced-mfa-phone', + 'https://supabase.com/docs/guides/platform/manage-your-usage/monthly-active-users-third-party', + [PricingMetric.AUTH_MFA_PHONE]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/advanced-mfa-phone', - [PricingMetric.EGRESS]: '/docs/guides/platform/manage-your-usage/egress', + [PricingMetric.EGRESS]: 'https://supabase.com/docs/guides/platform/manage-your-usage/egress', [PricingMetric.FUNCTION_INVOCATIONS]: - '/docs/guides/platform/manage-your-usage/edge-function-invocations', + 'https://supabase.com/docs/guides/platform/manage-your-usage/edge-function-invocations', - [PricingMetric.STORAGE_SIZE]: '/docs/guides/platform/manage-your-usage/storage-size', + [PricingMetric.STORAGE_SIZE]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/storage-size', [PricingMetric.STORAGE_IMAGES_TRANSFORMED]: - '/docs/guides/platform/manage-your-usage/storage-image-transformations', + 'https://supabase.com/docs/guides/platform/manage-your-usage/storage-image-transformations', [PricingMetric.REALTIME_MESSAGE_COUNT]: - '/docs/guides/platform/manage-your-usage/realtime-messages', + 'https://supabase.com/docs/guides/platform/manage-your-usage/realtime-messages', [PricingMetric.REALTIME_PEAK_CONNECTIONS]: - '/docs/guides/platform/manage-your-usage/realtime-peak-connections', - - [PricingMetric.CUSTOM_DOMAIN]: '/docs/guides/platform/manage-your-usage/custom-domains', - [PricingMetric.IPV4]: '/docs/guides/platform/manage-your-usage/ipv4', - [PricingMetric.PITR_7]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', - [PricingMetric.PITR_14]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', - [PricingMetric.PITR_28]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', - [PricingMetric.DISK_SIZE_GB_HOURS_GP3]: '/docs/guides/platform/manage-your-usage/disk-size', - [PricingMetric.DISK_SIZE_GB_HOURS_IO2]: '/docs/guides/platform/manage-your-usage/disk-size', - [PricingMetric.DISK_IOPS_GP3]: '/docs/guides/platform/manage-your-usage/disk-iops', - [PricingMetric.DISK_IOPS_IO2]: '/docs/guides/platform/manage-your-usage/disk-iops', - [PricingMetric.DISK_THROUGHPUT_GP3]: '/docs/guides/platform/manage-your-usage/disk-throughput', - [PricingMetric.LOG_DRAIN]: '/docs/guides/platform/manage-your-usage/log-drains', + 'https://supabase.com/docs/guides/platform/manage-your-usage/realtime-peak-connections', + + [PricingMetric.CUSTOM_DOMAIN]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/custom-domains', + [PricingMetric.IPV4]: 'https://supabase.com/docs/guides/platform/manage-your-usage/ipv4', + [PricingMetric.PITR_7]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.PITR_14]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.PITR_28]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.DISK_SIZE_GB_HOURS_GP3]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', + [PricingMetric.DISK_SIZE_GB_HOURS_IO2]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', + [PricingMetric.DISK_IOPS_GP3]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-iops', + [PricingMetric.DISK_IOPS_IO2]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-iops', + [PricingMetric.DISK_THROUGHPUT_GP3]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-throughput', + [PricingMetric.LOG_DRAIN]: + 'https://supabase.com/docs/guides/platform/manage-your-usage/log-drains', } const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { @@ -133,7 +145,7 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { compute costs starting at $10/month, independent of activity. See{' '} docs @@ -153,7 +165,9 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { Compute, Disk Size, provisioned Disk IOPS, provisioned Disk Throughput, and IPv4. See{' '} docs diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index cc82d7fc1bfd0..3a5ff8dca18bc 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -351,7 +351,9 @@ export const SubscriptionPlanUpdateDialog = ({ Credits; additional projects start at $10 /month regardless of usage.{' '} Learn more @@ -587,7 +589,7 @@ export const SubscriptionPlanUpdateDialog = ({ /month regardless of usage.{' '}
diff --git a/apps/studio/components/interfaces/Reports/ReportPadding.tsx b/apps/studio/components/interfaces/Reports/ReportPadding.tsx index 37e30aaff1c5f..1c74cd7b3211a 100644 --- a/apps/studio/components/interfaces/Reports/ReportPadding.tsx +++ b/apps/studio/components/interfaces/Reports/ReportPadding.tsx @@ -1,7 +1,6 @@ import { PropsWithChildren } from 'react' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useAppStateSnapshot } from 'state/app-state' import { cn } from 'ui' /** @@ -9,13 +8,12 @@ import { cn } from 'ui' */ const ReportPadding = ({ children }: PropsWithChildren<{}>) => { const snap = useAiAssistantStateSnapshot() - const { editorPanel } = useAppStateSnapshot() return (
{children} diff --git a/apps/studio/components/interfaces/Reports/Reports.tsx b/apps/studio/components/interfaces/Reports/Reports.tsx index 044d4e73534ce..1bd29985dff80 100644 --- a/apps/studio/components/interfaces/Reports/Reports.tsx +++ b/apps/studio/components/interfaces/Reports/Reports.tsx @@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { groupBy, isEqual, isNull } from 'lodash' import { ArrowRight, Plus, RefreshCw, Save } from 'lucide-react' +import { useRouter } from 'next/router' import { DragEvent, useEffect, useState } from 'react' import { toast } from 'sonner' @@ -30,6 +31,7 @@ import { useProfile } from 'lib/profile' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { Dashboards } from 'types' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { createSqlSnippetSkeletonV2 } from '../SQLEditor/SQLEditor.utils' import { ChartConfig } from '../SQLEditor/UtilityPanel/ChartConfig' import { GridResize } from './GridResize' @@ -40,6 +42,7 @@ const DEFAULT_CHART_COLUMN_COUNT = 1 const DEFAULT_CHART_ROW_COUNT = 1 const Reports = () => { + const router = useRouter() const { id, ref } = useParams() const { profile } = useProfile() const { data: project } = useSelectedProjectQuery() @@ -54,6 +57,9 @@ const Reports = () => { const [hasEdits, setHasEdits] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) + const [navigateUrl, setNavigateUrl] = useState() + const [confirmNavigate, setConfirmNavigate] = useState(false) + const { data: userContents, isLoading, @@ -352,6 +358,31 @@ const Reports = () => { checkEditState() }, [config]) + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasEdits) { + e.preventDefault() + e.returnValue = '' // deprecated, but older browsers still require this + } + } + + const handleBrowseAway = (url: string) => { + if (hasEdits && !confirmNavigate) { + setNavigateUrl(url) + throw 'Route change declined' // Just to prevent the route change + } else { + setNavigateUrl(undefined) + } + } + + window.addEventListener('beforeunload', handleBeforeUnload) + router.events.on('routeChangeStart', handleBrowseAway) + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload) + router.events.off('routeChangeStart', handleBrowseAway) + } + }, [hasEdits, confirmNavigate, router]) + if (isLoading) { return } @@ -361,149 +392,167 @@ const Reports = () => { } return ( -
-
-
-

{currentReport?.name || 'Reports'}

-

{currentReport?.description}

+ <> +
+
+
+

{currentReport?.name || 'Reports'}

+

{currentReport?.description}

+
+ {hasEdits && ( +
+ + +
+ )}
- {hasEdits && ( +
- - -
- )} -
-
-
- } - className="w-7" - disabled={isRefreshing} - tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} - onClick={onRefreshReport} - /> -
- -

- SQL blocks are independent of the selected date range -

-
- } + icon={} + className="w-7" + disabled={isRefreshing} + tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} + onClick={onRefreshReport} /> +
+ +

+ SQL blocks are independent of the selected date range +

+
+ } + /> + + {startDate && endDate && ( +
+ + {dayjs(startDate).format('MMM D, YYYY')} + + + + + + {dayjs(endDate).format('MMM D, YYYY')} + +
+ )} +
+
- {startDate && endDate && ( -
- - {dayjs(startDate).format('MMM D, YYYY')} - - - - - - {dayjs(endDate).format('MMM D, YYYY')} - -
+
+ {canUpdateReport ? ( + + + + + + + + + ) : ( + } + tooltip={{ + content: { + side: 'bottom', + className: 'w-56 text-center', + text: 'You need additional permissions to update custom reports', + }, + }} + > + Add block + )} +
-
- {canUpdateReport ? ( - - - - - - - - - ) : ( - } - tooltip={{ - content: { - side: 'bottom', - className: 'w-56 text-center', - text: 'You need additional permissions to update custom reports', - }, - }} - > - Add block - - )} - -
+ {config?.layout !== undefined && config.layout.length === 0 ? ( +
+ {canUpdateReport ? ( + + + + + + + + + ) : ( +

No charts set up yet in report

+ )} +
+ ) : ( +
+ {config && startDate && endDate && ( + + )} +
+ )}
- - {config?.layout !== undefined && config.layout.length === 0 ? ( -
- {canUpdateReport ? ( - - - - - - - - - ) : ( -

No charts set up yet in report

- )} -
- ) : ( -
- {config && startDate && endDate && ( - - )} -
- )} -
+ { + setConfirmNavigate(true) + setNavigateUrl(undefined) + router.push(navigateUrl ?? '/') + }} + onCancel={() => setNavigateUrl(undefined)} + > +

+ Unsaved changes will be lost, are you sure you want to navigate away? +

+
+ ) } diff --git a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx index 59ea6403dc820..97f4fdb668e11 100644 --- a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx @@ -88,6 +88,17 @@ const MonacoEditor = ({ }, }) + editor.addAction({ + id: 'save-query', + label: 'Save Query', + keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyS], + contextMenuGroupId: 'operation', + contextMenuOrder: 0, + run: () => { + if (snippet) snapV2.addNeedsSaving(snippet.snippet.id) + }, + }) + editor.addAction({ id: 'explain-code', label: 'Explain Code', diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx index 627e73768ab92..19403f8278f5f 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx @@ -1,5 +1,6 @@ -import { detectOS } from 'lib/helpers' import { Command, CornerDownLeft, Loader2 } from 'lucide-react' + +import { detectOS } from 'lib/helpers' import { Button } from 'ui' interface SqlRunButtonProps { diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx index 29a026d50dd43..d7704a00b44a1 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx @@ -6,7 +6,6 @@ import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonatio import DatabaseSelector from 'components/ui/DatabaseSelector' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { IS_PLATFORM } from 'lib/constants' -import { detectOS } from 'lib/helpers' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Button, @@ -27,7 +26,7 @@ export type UtilityActionsProps = { id: string isExecuting?: boolean isDisabled?: boolean - hasSelection: boolean + hasSelection?: boolean prettifyQuery: () => void executeQuery: () => void } @@ -36,11 +35,10 @@ const UtilityActions = ({ id, isExecuting = false, isDisabled = false, - hasSelection, + hasSelection = false, prettifyQuery, executeQuery, }: UtilityActionsProps) => { - const os = detectOS() const { ref } = useParams() const snapV2 = useSqlEditorV2StateSnapshot() @@ -189,6 +187,7 @@ const UtilityActions = ({ variant={IS_PLATFORM ? 'connected-on-both' : 'connected-on-right'} /> void @@ -23,7 +22,6 @@ export interface FileExplorerProps { const FileExplorer = ({ columns = [], - openedFolders = [], selectedItems = [], itemSearchString, onFilesUpload = noop, @@ -61,7 +59,6 @@ const FileExplorer = ({ key={`column-${index}`} index={index} column={column} - openedFolders={openedFolders} selectedItems={selectedItems} itemSearchString={itemSearchString} onFilesUpload={onFilesUpload} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx index 84167c8d27107..5d1ed5f3066cd 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -19,7 +19,7 @@ import { STORAGE_ROW_TYPES, STORAGE_VIEWS, } from '../Storage.constants' -import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' +import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' import FileExplorerRow from './FileExplorerRow' const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => { @@ -60,7 +60,6 @@ export interface FileExplorerColumnProps { index: number column: StorageColumn fullWidth?: boolean - openedFolders?: StorageItem[] selectedItems: StorageItemWithColumn[] itemSearchString: string onFilesUpload: (event: any, index: number) => void @@ -73,7 +72,6 @@ const FileExplorerColumn = ({ index = 0, column, fullWidth = false, - openedFolders = [], selectedItems = [], itemSearchString, onFilesUpload = noop, @@ -221,7 +219,6 @@ const FileExplorerColumn = ({ view: snap.view, columnIndex: index, selectedItems, - openedFolders, }} ItemComponent={FileExplorerRow} getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index 306eb49098fec..8979041793e68 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -22,7 +22,6 @@ import type { ItemRenderer } from 'components/ui/InfiniteList' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' -import { toast } from 'sonner' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Checkbox, @@ -51,7 +50,6 @@ import { StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRowEditing from './FileExplorerRowEditing' import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' -import { useSelectedBucket } from './useSelectedBucket' export const RowIcon = ({ view, @@ -104,7 +102,6 @@ export interface FileExplorerRowProps { view: STORAGE_VIEWS columnIndex: number selectedItems: StorageItemWithColumn[] - openedFolders: StorageItem[] } const FileExplorerRow: ItemRenderer = ({ @@ -113,16 +110,14 @@ const FileExplorerRow: ItemRenderer = ({ view = STORAGE_VIEWS.COLUMNS, columnIndex = 0, selectedItems = [], - openedFolders = [], }) => { const { ref: projectRef, bucketId } = useParams() - const { bucket } = useSelectedBucket() const { selectedBucket, selectedFilePreview, + openedFolders, popColumnAtIndex, - pushOpenedFolderAtIndex, popOpenedFoldersAtIndex, clearSelectedItems, setSelectedFilePreview, @@ -131,22 +126,21 @@ const FileExplorerRow: ItemRenderer = ({ setSelectedItemsToDelete, setSelectedItemToRename, setSelectedItemsToMove, - fetchFolderContents, + openFolder, downloadFolder, selectRangeItems, } = useStorageExplorerStateSnapshot() + const { show } = useContextMenu() const { onCopyUrl } = useCopyUrl() const isPublic = selectedBucket.public const itemWithColumnIndex = { ...item, columnIndex } const isSelected = !!selectedItems.find((i) => i.id === item.id) const isOpened = - openedFolders.length > columnIndex ? isEqual(openedFolders[columnIndex], item) : false + openedFolders.length > columnIndex ? openedFolders[columnIndex].name === item.name : false const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id) const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') - const { show } = useContextMenu() - const onSelectFile = async (columnIndex: number, file: StorageItem) => { popColumnAtIndex(columnIndex) popOpenedFoldersAtIndex(columnIndex - 1) @@ -154,21 +148,6 @@ const FileExplorerRow: ItemRenderer = ({ clearSelectedItems() } - const onSelectFolder = async (columnIndex: number, folder: StorageItem) => { - if (!bucket) return toast.error('Unable to retrieve bucket details') - - setSelectedFilePreview(undefined) - clearSelectedItems(columnIndex + 1) - popOpenedFoldersAtIndex(columnIndex - 1) - pushOpenedFolderAtIndex(folder, columnIndex) - await fetchFolderContents({ - bucketId: bucket.id, - folderId: folder.id, - folderName: folder.name, - index: columnIndex, - }) - } - const onCheckItem = (isShiftKeyHeld: boolean) => { // Select a range if shift is held down if (isShiftKeyHeld && selectedItems.length !== 0) { @@ -346,7 +325,7 @@ const FileExplorerRow: ItemRenderer = ({ event.preventDefault() if (item.status !== STORAGE_ROW_STATUS.LOADING && !isOpened && !isPreviewed) { item.type === STORAGE_ROW_TYPES.FOLDER || item.type === STORAGE_ROW_TYPES.BUCKET - ? onSelectFolder(columnIndex, item) + ? openFolder(columnIndex, item) : onSelectFile(columnIndex, item) } }} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx index 2be0df79f40bd..cd17e1b162240 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -30,7 +30,17 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit const itemWithColumnIndex = { ...item, columnIndex } renameFolder(itemWithColumnIndex, name, columnIndex) } else { - addNewFolder(name, columnIndex) + addNewFolder({ + folderName: name, + columnIndex, + onError: () => { + if (event.type === 'blur') { + addNewFolder({ folderName: '', columnIndex }) + } else { + inputRef.current.select() + } + }, + }) } } @@ -47,7 +57,7 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit const handleEsc = (event: KeyboardEvent) => { if (event.key === 'Escape') { if (item?.id !== undefined) onSaveItemName(item.name) - else addNewFolder('', columnIndex) + else addNewFolder({ folderName: '', columnIndex }) } } diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index cfc8ecd5c40ad..160e8118e4f9b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -181,7 +181,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
{ passHref href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`} > - Auth {policies.length > 1 ? 'policies' : 'policy'} + RLS {policies.length > 1 ? 'policies' : 'policy'} )} diff --git a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx index 8b9b80ffef26f..93254be01c471 100644 --- a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx +++ b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx @@ -1,12 +1,10 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useAppStateSnapshot } from 'state/app-state' import { AiIconAnimation } from 'ui' import { KeyboardShortcut } from 'ui' export const AssistantButton = () => { const snap = useAiAssistantStateSnapshot() - const { setEditorPanel } = useAppStateSnapshot() return ( { className="rounded-none w-[32px] h-[30px] flex items-center justify-center p-0 hover:bg-brand-400" onClick={() => { snap.toggleAssistant() - setEditorPanel({ open: false }) }} tooltip={{ content: { diff --git a/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx b/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx index a88a80a5bdb7c..d447221b08b6e 100644 --- a/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx +++ b/apps/studio/components/layouts/AppLayout/InlineEditorButton.tsx @@ -1,23 +1,15 @@ -import { SqlEditor } from 'icons' -import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useAppStateSnapshot } from 'state/app-state' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { SqlEditor } from 'icons' import { KeyboardShortcut } from 'ui' -export const InlineEditorButton = () => { - const { closeAssistant } = useAiAssistantStateSnapshot() - const { setEditorPanel, editorPanel } = useAppStateSnapshot() - +export const InlineEditorButton = ({ onClick }: { onClick: () => void }) => { return ( { - closeAssistant() - setEditorPanel({ open: !editorPanel.open }) - }} + onClick={onClick} tooltip={{ content: { text: ( diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 17ca647da39dc..b0d99ecc117df 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, motion } from 'framer-motion' import Link from 'next/link' -import { ReactNode, useMemo } from 'react' +import { ReactNode, useMemo, useState } from 'react' import { useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' @@ -12,10 +12,12 @@ import { BranchDropdown } from 'components/layouts/AppLayout/BranchDropdown' import { InlineEditorButton } from 'components/layouts/AppLayout/InlineEditorButton' import { OrganizationDropdown } from 'components/layouts/AppLayout/OrganizationDropdown' import { ProjectDropdown } from 'components/layouts/AppLayout/ProjectDropdown' +import EditorPanel from 'components/ui/EditorPanel/EditorPanel' import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils' import { useOrgUsageQuery } from 'data/usage/org-usage-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useHotKey } from 'hooks/ui/useHotKey' import { IS_PLATFORM } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { Badge, cn } from 'ui' @@ -64,6 +66,15 @@ const LayoutHeader = ({ const { setMobileMenuOpen } = useAppStateSnapshot() const gitlessBranching = useIsBranching2Enabled() + const [showEditorPanel, setShowEditorPanel] = useState(false) + useHotKey( + () => { + if (projectRef) setShowEditorPanel(!showEditorPanel) + }, + 'e', + [showEditorPanel, projectRef] + ) + // We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap) const { data: orgUsage } = useOrgUsageQuery( { orgSlug: selectedOrganization?.slug }, @@ -82,74 +93,94 @@ const LayoutHeader = ({ const showOrgSelection = slug || (selectedOrganization && projectRef) return ( -
- {showProductMenu && ( -
- -
- )} -
+
+ {showProductMenu && ( +
+ +
)} - > -
- -
- {showOrgSelection && IS_PLATFORM ? ( - <> - - - - ) : null} - - {projectRef && ( - - - +
+
+ +
+ {showOrgSelection && IS_PLATFORM ? ( + <> + + + + ) : null} + + {projectRef && ( + + + - {exceedingLimits && ( -
- - - Exceeding usage limits - - -
- )} + {exceedingLimits && ( +
+ + + Exceeding usage limits + + +
+ )} - {selectedProject && ( - <> - - {IS_PLATFORM && } - - )} -
- )} -
+ {selectedProject && ( + <> + + {IS_PLATFORM && } + + )} + + )} + + + + {headerTitle && ( + + + {headerTitle} + + )} + +
- {headerTitle && ( + {projectRef && ( - - {headerTitle} + {IS_PLATFORM && gitlessBranching && } + )} +
+
+ {customHeaderComponents && customHeaderComponents} + {IS_PLATFORM ? ( + <> + - - {projectRef && ( - - {IS_PLATFORM && gitlessBranching && } - - +
+ + + + {!!projectRef && ( + <> + setShowEditorPanel(true)} /> + + + )} + +
+ + + ) : ( + <> + +
+ + {!!projectRef && ( + <> + setShowEditorPanel(true)} /> + + + )} + +
+ + )} -
- -
-
- {customHeaderComponents && customHeaderComponents} - {IS_PLATFORM ? ( - <> - - -
- - - - {!!projectRef && ( - <> - - - - )} - -
- - - ) : ( - <> - -
- - {!!projectRef && ( - <> - - - - )} - -
- - - )} +
-
-
+
+ setShowEditorPanel(false)} /> + ) } diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 831d3c0a021bf..fb9c5bcf9fa2a 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -7,12 +7,12 @@ import { useParams } from 'common' import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal' import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs' import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant' -import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { Loading } from 'components/ui/Loading' import { ResourceExhaustionWarningBanner } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' +import { useHotKey } from 'hooks/ui/useHotKey' import { PROJECT_STATUS } from 'lib/constants' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useAppStateSnapshot } from 'state/app-state' @@ -91,16 +91,11 @@ const ProjectLayout = forwardRef aiSnap.toggleAssistant(), 'i', [aiSnap]) + const editor = useEditorType() const forceShowProductMenu = editor === undefined const sideBarIsOpen = forceShowProductMenu || showSidebar @@ -124,28 +119,6 @@ const ProjectLayout = forwardRef { - const handler = (e: KeyboardEvent) => { - // Cmd+I: Open AI Assistant, close Editor Panel - if (e.metaKey && e.key === 'i' && !e.altKey && !e.shiftKey) { - setEditorPanel({ open: false }) - aiSnap.toggleAssistant() - e.preventDefault() - e.stopPropagation() - } - // Cmd+E: Toggle Editor Panel, always close AI Assistant - if (e.metaKey && e.key === 'e' && !e.altKey && !e.shiftKey) { - aiSnap.closeAssistant() - toggleEditorPanel() - e.preventDefault() - e.stopPropagation() - } - } - window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setEditorPanel, aiSnap, editorPanel.open]) - return ( <> @@ -238,7 +211,7 @@ const ProjectLayout = forwardRef - {isClient && (aiSnap.open || editorPanel.open) && ( + {isClient && aiSnap.open && ( <> - {aiSnap.open ? ( - - ) : editorPanel.open ? ( - - ) : null} + )} diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx index 3633fdf5b74f5..d2c48404aa212 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx @@ -6,7 +6,18 @@ import { Snippet } from 'data/content/sql-folders-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import useLatest from 'hooks/misc/useLatest' import { useProfile } from 'lib/profile' -import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react' +import { + Copy, + Download, + Edit, + ExternalLink, + Heart, + Lock, + Move, + Plus, + Share, + Trash, +} from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { ComponentProps, useEffect } from 'react' @@ -19,6 +30,7 @@ import { ContextMenuTrigger_Shadcn_, ContextMenu_Shadcn_, TreeViewItem, + cn, } from 'ui' interface SQLEditorTreeViewItemProps @@ -85,6 +97,7 @@ export const SQLEditorTreeViewItem = ({ const isOwner = profile?.id === element?.metadata.owner_id const isSharedSnippet = element.metadata.visibility === 'project' + const isFavorite = element.metadata.favorite const isEditing = status === 'editing' const isSaving = status === 'saving' @@ -154,6 +167,14 @@ export const SQLEditorTreeViewItem = ({ } } + const onToggleFavorite = () => { + const snippetId = element.metadata.id + if (snippetId) { + if (isFavorite) snapV2.removeFavorite(snippetId) + else snapV2.addFavorite(snippetId) + } + } + return ( <> @@ -322,6 +343,19 @@ export const SQLEditorTreeViewItem = ({ Duplicate query )} + onToggleFavorite()} + onFocusCapture={(e) => e.stopPropagation()} + > + + {isFavorite ? 'Remove from' : 'Add to'} favorites + {onSelectDownload !== undefined && IS_PLATFORM && ( void + onClose?: () => void executeQuery?: () => void } @@ -41,9 +43,11 @@ const AIEditor = ({ aiMetadata, initialPrompt, readOnly = false, + autoFocus = false, className = '', options = {}, onChange, + onClose, executeQuery, }: AIEditorProps) => { const os = detectOS() @@ -149,6 +153,17 @@ const AIEditor = ({ }) } + if (!!onClose) { + editor.addAction({ + id: 'close-editor', + label: 'Close editor', + keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyE], + contextMenuGroupId: 'operation', + contextMenuOrder: 0, + run: () => onClose(), + }) + } + editor.addAction({ id: 'generate-ai', label: 'Generate with AI', @@ -176,6 +191,11 @@ const AIEditor = ({ }) }, }) + + if (autoFocus) { + if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 }) + editor.focus() + } } const handlePrompt = async ( @@ -327,6 +347,7 @@ const AIEditor = ({
) : (
+ {/* [Joshen] Refactor: Use CodeEditor.tsx instead, reduce duplicate declaration of Editor */} table.resetColumnFilters(), 'Escape') return ( diff --git a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx index 62d593fbab33a..ae19f3ca6dbbe 100644 --- a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx +++ b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx @@ -1,6 +1,6 @@ -import { debounce } from 'lodash' import { Book, Save, X } from 'lucide-react' -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { useParams } from 'common' @@ -17,7 +17,6 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' -import { useAppStateSnapshot } from 'state/app-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Button, @@ -29,12 +28,21 @@ import { CommandInput_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, + Form_Shadcn_, + FormField_Shadcn_, HoverCard_Shadcn_, HoverCardContent_Shadcn_, HoverCardTrigger_Shadcn_, + Input_Shadcn_ as Input, + KeyboardShortcut, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, SQL_ICON, } from 'ui' import { Admonition } from 'ui-patterns' @@ -44,14 +52,44 @@ import { ButtonTooltip } from '../ButtonTooltip' import { InlineLink } from '../InlineLink' import SqlWarningAdmonition from '../SqlWarningAdmonition' +type Template = { + name: string + description: string + content: string +} + interface EditorPanelProps { + open: boolean + onClose: () => void + initialValue?: string + label?: string + saveLabel?: string + saveValue?: string + onSave?: (value: string, saveValue: string) => void + onRunSuccess?: (value: any[]) => void + onRunError?: (value: any) => void + functionName?: string + templates?: Template[] + initialPrompt?: string onChange?: (value: string) => void } -export const EditorPanel = ({ onChange }: EditorPanelProps) => { +export const EditorPanel = ({ + open, + onClose, + initialValue = '', + label = '', + saveLabel = 'Save', + saveValue = '', + onSave, + onRunSuccess, + onRunError, + templates = [], + initialPrompt = '', + onChange, +}: EditorPanelProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() - const { editorPanel, setEditorPanel } = useAppStateSnapshot() const { profile } = useProfile() const snapV2 = useSqlEditorV2StateSnapshot() const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation() @@ -61,26 +99,38 @@ export const EditorPanel = ({ onChange }: EditorPanelProps) => { const [error, setError] = useState() const [results, setResults] = useState(undefined) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() - const [currentValue, setCurrentValue] = useState(editorPanel.initialValue || '') - const [savedCode, setSavedCode] = useState('') - const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false) + const [currentValue, setCurrentValue] = useState(initialValue) const [showResults, setShowResults] = useState(true) const [isTemplatesOpen, setIsTemplatesOpen] = useState(false) + const saveForm = useForm({ + defaultValues: { + saveValue: saveValue || '', + }, + }) + const errorHeader = error?.formattedError?.split('\n')?.filter((x: string) => x.length > 0)?.[0] const errorContent = - error?.formattedError - ?.split('\n') - ?.filter((x: string) => x.length > 0) - ?.slice(1) ?? [] + 'formattedError' in (error || {}) + ? error?.formattedError + ?.split('\n') + ?.filter((x: string) => x.length > 0) + ?.slice(1) ?? [] + : [error?.message ?? ''] const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: async (res) => { setResults(res.result) + if (onRunSuccess) { + onRunSuccess(res.result) + } }, onError: (error) => { setError(error) setResults([]) + if (onRunError) { + onRunError(error) + } }, }) @@ -98,11 +148,6 @@ export const EditorPanel = ({ onChange }: EditorPanelProps) => { return } } - - if (editorPanel.onSave) { - editorPanel.onSave(currentValue) - } - executeSql({ sql: suffixWithLimit(currentValue, 100), projectRef: project?.ref, @@ -124,250 +169,285 @@ export const EditorPanel = ({ onChange }: EditorPanelProps) => { setIsTemplatesOpen(false) } - // Create a debounced version of the revert code function - const debouncedRevertCode = useCallback( - debounce(() => { - setIsPreviewingTemplate(false) - handleChange(savedCode) - }, 300), - [savedCode] - ) - - // Cleanup debounce on unmount - useEffect(() => { - return () => { - debouncedRevertCode.cancel() - } - }, [debouncedRevertCode]) - useEffect(() => { - if (editorPanel.initialValue !== undefined && editorPanel.initialValue !== currentValue) { - setCurrentValue(editorPanel.initialValue) + if (initialValue !== undefined && initialValue !== currentValue) { + setCurrentValue(initialValue) + setResults(undefined) + setError(undefined) + setShowWarning(undefined) } - }, [editorPanel.initialValue]) + }, [initialValue]) useEffect(() => { - if (editorPanel.initialValue !== currentValue) { - setEditorPanel({ - initialValue: currentValue, - }) - } - }, [currentValue, setEditorPanel]) + saveForm.reset({ + saveValue: saveValue || '', + }) + }, [saveValue, saveForm]) return ( -
-
- SQL Editor -
- - - - - - - - - No templates found. - - {editorPanel.templates?.map((template) => ( - - - onSelectTemplate(template.content)} - className="cursor-pointer" - > -
- !open && onClose()}> + + +
+ SQL Editor + {label && {label}} +
+
+ {templates.length > 0 && ( + + + + + + + + + No templates found. + + {templates.map((template) => ( + + + onSelectTemplate(template.content)} + className="cursor-pointer" + > +
+ +
+

{template.name}

+

+ {template.description} +

+
+
+
+
+ + -
-

{template.name}

-

- {template.description} -

-
-
- - - - - - - ))} - - - - - - } - onClick={async () => { - if (!ref) return console.error('Project ref is required') - if (!project) return console.error('Project is required') - if (!profile) return console.error('Profile is required') - - try { - setIsSaving(true) - const { title: name } = await generateSqlTitle({ - sql: currentValue, - }) - const snippet = createSqlSnippetSkeletonV2({ - id: uuidv4(), - name, - sql: currentValue, - owner_id: profile.id, - project_id: project.id, - }) - snapV2.addSnippet({ projectRef: ref, snippet }) - snapV2.addNeedsSaving(snippet.id) - toast.success( -
- Saved snippet! View it{' '} - here -
- ) - } catch (error: any) { - toast.error(`Failed to create new query: ${error.message}`) - } finally { - setIsSaving(false) - } - }} - /> + + + ))} + + + + + + )} + } + onClick={async () => { + if (!ref) return console.error('Project ref is required') + if (!project) return console.error('Project is required') + if (!profile) return console.error('Profile is required') -
-
+ try { + setIsSaving(true) + const { title: name } = await generateSqlTitle({ + sql: currentValue, + }) + const snippet = createSqlSnippetSkeletonV2({ + id: uuidv4(), + name, + sql: currentValue, + owner_id: profile.id, + project_id: project.id, + }) + snapV2.addSnippet({ projectRef: ref, snippet }) + snapV2.addNeedsSaving(snippet.id) + toast.success( +
+ Saved snippet! View it{' '} + here +
+ ) + } catch (error: any) { + toast.error(`Failed to create new query: ${error.message}`) + } finally { + setIsSaving(false) + } + }} + /> -
-
- -
+ } + tooltip={{ + content: { + side: 'bottom', + text: ( +
+ Close Editor + +
+ ), + }, + }} + /> +
+ - {error !== undefined && ( -
- - {errorContent.length > 0 ? ( - errorContent.map((errorText: string, i: number) => ( -
-                        {errorText}
-                      
- )) - ) : ( -

{error.error}

- )} -
- } +
+
+ onClose()} />
- )} - {showWarning && ( - setShowWarning(undefined)} - onConfirm={() => { - setShowWarning(undefined) - onExecuteSql(true) - }} - /> - )} + {error !== undefined && ( +
+ + {errorContent.length > 0 ? ( + errorContent.map((errorText: string, i: number) => ( +
+                          {errorText}
+                        
+ )) + ) : ( +

{error.error}

+ )} +
+ } + /> +
+ )} + + {showWarning && ( + setShowWarning(undefined)} + onConfirm={() => { + setShowWarning(undefined) + onExecuteSql(true) + }} + /> + )} + + {results !== undefined && results.length > 0 && ( +
+ {showResults && ( +
+ +
+ )} +

+ + {results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`} + + +

+
+ )} + {results !== undefined && results.length === 0 && !error && ( +
+

+ Success. No rows returned. +

+
+ )} - {results !== undefined && results.length > 0 && ( -
- {showResults && ( -
- -
+
+ {onSave && ( + +
{ + onSave(currentValue, values.saveValue) + })} + className="flex items-center gap-2" + > + {saveValue && ( + ( + + )} + /> + )} + + +
)} -

- - {results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`} - - -

-
- )} - {results !== undefined && results.length === 0 && ( -
-

- Success. No rows returned. -

+
- )} - -
-
-
-
+ + ) } diff --git a/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx b/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx index 8559432226d2f..60585ed66620d 100644 --- a/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx +++ b/apps/studio/components/ui/QueryBlock/EditQueryButton.tsx @@ -10,7 +10,6 @@ import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization import Link from 'next/link' import { ComponentProps } from 'react' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useAppStateSnapshot } from 'state/app-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { cn, @@ -42,7 +41,6 @@ export const EditQueryButton = ({ const { newQuery } = useNewQuery() const sqlEditorSnap = useSqlEditorV2StateSnapshot() - const { setEditorPanel } = useAppStateSnapshot() const snap = useAiAssistantStateSnapshot() const isInSQLEditor = router.pathname.includes('/sql') @@ -78,10 +76,9 @@ export const EditQueryButton = ({ icon={} onClick={() => { if (isInlineEditorEnabled) { - setEditorPanel({ - open: true, - initialValue: sql, - }) + // This component needs to be updated to work with local EditorPanel state + // For now, fall back to creating a new query + if (sql) newQuery(sql, title) snap.closeAssistant() } else { if (sql) newQuery(sql, title) diff --git a/apps/studio/hooks/ui/useHotKey.ts b/apps/studio/hooks/ui/useHotKey.ts index d0c381b9beb03..4d1093ea10e8c 100644 --- a/apps/studio/hooks/ui/useHotKey.ts +++ b/apps/studio/hooks/ui/useHotKey.ts @@ -1,11 +1,14 @@ import { useEffect } from 'react' -export function useHotKey(callback: () => void, key: string): void { +export function useHotKey( + callback: (e: KeyboardEvent) => void, + key: string, + dependencies: any[] = [] +): void { useEffect(() => { function handler(e: KeyboardEvent) { - if (e.key === key && (e.metaKey || e.ctrlKey)) { - // e.preventDefault(); - callback() + if ((e.metaKey || e.ctrlKey) && e.key === key && !e.altKey && !e.shiftKey) { + callback(e) } } @@ -13,5 +16,5 @@ export function useHotKey(callback: () => void, key: string): void { return () => { window.removeEventListener('keydown', handler) } - }, [key]) + }, [key, ...dependencies]) } diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index 0e6ba60d9ac7d..fe8f6313386cc 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -12,6 +12,7 @@ import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' +import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import NoPermission from 'components/ui/NoPermission' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -21,7 +22,6 @@ import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPe import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' -import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' import { Input } from 'ui' @@ -67,13 +67,15 @@ const AuthPoliciesPage: NextPageWithLayout = () => { }>() const { schema = 'public', search: searchString = '' } = params const { data: project } = useSelectedProjectQuery() - const { setEditorPanel } = useAppStateSnapshot() const isInlineEditorEnabled = useIsInlineEditorEnabled() const [selectedTable, setSelectedTable] = useState() const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false) const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState() + // Local editor panel state + const [editorPanelOpen, setEditorPanelOpen] = useState(false) + const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] }) const { data: policies } = useDatabasePoliciesQuery({ @@ -142,43 +144,20 @@ const AuthPoliciesPage: NextPageWithLayout = () => { hasTables={tables.length > 0} isLocked={isSchemaLocked} onSelectCreatePolicy={(table: string) => { + setSelectedTable(table) + setSelectedPolicyToEdit(undefined) if (isInlineEditorEnabled) { - setEditorPanel({ - open: true, - initialValue: `create policy "replace_with_policy_name" - on ${schema}.${table} - for select - to authenticated - using ( - true -- Write your policy condition here -);`, - label: `Create new RLS policy on "${table}"`, - saveLabel: 'Create policy', - initialPrompt: `Create and name a entirely new RLS policy for the "${table}" table in the ${schema} schema. The policy should...`, - }) + setEditorPanelOpen(true) } else { - setSelectedTable(table) setShowPolicyAiEditor(true) } }} onSelectEditPolicy={(policy) => { + setSelectedPolicyToEdit(policy) + setSelectedTable(undefined) if (isInlineEditorEnabled) { - const sql = generatePolicyUpdateSQL(policy) - const templates = getGeneralPolicyTemplates(policy.schema, policy.table) - setEditorPanel({ - open: true, - initialValue: sql, - label: `Edit policy "${policy.name}"`, - saveLabel: 'Update policy', - templates: templates.map((template) => ({ - name: template.templateName, - description: template.description, - content: template.statement, - })), - initialPrompt: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`, - }) + setEditorPanelOpen(true) } else { - setSelectedPolicyToEdit(policy) setShowPolicyAiEditor(true) } }} @@ -198,6 +177,59 @@ const AuthPoliciesPage: NextPageWithLayout = () => { }} authContext="database" /> + + { + setEditorPanelOpen(false) + setSelectedPolicyToEdit(undefined) + setSelectedTable(undefined) + }} + onRunSuccess={() => { + setEditorPanelOpen(false) + setSelectedPolicyToEdit(undefined) + setSelectedTable(undefined) + }} + initialValue={ + selectedPolicyToEdit + ? generatePolicyUpdateSQL(selectedPolicyToEdit) + : selectedTable + ? `create policy "replace_with_policy_name" + on ${schema}.${selectedTable} + for select + to authenticated + using ( + true -- Write your policy condition here +);` + : '' + } + label={ + selectedPolicyToEdit + ? 'RLS policies are just SQL statements that you can alter' + : selectedTable + ? `Create new RLS policy on "${selectedTable}"` + : '' + } + initialPrompt={ + selectedPolicyToEdit + ? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...` + : selectedTable + ? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...` + : '' + } + templates={ + selectedPolicyToEdit + ? getGeneralPolicyTemplates( + selectedPolicyToEdit.schema, + selectedPolicyToEdit.table + ).map((template) => ({ + name: template.templateName, + description: template.description, + content: template.statement, + })) + : [] + } + />
) } diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index 0fba9273945b2..c4653c0b56b22 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -7,39 +7,32 @@ import FunctionsList from 'components/interfaces/Database/Functions/FunctionsLis import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { DatabaseFunction } from 'data/database-functions/database-functions-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' -import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' const DatabaseFunctionsPage: NextPageWithLayout = () => { const [selectedFunction, setSelectedFunction] = useState() const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false) const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false) - const { setEditorPanel } = useAppStateSnapshot() const isInlineEditorEnabled = useIsInlineEditorEnabled() + // Local editor panel state + const [editorPanelOpen, setEditorPanelOpen] = useState(false) + const [selectedFunctionForEditor, setSelectedFunctionForEditor] = useState< + DatabaseFunction | undefined + >() + const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions') const isPermissionsLoaded = usePermissionsLoaded() const createFunction = () => { if (isInlineEditorEnabled) { - setEditorPanel({ - open: true, - initialValue: `create function function_name() -returns void -language plpgsql -as $$ -begin - -- Write your function logic here -end; -$$;`, - label: 'Create new database function', - saveLabel: 'Create function', - initialPrompt: 'Create a new database function that...', - }) + setSelectedFunctionForEditor(undefined) + setEditorPanelOpen(true) } else { setSelectedFunction(undefined) setShowCreateFunctionForm(true) @@ -48,13 +41,8 @@ $$;`, const editFunction = (fn: DatabaseFunction) => { if (isInlineEditorEnabled) { - setEditorPanel({ - open: true, - initialValue: fn.complete_statement, - label: `Edit function "${fn.name}"`, - saveLabel: 'Update function', - initialPrompt: `Update the database function "${fn.name}" to...`, - }) + setSelectedFunctionForEditor(fn) + setEditorPanelOpen(true) } else { setSelectedFunction(fn) setShowCreateFunctionForm(true) @@ -97,6 +85,40 @@ $$;`, visible={showDeleteFunctionForm} setVisible={setShowDeleteFunctionForm} /> + + { + setEditorPanelOpen(false) + setSelectedFunctionForEditor(undefined) + }} + onClose={() => { + setEditorPanelOpen(false) + setSelectedFunctionForEditor(undefined) + }} + initialValue={ + selectedFunctionForEditor + ? selectedFunctionForEditor.complete_statement + : `create function function_name() +returns void +language plpgsql +as $$ +begin + -- Write your function logic here +end; +$$;` + } + label={ + selectedFunctionForEditor + ? `Edit function "${selectedFunctionForEditor.name}"` + : 'Create new database function' + } + initialPrompt={ + selectedFunctionForEditor + ? `Update the database function "${selectedFunctionForEditor.name}" to...` + : 'Create a new database function that...' + } + /> ) } diff --git a/apps/studio/pages/project/[ref]/database/triggers.tsx b/apps/studio/pages/project/[ref]/database/triggers.tsx index 890b49919cd7c..587f409b39dbb 100644 --- a/apps/studio/pages/project/[ref]/database/triggers.tsx +++ b/apps/studio/pages/project/[ref]/database/triggers.tsx @@ -10,35 +10,30 @@ import TriggersList from 'components/interfaces/Database/Triggers/TriggersList/T import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' -import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' const TriggersPage: NextPageWithLayout = () => { - const { setEditorPanel } = useAppStateSnapshot() const isInlineEditorEnabled = useIsInlineEditorEnabled() const [selectedTrigger, setSelectedTrigger] = useState() const [showCreateTriggerForm, setShowCreateTriggerForm] = useState(false) const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState(false) + // Local editor panel state + const [editorPanelOpen, setEditorPanelOpen] = useState(false) + const [selectedTriggerForEditor, setSelectedTriggerForEditor] = useState() + const canReadTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const isPermissionsLoaded = usePermissionsLoaded() const createTrigger = () => { if (isInlineEditorEnabled) { - setEditorPanel({ - open: true, - initialValue: `create trigger trigger_name -after insert or update or delete on table_name -for each row -execute function function_name();`, - label: 'Create new database trigger', - saveLabel: 'Create trigger', - initialPrompt: 'Create a new database trigger that...', - }) + setSelectedTriggerForEditor(undefined) + setEditorPanelOpen(true) } else { setSelectedTrigger(undefined) setShowCreateTriggerForm(true) @@ -47,14 +42,8 @@ execute function function_name();`, const editTrigger = (trigger: PostgresTrigger) => { if (isInlineEditorEnabled) { - const sql = generateTriggerCreateSQL(trigger) - setEditorPanel({ - open: true, - initialValue: sql, - label: `Edit trigger "${trigger.name}"`, - saveLabel: 'Update trigger', - initialPrompt: `Update the database trigger "${trigger.name}" to...`, - }) + setSelectedTriggerForEditor(trigger) + setEditorPanelOpen(true) } else { setSelectedTrigger(trigger) setShowCreateTriggerForm(true) @@ -98,6 +87,36 @@ execute function function_name();`, visible={showDeleteTriggerForm} setVisible={setShowDeleteTriggerForm} /> + + { + setEditorPanelOpen(false) + setSelectedTriggerForEditor(undefined) + }} + onClose={() => { + setEditorPanelOpen(false) + setSelectedTriggerForEditor(undefined) + }} + initialValue={ + selectedTriggerForEditor + ? generateTriggerCreateSQL(selectedTriggerForEditor) + : `create trigger trigger_name +after insert or update or delete on table_name +for each row +execute function function_name();` + } + label={ + selectedTriggerForEditor + ? `Edit trigger "${selectedTriggerForEditor.name}"` + : 'Create new database trigger' + } + initialPrompt={ + selectedTriggerForEditor + ? `Update the database trigger "${selectedTriggerForEditor.name}" to...` + : 'Create a new database trigger that...' + } + /> ) } diff --git a/apps/studio/pages/project/[ref]/reports/api-overview.tsx b/apps/studio/pages/project/[ref]/reports/api-overview.tsx index 6db01dc456b3b..db5c13970d793 100644 --- a/apps/studio/pages/project/[ref]/reports/api-overview.tsx +++ b/apps/studio/pages/project/[ref]/reports/api-overview.tsx @@ -1,7 +1,9 @@ import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' +import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav' import ReportWidget from 'components/interfaces/Reports/ReportWidget' +import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' import { ErrorCountsChartRenderer, NetworkTrafficRenderer, @@ -10,16 +12,12 @@ import { TotalRequestsChartRenderer, } from 'components/interfaces/Reports/renderers/ApiRenderers' import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers' +import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' import DefaultLayout from 'components/layouts/DefaultLayout' import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' -import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav' -import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' -import { useState } from 'react' - import { useApiReport } from 'data/reports/api-report-query' import { useReportDateRange } from 'hooks/misc/useReportDateRange' import { NextPageWithLayout } from 'types' -import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' export const ApiReport: NextPageWithLayout = () => { const report = useApiReport() diff --git a/apps/studio/pages/project/[ref]/settings/auth.tsx b/apps/studio/pages/project/[ref]/settings/auth.tsx index 9869b02e6f0f2..ce87957dfd9c0 100644 --- a/apps/studio/pages/project/[ref]/settings/auth.tsx +++ b/apps/studio/pages/project/[ref]/settings/auth.tsx @@ -17,77 +17,77 @@ const ProjectSettings: NextPageWithLayout = () => {

General user signup Password settings in email provider User sessions Refresh tokens Bot and abuse protection SMTP settings Access token expiry Multifactor authentication Third party authentication Max request duration Max direct database connections diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 9303cc4ed1296..133c53edd2e41 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -1,80 +1,18 @@ -import { proxy, snapshot, subscribe, useSnapshot } from 'valtio' +import { proxy, snapshot, useSnapshot } from 'valtio' import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS, LOCAL_STORAGE_KEYS } from 'common' -import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' - -export type Template = { - name: string - description: string - content: string -} - -type EditorPanelType = { - open: boolean - initialValue?: string - label?: string - saveLabel?: string - onSave?: (value: string) => void - functionName?: string - templates?: Template[] - initialPrompt?: string -} - type DashboardHistoryType = { sql?: string editor?: string } -const INITIAL_EDITOR_PANEL: EditorPanelType = { - open: false, - initialValue: '', - label: '', - saveLabel: '', - initialPrompt: '', - templates: SQL_TEMPLATES.filter((template) => template.type === 'template').map((template) => ({ - name: template.title, - description: template.description, - content: template.sql, - })), -} - const EMPTY_DASHBOARD_HISTORY: DashboardHistoryType = { sql: undefined, editor: undefined, } const getInitialState = () => { - if (typeof window === 'undefined') { - return { - editorPanel: INITIAL_EDITOR_PANEL, - dashboardHistory: EMPTY_DASHBOARD_HISTORY, - activeDocsSection: ['introduction'], - docsLanguage: 'js', - showProjectApiDocs: false, - showCreateBranchModal: false, - showAiSettingsModal: false, - showConnectDialog: false, - ongoingQueriesPanelOpen: false, - mobileMenuOpen: false, - showSidebar: true, - lastRouteBeforeVisitingAccountPage: '', - } - } - - const storedEditor = localStorage.getItem(LOCAL_STORAGE_KEYS.EDITOR_PANEL_STATE) - - let parsedEditorPanel = INITIAL_EDITOR_PANEL - - try { - if (storedEditor) { - parsedEditorPanel = JSON.parse(storedEditor) - } - } catch { - // Ignore parsing errors - } - return { - editorPanel: parsedEditorPanel, dashboardHistory: EMPTY_DASHBOARD_HISTORY, activeDocsSection: ['introduction'], docsLanguage: 'js', @@ -85,6 +23,7 @@ const getInitialState = () => { ongoingQueriesPanelOpen: false, mobileMenuOpen: false, showSidebar: true, + showEditorPanel: false, lastRouteBeforeVisitingAccountPage: '', } } @@ -148,26 +87,6 @@ export const appState = proxy({ appState.ongoingQueriesPanelOpen = value }, - setEditorPanel: (value: Partial) => { - // Reset templates to initial if initialValue is empty - if (value.initialValue === '') { - value.templates = INITIAL_EDITOR_PANEL.templates - } - - if (!value.open) { - value.initialPrompt = INITIAL_EDITOR_PANEL.initialPrompt - } - - appState.editorPanel = { - ...appState.editorPanel, - ...value, - } - }, - - toggleEditorPanel: (value?: boolean) => { - appState.editorPanel.open = value ?? !appState.editorPanel.open - }, - mobileMenuOpen: false, setMobileMenuOpen: (value: boolean) => { appState.mobileMenuOpen = value @@ -179,16 +98,6 @@ export const appState = proxy({ }, }) -// Set up localStorage subscriptions -if (typeof window !== 'undefined') { - subscribe(appState, () => { - localStorage.setItem( - LOCAL_STORAGE_KEYS.EDITOR_PANEL_STATE, - JSON.stringify(appState.editorPanel) - ) - }) -} - export const getAppStateSnapshot = () => snapshot(appState) export const useAppStateSnapshot = (options?: Parameters[1]) => diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index afa80ef49adc9..67e2799411275 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -275,7 +275,15 @@ function createStorageExplorerState({ }) }, - addNewFolder: async (folderName: string, columnIndex: number) => { + addNewFolder: async ({ + folderName, + columnIndex, + onError, + }: { + folderName: string + columnIndex: number + onError?: () => void + }) => { if (!state.supabaseClient) return console.error('Supabase Client is missing') const autofix = false @@ -284,10 +292,16 @@ function createStorageExplorerState({ autofix, columnIndex, }) - if (formattedName === null) return + if (formattedName === null) { + onError?.() + return + } if (!/^[a-zA-Z0-9_-\s]*$/.test(formattedName)) { - return toast.error('Folder name contains invalid special characters') + onError?.() + return toast.error( + 'Only alphanumeric characters, hyphens, and underscores are allowed for folder names.' + ) } if (formattedName.length === 0) { @@ -318,6 +332,9 @@ function createStorageExplorerState({ paths: [`${pathToFolder}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`], }) } + + const newFolder = state.columns[columnIndex].items.find((x) => x.name === formattedName) + if (newFolder) state.openFolder(columnIndex, newFolder) }, fetchFolderContents: async ({ @@ -730,6 +747,19 @@ function createStorageExplorerState({ state.columns = updatedColumns }, + openFolder: async (columnIndex: number, folder: StorageItem) => { + state.setSelectedFilePreview(undefined) + state.clearSelectedItems(columnIndex + 1) + state.popOpenedFoldersAtIndex(columnIndex - 1) + state.pushOpenedFolderAtIndex(folder, columnIndex) + await state.fetchFolderContents({ + bucketId: state.selectedBucket.id, + folderId: folder.id, + folderName: folder.name, + index: columnIndex, + }) + }, + downloadFolder: async (folder: StorageItemWithColumn) => { let progress = 0 const toastId = toast.loading('Retrieving files from folder...') diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 072185fd20690..7146986b5599a 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -16,13 +16,6 @@ const Hero = () => {
-

Build in a weekend diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 5900e708122c0..3313dd542be59 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -1,4 +1,3 @@ -import { useTheme } from 'next-themes' import Link from 'next/link' import { useRouter } from 'next/router' import React, { useState } from 'react' @@ -14,17 +13,18 @@ import { NavigationMenuList, NavigationMenuTrigger, } from 'ui/src/components/shadcn/ui/navigation-menu' +import { AuthenticatedDropdownMenu } from 'ui-patterns' -import ScrollProgress from '~/components/ScrollProgress' -import { getMenu } from '~/data/nav' +import ScrollProgress from 'components/ScrollProgress' import GitHubButton from './GitHubButton' import HamburgerButton from './HamburgerMenu' import MenuItem from './MenuItem' import MobileMenu from './MobileMenu' import RightClickBrandLogo from './RightClickBrandLogo' -import { useSendTelemetryEvent } from '~/lib/telemetry' +import { useSendTelemetryEvent } from 'lib/telemetry' import useDropdownMenu from './useDropdownMenu' -import { AnnouncementBanner, AuthenticatedDropdownMenu } from 'ui-patterns' + +import { getMenu } from 'data/nav' interface Props { hideNavbar: boolean @@ -32,7 +32,6 @@ interface Props { } const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => { - const { resolvedTheme } = useTheme() const router = useRouter() const { width } = useWindowSize() const [open, setOpen] = useState(false) @@ -42,7 +41,6 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => { const user = useUser() const userMenu = useDropdownMenu(user) - const isHomePage = router.pathname === '/' const isLaunchWeekPage = router.pathname.includes('/launch-week') const isLaunchWeekXPage = router.pathname === '/launch-week/x' const isGAWeekSection = router.pathname.startsWith('/ga-week') @@ -67,11 +65,8 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => { return null } - // const showDarkLogo = isLaunchWeekPage || resolvedTheme?.includes('dark')! || isHomePage - return ( <> -
= [ + { id: 1, name: 'John', email: 'john@example.com' }, + { id: 2, name: 'Jane' }, +] + +function getUserById(id: number): User | undefined { + return users.find((user) => user.id === id) +} + +function createUser(name: string, email: string): User { + const newId = Math.max(...users.map((u) => u.id)) + 1 + const newUser: User = { id: newId, name } + if (email) { + newUser.email = email + } + users.push(newUser) + return newUser +} + +class UserManager { + users: Array = [] + + constructor(initialUsers = []) { + this.users = initialUsers + } + + addUser(user: User) { + this.users.push(user) + } + + getUsers() { + return [...this.users] + } +} diff --git a/packages/ui-patterns/src/Banners/data.json b/packages/ui-patterns/src/Banners/data.json index ebbb6e84c6030..a7f1e2fee1432 100644 --- a/packages/ui-patterns/src/Banners/data.json +++ b/packages/ui-patterns/src/Banners/data.json @@ -1,7 +1,7 @@ { - "text": "Launch Week 15", - "launch": "View all the announcements", + "text": "", + "launch": "", "launchDate": "2025-07-17T08:00:00.000-07:00", - "link": "/launch-week", + "link": "#", "cta": "Learn more" } diff --git a/packages/ui-patterns/src/PromoToast/PromoToast.tsx b/packages/ui-patterns/src/PromoToast/PromoToast.tsx index c25af133aef2b..af51c5e351544 100644 --- a/packages/ui-patterns/src/PromoToast/PromoToast.tsx +++ b/packages/ui-patterns/src/PromoToast/PromoToast.tsx @@ -47,9 +47,7 @@ const PromoToast = () => { />
{announcement.text} -

- July 14—18 -

+

@@ -62,15 +60,6 @@ const PromoToast = () => { Dismiss
- {/* */}
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f53cdb760d1a..6cbe691064396 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,7 +332,7 @@ importers: version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^9.15.0 - version: 9.15.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) + version: 9.15.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' version: 2.49.3 @@ -452,7 +452,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) + version: 1.19.1(next@15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) openai: specifier: ^4.20.1 version: 4.71.1(encoding@0.1.13)(zod@3.23.8) @@ -498,6 +498,9 @@ importers: remark-math: specifier: ^6.0.0 version: 6.0.0(supports-color@8.1.1) + remove-types: + specifier: 1.0.0 + version: 1.0.0(supports-color@8.1.1) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -534,7 +537,7 @@ importers: devDependencies: '@graphiql/toolkit': specifier: ^0.9.1 - version: 0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) + version: 0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) '@graphql-codegen/cli': specifier: 5.0.5 version: 5.0.5(@parcel/watcher@2.5.1)(@types/node@22.13.14)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.11.0))(graphql@16.11.0)(supports-color@8.1.1)(typescript@5.5.2) @@ -606,7 +609,7 @@ importers: version: 13.2.2 graphiql: specifier: ^4.0.2 - version: 4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -16047,6 +16050,9 @@ packages: remove-trailing-spaces@1.0.9: resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + remove-types@1.0.0: + resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} + rename-keys@1.2.0: resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} engines: {node: '>= 0.8.0'} @@ -20841,9 +20847,9 @@ snapshots: '@gar/promisify@1.1.3': {} - '@graphiql/plugin-doc-explorer@0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/plugin-doc-explorer@0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@headlessui/react': 2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.11.0 react: 18.3.1 @@ -20857,10 +20863,10 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/plugin-history@0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/plugin-history@0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) react: 18.3.1 react-compiler-runtime: 19.1.0-rc.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1) @@ -20900,9 +20906,9 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/react@0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) + '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) '@radix-ui/react-dialog': 1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': 1.2.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20929,13 +20935,13 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/toolkit@0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)': + '@graphiql/toolkit@0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.11.0 meros: 1.3.0(@types/node@22.13.14) optionalDependencies: - graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.3) + graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.1) transitivePeerDependencies: - '@types/node' @@ -20949,13 +20955,13 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@graphiql/toolkit@0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)': + '@graphiql/toolkit@0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.11.0 meros: 1.3.0(@types/node@22.13.14) optionalDependencies: - graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.3) + graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.1) transitivePeerDependencies: - '@types/node' @@ -25661,7 +25667,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@9.15.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': + '@sentry/nextjs@9.15.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)(supports-color@8.1.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.32.0 @@ -31516,11 +31522,11 @@ snapshots: graphemer@1.4.0: {} - graphiql@4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + graphiql@4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@graphiql/plugin-doc-explorer': 0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/plugin-history': 0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/plugin-doc-explorer': 0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/plugin-history': 0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.11.0 react: 18.3.1 react-compiler-runtime: 19.1.0-rc.1(react@18.3.1) @@ -31607,6 +31613,13 @@ snapshots: dependencies: graphql: 16.11.0 + graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1): + dependencies: + graphql: 16.11.0 + optionalDependencies: + ws: 8.18.1 + optional: true + graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3): dependencies: graphql: 16.11.0 @@ -34915,7 +34928,7 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): + nuqs@1.19.1(next@15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): dependencies: mitt: 3.0.1 next: 15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) @@ -36831,6 +36844,15 @@ snapshots: remove-trailing-spaces@1.0.9: {} + remove-types@1.0.0(supports-color@8.1.1): + dependencies: + '@babel/core': 7.26.10(supports-color@8.1.1) + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.10(supports-color@8.1.1)) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10(supports-color@8.1.1))(supports-color@8.1.1) + prettier: 2.8.8 + transitivePeerDependencies: + - supports-color + rename-keys@1.2.0: {} repeat-element@1.1.4: {}