From b55b1517291e6a143413ba1302b9ada9f8f1a99b Mon Sep 17 00:00:00 2001 From: Isaiah Hamilton <70828596+Isaiah-Hamilton@users.noreply.github.com> Date: Wed, 31 May 2023 13:13:50 -0400 Subject: [PATCH 001/117] update supasquad redirect --- apps/www/lib/redirects.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 8302c7e8aba..9622538e1b3 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1822,6 +1822,11 @@ module.exports = [ source: '/docs/handbook/supasquad', destination: '/supasquad', }, + { + permanent: true, + source: '/supasquad', + destination: '/open-source/contributing/supasquad', + }, { permanent: true, source: '/contact/enterprise', From 51e550ecc0ed0faf0ebe28a532e8a5e078fd97e5 Mon Sep 17 00:00:00 2001 From: Isaiah Hamilton <70828596+Isaiah-Hamilton@users.noreply.github.com> Date: Wed, 31 May 2023 13:13:28 -0400 Subject: [PATCH 002/117] update supasquad path --- apps/www/pages/{ => open-source/contributing}/supasquad.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/www/pages/{ => open-source/contributing}/supasquad.mdx (97%) diff --git a/apps/www/pages/supasquad.mdx b/apps/www/pages/open-source/contributing/supasquad.mdx similarity index 97% rename from apps/www/pages/supasquad.mdx rename to apps/www/pages/open-source/contributing/supasquad.mdx index 04eee13ef51..d12c13c971c 100644 --- a/apps/www/pages/supasquad.mdx +++ b/apps/www/pages/open-source/contributing/supasquad.mdx @@ -1,5 +1,5 @@ -import Layout from '../layouts/Layout' -import SectionContainer from '../components/Layouts/SectionContainer' +import Layout from '~/layouts/Layout' +import SectionContainer from '~/components/Layouts/SectionContainer' export const meta = { title: 'SupaSquad | Supabase', From 641c217a3d27e6f96615b3557df28bee04d78890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Uribe?= Date: Fri, 4 Aug 2023 17:19:56 -0500 Subject: [PATCH 003/117] Update auth-google.mdx Documentation Update: When using a nonce, Google's ID token will contain the raw value of the provided nonce, however supabase's GoTrue expects a hash of the nonce. Providing a raw nonce to both ends will result on an "invalid nonce"/ "Nonces mismatch" error (https://github.com/supabase/gotrue/blob/5caacc1f81ff20f8f09f4598b510c767e589a1c5/internal/api/token_oidc.go#L177) In order for validation to work, a hashed version of the nonce should be provided to Google and a non-hashed version to the Supabase client. --- .../guides/auth/social-login/auth-google.mdx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index a4d2728207b..ec767f1f8ba 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -289,6 +289,37 @@ async function handleSignInWithGoogle(response) { Use of nonce is recommended, though optional. Make sure each nonce is generated randomly and available both in the `data-nonce` attribute as well as in the `handleSignInWithGoogle` callback function; otherwise the ID token will not be accepted. + +### Important Note on Nonce Validation +When implementing nonce validation there is a specific requirement to understand: + +Supabase's GoTrue expects the nonce to be hashed (specifically with SHA-256, and then converted to a hexadecimal string), however Google's ID token will contain the raw value of the provided nonce. + +To succeed with nonce validation, you will need to provide a hashed version of the nonce to Google and the non-hashed version to the Supabase client. + +The following code snippet provides an example of how to create both versions in a web application: + +``` +import crypto from 'crypto'; + +... + +const nonce = crypto.randomBytes(16).toString('base64'); // Generate a random nonce +const encoder = new TextEncoder(); +const encodedNonce = encoder.encode(nonce); // Encode the nonce +const hash = await crypto.subtle.digest('SHA-256', encodedNonce); // Hash it with SHA-256 +const bytes = new Uint8Array(hash); +const hashedNonce = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string + .join(''); + +// Use 'nonce' when passing to Supabase's client +// Use 'hashedNonce' when passing to Google's authentication request +``` + +By following this approach, you'll ensure that the nonces are handled in a way that is compatible with both Google's and Supabase's expectations, allowing successful authentication. + + ### Configuration [#configuration-personalized-sign-in-button] 1. Obtain OAuth credentials for your Google Cloud project in the [Credentials](https://console.developers.google.com/apis/credentials) page of the console. When creating a new credential, choose _Web application_. As you're using the Google sign in button, you should configure the _Authorized JavaScript origins_ and _Authorized redirect URIs_ to the website where the buttons appear. You should not use your Supabase project domain name. For this use case, the client secret provided is not needed and can be ignored. From 8bb2dd1bb450072f1ad3db695a580a2c7fb2fc3a Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 4 Aug 2023 13:28:09 +0200 Subject: [PATCH 004/117] Add query and mutation for fetching and mutation auth config. --- studio/data/auth/auth-config-query.ts | 61 +++++++++++++++++++ .../data/auth/auth-config-update-mutation.ts | 58 ++++++++++++++++++ studio/data/auth/keys.ts | 1 + 3 files changed, 120 insertions(+) create mode 100644 studio/data/auth/auth-config-query.ts create mode 100644 studio/data/auth/auth-config-update-mutation.ts diff --git a/studio/data/auth/auth-config-query.ts b/studio/data/auth/auth-config-query.ts new file mode 100644 index 00000000000..f1b8dc60235 --- /dev/null +++ b/studio/data/auth/auth-config-query.ts @@ -0,0 +1,61 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { components } from 'data/api' +import { get, isResponseOk } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { authKeys } from './keys' + +export type AuthConfigVariables = { + projectRef?: string +} + +export type AuthConfigResponse = components['schemas']['GetGoTrueConfigResponse'] + +export async function getProjectAuthConfig( + { projectRef }: AuthConfigVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const response = await get(`${API_URL}/auth/${projectRef}/config`, { + signal, + }) + if (!isResponseOk(response)) { + throw response.error + } + + return response +} + +export type ProjectAuthConfigData = Awaited> +export type ProjectAuthConfigError = unknown + +export const useAuthConfigQuery = ( + { projectRef }: AuthConfigVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + authKeys.authConfig(projectRef), + ({ signal }) => getProjectAuthConfig({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) + +export const useAuthConfigPrefetch = ({ projectRef }: AuthConfigVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef) { + client.prefetchQuery(authKeys.authConfig(projectRef), ({ signal }) => + getProjectAuthConfig({ projectRef }, signal) + ) + } + }, [client, projectRef]) +} diff --git a/studio/data/auth/auth-config-update-mutation.ts b/studio/data/auth/auth-config-update-mutation.ts new file mode 100644 index 00000000000..908d9d1365f --- /dev/null +++ b/studio/data/auth/auth-config-update-mutation.ts @@ -0,0 +1,58 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { components } from 'data/api' +import { patch } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { ResponseError } from 'types' +import { authKeys } from './keys' + +export type AuthConfigUpdateVariables = { + projectRef: string + config: Partial +} + +export type UpdateAuthConfigResponse = components['schemas']['GoTrueConfig'] + +export async function updateAuthConfig({ projectRef, config }: AuthConfigUpdateVariables) { + const response = await patch(`${API_URL}/auth/${projectRef}/config`, { ...config }) + if (response.error) { + throw response.error + } + + return response as UpdateAuthConfigResponse +} + +type AuthConfigUpdateData = Awaited> + +export const useAuthConfigUpdateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => updateAuthConfig(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await Promise.all([queryClient.invalidateQueries(authKeys.authConfig(projectRef))]) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to mutate: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/data/auth/keys.ts b/studio/data/auth/keys.ts index 052713f52ff..9dbece0ae3c 100644 --- a/studio/data/auth/keys.ts +++ b/studio/data/auth/keys.ts @@ -1,3 +1,4 @@ export const authKeys = { + authConfig: (projectRef: string | undefined) => ['auth', projectRef, 'config'] as const, accessToken: () => ['access-token'] as const, } From 678d79d3694c6b618cdf04d4c53de855603962d8 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 4 Aug 2023 13:31:06 +0200 Subject: [PATCH 005/117] Change the page layouts and the main auth layout to not use the Auth Store. --- .../components/layouts/AuthLayout/AuthLayout.tsx | 5 +++-- studio/pages/project/[ref]/auth/providers.tsx | 10 +++------- studio/pages/project/[ref]/auth/templates.tsx | 12 +++--------- .../project/[ref]/auth/url-configuration.tsx | 10 +++------- studio/pages/project/[ref]/settings/auth.tsx | 16 +++------------- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/studio/components/layouts/AuthLayout/AuthLayout.tsx b/studio/components/layouts/AuthLayout/AuthLayout.tsx index 72456fe6fc2..dd060084a53 100644 --- a/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -5,6 +5,7 @@ import { PropsWithChildren, useEffect, useState } from 'react' import { useParams } from 'common' import Error from 'components/ui/Error' import ProductMenu from 'components/ui/ProductMenu' +import { useAuthConfigPrefetch } from 'data/auth/auth-config-query' import { useStore, withAuth } from 'hooks' import ProjectLayout from '../' import { generateAuthMenu } from './AuthLayout.utils' @@ -14,9 +15,10 @@ export interface AuthLayoutProps { } const AuthLayout = ({ title, children }: PropsWithChildren) => { - const { ui, meta, authConfig } = useStore() + const { ui, meta } = useStore() const { isInitialized, isLoading, error } = meta.tables const { ref: projectRef = 'default' } = useParams() + useAuthConfigPrefetch({ projectRef }) const router = useRouter() const page = router.pathname.split('/')[4] @@ -29,7 +31,6 @@ const AuthLayout = ({ title, children }: PropsWithChildren) => meta.tables.load() meta.roles.load() meta.schemas.load() - authConfig.load() } }, [ui.selectedProjectRef]) diff --git a/studio/pages/project/[ref]/auth/providers.tsx b/studio/pages/project/[ref]/auth/providers.tsx index 1ddd4ccc705..6524762fc9a 100644 --- a/studio/pages/project/[ref]/auth/providers.tsx +++ b/studio/pages/project/[ref]/auth/providers.tsx @@ -1,27 +1,23 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' import { AuthProvidersForm } from 'components/interfaces' import { AuthLayout } from 'components/layouts' import { FormsContainer } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions } from 'hooks' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { - const { authConfig } = useStore() const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { return - } else if (authConfig) { + } else { return ( ) - } else { - return
} } @@ -29,4 +25,4 @@ PageLayout.getLayout = (page) => { return {page} } -export default observer(PageLayout) +export default PageLayout diff --git a/studio/pages/project/[ref]/auth/templates.tsx b/studio/pages/project/[ref]/auth/templates.tsx index fe0f5d01b77..3d1d6bd1d16 100644 --- a/studio/pages/project/[ref]/auth/templates.tsx +++ b/studio/pages/project/[ref]/auth/templates.tsx @@ -1,29 +1,23 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' import { EmailTemplates } from 'components/interfaces' import { AuthLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormsContainer } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions } from 'hooks' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { - const { authConfig } = useStore() const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { return - } else if (authConfig) { + } else { return ( ) - } else { - return
} } @@ -31,4 +25,4 @@ PageLayout.getLayout = (page) => { return {page} } -export default observer(PageLayout) +export default PageLayout diff --git a/studio/pages/project/[ref]/auth/url-configuration.tsx b/studio/pages/project/[ref]/auth/url-configuration.tsx index 896c81a5225..09c276ed205 100644 --- a/studio/pages/project/[ref]/auth/url-configuration.tsx +++ b/studio/pages/project/[ref]/auth/url-configuration.tsx @@ -1,29 +1,25 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' import RedirectUrls from 'components/interfaces/Auth/RedirectUrls/RedirectUrls' import SiteUrl from 'components/interfaces/Auth/SiteUrl/SiteUrl' import { AuthLayout } from 'components/layouts' import { FormsContainer } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions } from 'hooks' import { NextPageWithLayout } from 'types' const URLConfiguration: NextPageWithLayout = () => { - const { authConfig } = useStore() const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { return - } else if (authConfig) { + } else { return ( ) - } else { - return null } } @@ -31,4 +27,4 @@ URLConfiguration.getLayout = (page) => { return {page} } -export default observer(URLConfiguration) +export default URLConfiguration diff --git a/studio/pages/project/[ref]/settings/auth.tsx b/studio/pages/project/[ref]/settings/auth.tsx index 942ade35fb3..70c5b6696b7 100644 --- a/studio/pages/project/[ref]/settings/auth.tsx +++ b/studio/pages/project/[ref]/settings/auth.tsx @@ -1,33 +1,23 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' import { AutoSchemaForm, SmtpForm } from 'components/interfaces/Auth' import { SettingsLayout } from 'components/layouts' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, useStore } from 'hooks' +import { useCheckPermissions } from 'hooks' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { - const { authConfig, ui } = useStore() - - useEffect(() => { - authConfig.load() - }, [ui.selectedProjectRef]) - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { return - } else if (authConfig) { + } else { return (
) - } else { - return
} } @@ -35,4 +25,4 @@ PageLayout.getLayout = (page) => { return {page} } -export default observer(PageLayout) +export default PageLayout From 1c95b0766cee1e4c433b14efdbfbbdff2a7081e1 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 4 Aug 2023 14:28:50 +0200 Subject: [PATCH 006/117] Rewrite the Auth components using react-query. --- .../AuthProvidersForm/AuthProvidersForm.tsx | 15 ++-- .../Auth/AuthProvidersForm/ProviderForm.tsx | 66 ++++++++------ .../interfaces/Auth/AutoSchemaForm.tsx | 76 +++++++++------- .../Auth/EmailTemplates/TemplateEditor.tsx | 60 +++++++------ .../Auth/RedirectUrls/RedirectUrlList.tsx | 12 +-- .../Auth/RedirectUrls/RedirectUrls.tsx | 87 +++++++++++-------- .../interfaces/Auth/SiteUrl/SiteUrl.tsx | 68 ++++++++------- .../interfaces/Auth/SmtpForm/SmtpForm.tsx | 50 ++++++----- .../Auth/SmtpForm/SmtpForm.utils.ts | 6 +- 9 files changed, 256 insertions(+), 184 deletions(-) diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx index f9ab8236697..695e69e8d72 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx +++ b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx @@ -1,13 +1,16 @@ -import { observer } from 'mobx-react-lite' -import { useStore, useFlag } from 'hooks' -import { PROVIDERS_SCHEMAS, PROVIDER_SAML } from 'stores/authConfig/schema' +import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers' -import ProviderForm from './ProviderForm' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useFlag } from 'hooks' +import { observer } from 'mobx-react-lite' +import { PROVIDERS_SCHEMAS, PROVIDER_SAML } from 'stores/authConfig/schema' import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants' +import ProviderForm from './ProviderForm' const AuthProvidersForm = () => { - const { authConfig } = useStore() + const { ref: projectRef } = useParams() + const { isFetched } = useAuthConfigQuery({ projectRef }) const isSamlEnabled = useFlag('isSamlEnabled') const providers = isSamlEnabled ? PROVIDERS_SCHEMAS @@ -21,7 +24,7 @@ const AuthProvidersForm = () => { />
- {!authConfig.isLoaded + {!isFetched ? providers.map((provider) => (
{ const [open, setOpen] = useState(false) - const { authConfig, ui } = useStore() - const { project: selectedProject } = useProjectContext() - const { ref } = useParams() + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data: config } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM'] const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') - const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) + const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) const generateInitialValues = () => { const initialValues: { [x: string]: string } = {} + // the config is already loaded through the parent component + if (!config) { + return + } Object.keys(provider.properties).forEach((key) => { // When the key is a 'double negative' key, we must reverse the boolean before adding it to the form const isDoubleNegative = doubleNegativeKeys.includes(key) if (provider.title === 'SAML 2.0') { - initialValues[key] = authConfig.config[key] ?? false + initialValues[key] = (config as any)[key] ?? false } else { - initialValues[key] = isDoubleNegative - ? !authConfig.config[key] - : authConfig.config[key] ?? '' + initialValues[key] = isDoubleNegative ? !(config as any)[key] : (config as any)[key] ?? '' } }) return initialValues } // [Joshen] Doing this check as SAML doesn't follow the same naming structure as the other provider options - const isActive = + const isActive: boolean = provider.title === 'SAML 2.0' - ? authConfig.config['SAML_ENABLED'] || false - : authConfig.config[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`] + ? (config && (config as any)['SAML_ENABLED']) ?? false + : (config && (config as any)[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`]) ?? false const INITIAL_VALUES = generateInitialValues() const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { @@ -61,21 +66,26 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { } }) - const { error } = await authConfig.update(payload) - - if (!error) { - resetForm({ values: { ...values }, initialValues: { ...values } }) - setOpen(false) - ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) - } else { - ui.setNotification({ - error, - category: 'error', - message: `Failed to update settings: ${error?.message}`, - }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to update settings: ${error?.message}`, + }) + }, + onSuccess: () => { + resetForm({ values: { ...values }, initialValues: { ...values } }) + setOpen(false) + ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } return ( @@ -159,7 +169,7 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { value={ customDomainData?.customDomain?.status === 'active' ? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback` - : `https://${selectedProject?.ref}.supabase.co/auth/v1/callback` + : `https://${projectRef}.supabase.co/auth/v1/callback` } descriptionText={ diff --git a/studio/components/interfaces/Auth/AutoSchemaForm.tsx b/studio/components/interfaces/Auth/AutoSchemaForm.tsx index 72722a74e46..92e66631322 100644 --- a/studio/components/interfaces/Auth/AutoSchemaForm.tsx +++ b/studio/components/interfaces/Auth/AutoSchemaForm.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState } from 'react' -import { observer } from 'mobx-react-lite' -import { boolean, number, object, string } from 'yup' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Button, Form, Input, IconEye, IconEyeOff, InputNumber, Toggle, Radio } from 'ui' +import { observer } from 'mobx-react-lite' +import { useEffect, useState } from 'react' +import { Button, Form, IconEye, IconEyeOff, Input, InputNumber, Radio, Toggle } from 'ui' +import { boolean, number, object, string } from 'yup' -import { useStore, useCheckPermissions } from 'hooks' +import { useParams } from 'common' import { FormActions, FormHeader, @@ -13,25 +13,30 @@ import { FormSectionContent, FormSectionLabel, } from 'components/ui/Forms' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { useCheckPermissions, useStore } from 'hooks' const AutoSchemaForm = observer(() => { - const { authConfig, ui } = useStore() - const { isLoaded } = authConfig + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-general-form' const [hidden, setHidden] = useState(true) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const INITIAL_VALUES = { - DISABLE_SIGNUP: !authConfig.config.DISABLE_SIGNUP, - SITE_URL: authConfig.config.SITE_URL, - JWT_EXP: authConfig.config.JWT_EXP, - REFRESH_TOKEN_ROTATION_ENABLED: authConfig.config.REFRESH_TOKEN_ROTATION_ENABLED || false, - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig.config.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, - SECURITY_CAPTCHA_ENABLED: authConfig.config.SECURITY_CAPTCHA_ENABLED || false, - SECURITY_CAPTCHA_SECRET: authConfig.config.SECURITY_CAPTCHA_SECRET || '', - SECURITY_CAPTCHA_PROVIDER: authConfig.config.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', - MFA_MAX_ENROLLED_FACTORS: authConfig.config.MFA_MAX_ENROLLED_FACTORS || 10, + DISABLE_SIGNUP: config?.DISABLE_SIGNUP, + SITE_URL: config?.SITE_URL, + JWT_EXP: config?.JWT_EXP, + REFRESH_TOKEN_ROTATION_ENABLED: config?.REFRESH_TOKEN_ROTATION_ENABLED || false, + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: config?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, + SECURITY_CAPTCHA_ENABLED: config?.SECURITY_CAPTCHA_ENABLED || false, + SECURITY_CAPTCHA_SECRET: config?.SECURITY_CAPTCHA_SECRET || '', + SECURITY_CAPTCHA_PROVIDER: config?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', + MFA_MAX_ENROLLED_FACTORS: config?.MFA_MAX_ENROLLED_FACTORS || 10, } const schema = object({ @@ -65,22 +70,27 @@ const AutoSchemaForm = observer(() => { payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP setSubmitting(true) - const { error } = await authConfig.update(payload) - - if (!error) { - ui.setNotification({ - category: 'success', - message: `Successfully updated settings`, - }) - resetForm({ values: values, initialValues: values }) - } else { - ui.setNotification({ - category: 'error', - message: `Failed to update settings: ${error?.message}`, - }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: (error) => { + ui.setNotification({ + category: 'error', + message: `Failed to update settings: ${error?.message}`, + }) + }, + onSuccess: () => { + ui.setNotification({ + category: 'success', + message: `Successfully updated settings`, + }) + resetForm({ values: values, initialValues: values }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } return ( @@ -91,7 +101,7 @@ const AutoSchemaForm = observer(() => { // Form is reset once remote data is loaded in store useEffect(() => { resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [authConfig.isLoaded]) + }, [isLoaded]) return ( <> diff --git a/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index 4ab4bdf3300..fe168d7f496 100644 --- a/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -7,6 +7,10 @@ import { Form, Input } from 'ui' import CodeEditor from 'components/ui/CodeEditor' import { FormActions, FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' import InformationBox from 'components/ui/InformationBox' + +import { useParams } from 'common' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions, useStore } from 'hooks' import { FormSchema } from 'types' @@ -15,11 +19,13 @@ interface TemplateEditorProps { } const TemplateEditor = ({ template }: TemplateEditorProps) => { - const { ui, authConfig } = useStore() + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const [bodyValue, setBodyValue] = useState('') - - const { isLoaded } = authConfig const { id, properties } = template + const config = data as Record | undefined const formId = `auth-config-email-templates-${id}` const INITIAL_VALUES: { [x: string]: string } = {} @@ -28,7 +34,7 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { useEffect(() => { if (isLoaded) { Object.keys(properties).forEach((key) => { - INITIAL_VALUES[key] = authConfig.config[key] ?? '' + INITIAL_VALUES[key] = (config && config[key]) ?? '' }) } }, [isLoaded]) @@ -36,7 +42,7 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { const messageSlug = `MAILER_TEMPLATES_${id}_CONTENT` const messageProperty = properties[messageSlug] - const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { const payload = { ...values } // Because the template content uses the code editor which is not a form component @@ -45,20 +51,24 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { if (messageProperty) payload[messageSlug] = bodyValue setSubmitting(true) - const { error } = await authConfig.update(payload) - - if (!error) { - ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) - resetForm({ - values: values, - initialValues: values, - }) - setBodyValue(authConfig?.config?.[messageSlug]) - } else { - ui.setNotification({ category: 'error', message: 'Failed to update settings' }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: () => { + ui.setNotification({ category: 'error', message: 'Failed to update settings' }) + }, + onSuccess: () => { + ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) + resetForm({ + values: values, + initialValues: values, + }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } return ( @@ -73,10 +83,10 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { values: INITIAL_VALUES, initialValues: INITIAL_VALUES, }) - setBodyValue(authConfig?.config?.[messageSlug]) - }, [authConfig.isLoaded]) + setBodyValue((config && config[messageSlug]) ?? '') + }, [isLoaded]) - const message = authConfig?.config?.[messageSlug] + const message = (config && config[messageSlug]) ?? '' const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) || message !== bodyValue @@ -157,10 +167,10 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { { resetForm({ - values: authConfig.config, - initialValues: authConfig.config, + values: config, + initialValues: config, }) - setBodyValue(authConfig?.config?.[messageSlug]) + setBodyValue((config && config[messageSlug]) ?? '') }} form={formId} isSubmitting={isSubmitting} diff --git a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx index 81bbb966fda..30e9f13ba10 100644 --- a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx +++ b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx @@ -2,9 +2,10 @@ import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { Button, IconGlobe, IconTrash } from 'ui' +import { useParams } from 'common' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers' import { EmptyListState } from 'components/ui/States' -import { useStore } from 'hooks' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' import ValueContainer from './ValueContainer' interface RedirectUrlListProps { @@ -13,15 +14,14 @@ interface RedirectUrlListProps { } const RedirectUrlList = ({ canUpdate, onSelectUrlToDelete = noop }: RedirectUrlListProps) => { - const { authConfig } = useStore() + const { ref: projectRef } = useParams() + const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - const URI_ALLOW_LIST_ARRAY = authConfig.config.URI_ALLOW_LIST - ? authConfig.config.URI_ALLOW_LIST.split(',') - : [] + const URI_ALLOW_LIST_ARRAY = config?.URI_ALLOW_LIST ? config.URI_ALLOW_LIST.split(',') : [] return (
- {!authConfig.isLoaded ? ( + {!isLoaded ? ( <> diff --git a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx index 19ed2369767..95010a466f4 100644 --- a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx +++ b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx @@ -1,20 +1,26 @@ -import { useState } from 'react' -import { object, string } from 'yup' -import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' -import { Button, Form, Input, Modal } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { observer } from 'mobx-react-lite' +import { useState } from 'react' +import { Button, Form, Input, Modal } from 'ui' +import { object, string } from 'yup' -import { useCheckPermissions, useStore } from 'hooks' +import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { useCheckPermissions, useStore } from 'hooks' import { urlRegex } from '../Auth.constants' import RedirectUrlList from './RedirectUrlList' const RedirectUrls = () => { - const { authConfig, ui } = useStore() + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data: config } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() - const URI_ALLOW_LIST_ARRAY = authConfig.config.URI_ALLOW_LIST - ? authConfig.config.URI_ALLOW_LIST.split(/\s*[,]+\s*/).filter((url: string) => url) + const URI_ALLOW_LIST_ARRAY = config?.URI_ALLOW_LIST + ? config.URI_ALLOW_LIST.split(/\s*[,]+\s*/).filter((url: string) => url) : [] const [open, setOpen] = useState(false) @@ -49,19 +55,25 @@ const RedirectUrls = () => { return } - const { error } = await authConfig.update({ URI_ALLOW_LIST: payloadString }) - if (!error) { - setOpen(false) - ui.setNotification({ category: 'success', message: 'Successfully added URL' }) - } else { - ui.setNotification({ - error, - category: 'error', - message: `Failed to update URL: ${error?.message}`, - }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: { URI_ALLOW_LIST: payloadString } }, + { + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to update URL: ${error?.message}`, + }) + }, + onSuccess: () => { + setOpen(false) + ui.setNotification({ category: 'success', message: 'Successfully added URL' }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } const onConfirmDeleteUrl = async (url?: string) => { @@ -72,20 +84,25 @@ const RedirectUrls = () => { // Remove selectedUrl from array and update const payload = URI_ALLOW_LIST_ARRAY.filter((e: string) => e !== url) - const { error } = await authConfig.update({ URI_ALLOW_LIST: payload.toString() }) - - if (!error) { - setSelectedUrlToDelete(undefined) - ui.setNotification({ category: 'success', message: 'Successfully removed URL' }) - } else { - ui.setNotification({ - error, - category: 'error', - message: `Failed to remove URL: ${error?.message}`, - }) - } - - setIsDeleting(false) + updateAuthConfig( + { projectRef: projectRef!, config: { URI_ALLOW_LIST: payload.toString() } }, + { + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to remove URL: ${error?.message}`, + }) + }, + onSuccess: () => { + setSelectedUrlToDelete(undefined) + ui.setNotification({ category: 'success', message: 'Successfully removed URL' }) + }, + onSettled: () => { + setIsDeleting(false) + }, + } + ) } return ( diff --git a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index b19659bb96c..2470fed6324 100644 --- a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react' import { Form, Input } from 'ui' import { boolean, number, object, string } from 'yup' +import { useParams } from 'common' import { FormActions, FormHeader, @@ -11,24 +12,28 @@ import { FormSection, FormSectionContent, } from 'components/ui/Forms' -import { useCheckPermissions, useSelectedProject, useStore } from 'hooks' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { useCheckPermissions, useStore } from 'hooks' const SiteUrl = observer(() => { - const { authConfig, ui } = useStore() - const { isLoaded } = authConfig + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-general-form' const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const INITIAL_VALUES = { - DISABLE_SIGNUP: !authConfig.config.DISABLE_SIGNUP, - JWT_EXP: authConfig.config.JWT_EXP, - SITE_URL: authConfig.config.SITE_URL, - REFRESH_TOKEN_ROTATION_ENABLED: authConfig.config.REFRESH_TOKEN_ROTATION_ENABLED || false, - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig.config.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, - SECURITY_CAPTCHA_ENABLED: authConfig.config.SECURITY_CAPTCHA_ENABLED || false, - SECURITY_CAPTCHA_PROVIDER: authConfig.config.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', - SECURITY_CAPTCHA_SECRET: authConfig.config.SECURITY_CAPTCHA_SECRET || '', + DISABLE_SIGNUP: config?.DISABLE_SIGNUP, + JWT_EXP: config?.JWT_EXP, + SITE_URL: config?.SITE_URL, + REFRESH_TOKEN_ROTATION_ENABLED: config?.REFRESH_TOKEN_ROTATION_ENABLED || false, + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: config?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, + SECURITY_CAPTCHA_ENABLED: config?.SECURITY_CAPTCHA_ENABLED || false, + SECURITY_CAPTCHA_PROVIDER: config?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', + SECURITY_CAPTCHA_SECRET: config?.SECURITY_CAPTCHA_SECRET || '', } const schema = object({ @@ -48,27 +53,32 @@ const SiteUrl = observer(() => { }), }) - const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP setSubmitting(true) - const { error } = await authConfig.update(payload) - - if (!error) { - ui.setNotification({ - category: 'success', - message: `Successfully updated settings`, - }) - resetForm({ values: values, initialValues: values }) - } else { - ui.setNotification({ - category: 'error', - message: `Failed to update settings`, - }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: () => { + ui.setNotification({ + category: 'error', + message: `Failed to update settings`, + }) + }, + onSuccess: () => { + ui.setNotification({ + category: 'success', + message: `Successfully updated settings`, + }) + resetForm({ values: values, initialValues: values }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } return ( @@ -79,7 +89,7 @@ const SiteUrl = observer(() => { // Form is reset once remote data is loaded in store useEffect(() => { resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [authConfig.isLoaded]) + }, [isLoaded]) return ( <> diff --git a/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx index 4479917bd5c..3bd9dffefd0 100644 --- a/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx +++ b/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx @@ -17,6 +17,7 @@ import { } from 'ui' import { number, object, string } from 'yup' +import { useParams } from 'common' import { FormActions, FormHeader, @@ -25,20 +26,24 @@ import { FormSectionContent, FormSectionLabel, } from 'components/ui/Forms' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions, useStore } from 'hooks' import { urlRegex } from './../Auth.constants' import { defaultDisabledSmtpFormValues } from './SmtpForm.constants' import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils' const SmtpForm = () => { - const { authConfig, ui } = useStore() - const { config, isLoaded } = authConfig + const { ui } = useStore() + const { ref: projectRef } = useParams() + const { data: config, isLoading, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const [enableSmtp, setEnableSmtp] = useState(false) const [hidden, setHidden] = useState(true) const formId = 'auth-config-smtp-form' - const initialValues = generateFormValues(authConfig.config) + const initialValues = generateFormValues(config) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') useEffect(() => { @@ -110,7 +115,7 @@ const SmtpForm = () => { }), }) - const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { const payload = enableSmtp ? values : defaultDisabledSmtpFormValues // Format payload: Remove redundant value + convert port to string @@ -118,18 +123,23 @@ const SmtpForm = () => { payload.SMTP_PORT = payload.SMTP_PORT ? payload.SMTP_PORT.toString() : payload.SMTP_PORT setSubmitting(true) - const { error } = await authConfig.update(payload) - - if (!error) { - setHidden(true) - const updatedFormValues = generateFormValues(payload) - resetForm({ values: updatedFormValues, initialValues: updatedFormValues }) - ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) - } else { - ui.setNotification({ category: 'error', message: 'Failed to update settings', error }) - } - - setSubmitting(false) + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: (error) => { + ui.setNotification({ category: 'error', message: 'Failed to update settings', error }) + }, + onSuccess: () => { + setHidden(true) + const updatedFormValues = generateFormValues(payload) + resetForm({ values: updatedFormValues, initialValues: updatedFormValues }) + ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) + }, + onSettled: () => { + setSubmitting(false) + }, + } + ) } return ( @@ -178,7 +188,7 @@ const SmtpForm = () => { } > - + { documentation {' '} @@ -233,7 +243,7 @@ const SmtpForm = () => { header={Sender details} disabled={!enableSmtp} > - + { } > - + ): Partial Date: Fri, 4 Aug 2023 14:29:26 +0200 Subject: [PATCH 007/117] Remove the mobx AuthStore since all of its functionality has been moved to react-query. --- studio/stores/RootStore.ts | 11 +- .../authConfig/ProjectAuthConfigStore.ts | 113 ------------------ 2 files changed, 2 insertions(+), 122 deletions(-) delete mode 100644 studio/stores/authConfig/ProjectAuthConfigStore.ts diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index 0846dd49eef..f7d8e557d06 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -2,14 +2,11 @@ import { configure } from 'mobx' import { Project } from 'types' import AppStore, { IAppStore } from './app/AppStore' import MetaStore, { IMetaStore } from './pgmeta/MetaStore' -import UiStore, { IUiStore } from './UiStore' +import ProjectBackupsStore, { IProjectBackupsStore } from './project/ProjectBackupsStore' import ProjectContentStore, { IProjectContentStore } from './project/ProjectContentStore' import ProjectFunctionsStore, { IProjectFunctionsStore } from './project/ProjectFunctionsStore' -import ProjectBackupsStore, { IProjectBackupsStore } from './project/ProjectBackupsStore' -import ProjectAuthConfigStore, { - IProjectAuthConfigStore, -} from './authConfig/ProjectAuthConfigStore' import VaultStore, { IVaultStore } from './project/VaultStore' +import UiStore, { IUiStore } from './UiStore' // Temporary disable mobx warnings // TODO: need to remove this after refactoring old stores. @@ -24,7 +21,6 @@ export interface IRootStore { content: IProjectContentStore functions: IProjectFunctionsStore backups: IProjectBackupsStore - authConfig: IProjectAuthConfigStore vault: IVaultStore selectedProjectRef?: string @@ -38,7 +34,6 @@ export class RootStore implements IRootStore { content: IProjectContentStore functions: IProjectFunctionsStore backups: IProjectBackupsStore - authConfig: IProjectAuthConfigStore vault: IVaultStore selectedProjectRef: string | undefined @@ -51,7 +46,6 @@ export class RootStore implements IRootStore { this.content = new ProjectContentStore(this, { projectRef: '' }) this.functions = new ProjectFunctionsStore(this, { projectRef: '' }) this.backups = new ProjectBackupsStore(this, { projectRef: '' }) - this.authConfig = new ProjectAuthConfigStore(this, { projectRef: '' }) this.vault = new VaultStore(this) } @@ -69,7 +63,6 @@ export class RootStore implements IRootStore { this.meta.setProjectDetails(project) this.functions.setProjectRef(project.ref) - this.authConfig.setProjectRef(project.ref) this.content.setProjectRef(project.ref) this.backups.setProjectRef(project.ref) // ui set must come last diff --git a/studio/stores/authConfig/ProjectAuthConfigStore.ts b/studio/stores/authConfig/ProjectAuthConfigStore.ts deleted file mode 100644 index 92c43762a59..00000000000 --- a/studio/stores/authConfig/ProjectAuthConfigStore.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { makeAutoObservable, toJS } from 'mobx' -import { UserContentMap } from 'types' -import { IRootStore } from '../RootStore' -import { API_URL } from 'lib/constants' -import { get, patch } from 'lib/common/fetch' - -export interface IProjectAuthConfigStore { - isLoading: boolean - isInitialized: boolean - isLoaded: boolean - error: any - config: any - - baseUrl: string - projectRef: string - - load: () => void - update: (updates: any) => Promise - setProjectRef: (ref?: string) => void -} - -export default class ProjectAuthConfigStore implements IProjectAuthConfigStore { - rootStore: IRootStore - STATES = { - INITIAL: 'initial', - LOADING: 'loading', - ERROR: 'error', - LOADED: 'loaded', - } - baseUrl: string - projectRef: string - // @ts-ignore - data: UserContentMap = [] - - state = this.STATES.INITIAL - error = null - - constructor(rootStore: IRootStore, options: { projectRef: string }) { - const { projectRef } = options - this.projectRef = projectRef - this.rootStore = rootStore - this.baseUrl = `` - makeAutoObservable(this) - } - - get isLoading() { - return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING - } - - get isInitialized() { - return this.state !== this.STATES.INITIAL - } - - get isLoaded() { - return this.state === this.STATES.LOADED - } - - get config() { - return this.data - } - - async fetchData() { - const headers = { 'Content-Type': 'application/json' } - const response = await get(this.baseUrl, { headers }) - - if (response.error) throw response.error - this.data = response - return response - } - - async load() { - let { LOADING, ERROR, LOADED } = this.STATES - try { - this.error = null - this.state = LOADING - await this.fetchData() - this.state = LOADED - return this.data - } catch (e: any) { - this.error = e - this.state = ERROR - } - } - - byId(id: any) { - return toJS(this.data)[id] - } - - async update(updates: any) { - try { - const headers = { - 'Content-Type': 'application/json', - } - let payload = { - ...updates, - } - const url = `${this.baseUrl}` - const updated = await patch(url, payload, { headers }) - if (updated.error) throw updated.error - this.data = updated - return { data: updated, error: null } - } catch (error) { - return { data: null, error } - } - } - - setProjectRef(ref?: string) { - if (ref) { - this.projectRef = ref - this.baseUrl = `${API_URL}/auth/${ref}/config` - } - } -} From 7670bdf9def101008f1c42c90b200f4a426c1db0 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 10 Aug 2023 13:36:33 +0200 Subject: [PATCH 008/117] Fix the query and mutation comments. --- studio/data/auth/auth-config-query.ts | 18 +++++++------ .../data/auth/auth-config-update-mutation.ts | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/studio/data/auth/auth-config-query.ts b/studio/data/auth/auth-config-query.ts index f1b8dc60235..7a933c9ab40 100644 --- a/studio/data/auth/auth-config-query.ts +++ b/studio/data/auth/auth-config-query.ts @@ -1,8 +1,8 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import { components } from 'data/api' -import { get, isResponseOk } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { get } from 'data/fetchers' import { useCallback } from 'react' +import { ResponseError } from 'types' import { authKeys } from './keys' export type AuthConfigVariables = { @@ -19,18 +19,20 @@ export async function getProjectAuthConfig( throw new Error('projectRef is required') } - const response = await get(`${API_URL}/auth/${projectRef}/config`, { + const { data, error } = await get('/platform/auth/{ref}/config', { + params: { + path: { ref: projectRef }, + }, signal, }) - if (!isResponseOk(response)) { - throw response.error - } - return response + if (error) throw error + + return data } export type ProjectAuthConfigData = Awaited> -export type ProjectAuthConfigError = unknown +export type ProjectAuthConfigError = ResponseError export const useAuthConfigQuery = ( { projectRef }: AuthConfigVariables, diff --git a/studio/data/auth/auth-config-update-mutation.ts b/studio/data/auth/auth-config-update-mutation.ts index 908d9d1365f..f05ff956cb5 100644 --- a/studio/data/auth/auth-config-update-mutation.ts +++ b/studio/data/auth/auth-config-update-mutation.ts @@ -2,8 +2,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react import toast from 'react-hot-toast' import { components } from 'data/api' -import { patch } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { patch } from 'data/fetchers' import { ResponseError } from 'types' import { authKeys } from './keys' @@ -15,12 +14,22 @@ export type AuthConfigUpdateVariables = { export type UpdateAuthConfigResponse = components['schemas']['GoTrueConfig'] export async function updateAuthConfig({ projectRef, config }: AuthConfigUpdateVariables) { - const response = await patch(`${API_URL}/auth/${projectRef}/config`, { ...config }) - if (response.error) { - throw response.error - } + const { data, error } = await patch('/platform/auth/{ref}/config', { + params: { + path: { ref: projectRef }, + }, + body: { + // TODO: The config sent to the API can be partial according to the backend implementation. + // The definition for the body https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/auth/ref/config.dto.ts#L16 is wrong. + // Every property type should be optional (with ?). When that's fixed, the new generated code will have the correct type. + // Can be removed once https://github.com/supabase/infrastructure/pull/14167 goes in. + ...(config as any), + }, + }) - return response as UpdateAuthConfigResponse + if (error) throw error + + return data } type AuthConfigUpdateData = Awaited> @@ -47,7 +56,7 @@ export const useAuthConfigUpdateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to mutate: ${data.message}`) + toast.error(`Failed to update auth configuration: ${data.message}`) } else { onError(data, variables, context) } From 339bb538b63375f63c17e8d7c44c54f168385530 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 11 Aug 2023 14:23:00 +0200 Subject: [PATCH 009/117] Fix all remarks from Joshens review. --- .../AuthProvidersForm/AuthProvidersForm.tsx | 47 ++++++--- .../Auth/AuthProvidersForm/ProviderForm.tsx | 25 ++--- .../interfaces/Auth/AutoSchemaForm.tsx | 75 +++++++++----- .../Auth/EmailTemplates/EmailTemplates.tsx | 79 ++++++++++----- .../Auth/EmailTemplates/TemplateEditor.tsx | 64 +++++------- .../Auth/RedirectUrls/RedirectUrlList.tsx | 31 ++---- .../Auth/RedirectUrls/RedirectUrls.tsx | 82 +++++++++++----- .../interfaces/Auth/SiteUrl/SiteUrl.tsx | 97 +++++++++++-------- .../interfaces/Auth/SmtpForm/SmtpForm.tsx | 43 +++++--- 9 files changed, 323 insertions(+), 220 deletions(-) diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx index 695e69e8d72..bce8a499cea 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx +++ b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx @@ -5,12 +5,20 @@ import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useFlag } from 'hooks' import { observer } from 'mobx-react-lite' import { PROVIDERS_SCHEMAS, PROVIDER_SAML } from 'stores/authConfig/schema' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, IconAlertCircle } from 'ui' import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants' import ProviderForm from './ProviderForm' const AuthProvidersForm = () => { const { ref: projectRef } = useParams() - const { isFetched } = useAuthConfigQuery({ projectRef }) + const { + isLoading, + error: authConfigError, + isError, + data: authConfig, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + const isSamlEnabled = useFlag('isSamlEnabled') const providers = isSamlEnabled ? PROVIDERS_SCHEMAS @@ -24,19 +32,32 @@ const AuthProvidersForm = () => { />
- {!isFetched - ? providers.map((provider) => ( -
( +
+ +
+ ))} + {isError && ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + )} + {isSuccess && + providers.map((provider) => { + return ( + - -
- )) - : providers.map((provider) => { - // @ts-expect-error: Fix type to be dynamic - return - })} + config={authConfig} + provider={provider as any} + /> + ) + })}
) diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index d5bd2fd542c..6d6d3a565c0 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -5,7 +5,7 @@ import ReactMarkdown from 'react-markdown' import { Alert, Button, Collapsible, Form, IconCheck, IconChevronUp, Input } from 'ui' import { useParams } from 'common' -import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { components } from 'data/api' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useCheckPermissions, useStore } from 'hooks' @@ -15,16 +15,15 @@ import { Provider } from './AuthProvidersForm.types' import FormField from './FormField' export interface ProviderFormProps { + config: components['schemas']['GetGoTrueConfigResponse'] provider: Provider } -const ProviderForm = ({ provider }: ProviderFormProps) => { +const ProviderForm = ({ config, provider }: ProviderFormProps) => { const [open, setOpen] = useState(false) const { ui } = useStore() const { ref: projectRef } = useParams() - const { data: config } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() - + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM'] const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') @@ -33,9 +32,6 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { const generateInitialValues = () => { const initialValues: { [x: string]: string } = {} // the config is already loaded through the parent component - if (!config) { - return - } Object.keys(provider.properties).forEach((key) => { // When the key is a 'double negative' key, we must reverse the boolean before adding it to the form const isDoubleNegative = doubleNegativeKeys.includes(key) @@ -56,7 +52,7 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { : (config && (config as any)[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`]) ?? false const INITIAL_VALUES = generateInitialValues() - const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } // When the key is a 'double negative' key, we must reverse the boolean before the payload can be sent @@ -81,9 +77,6 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { setOpen(false) ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } @@ -134,7 +127,7 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { validationSchema={provider.validationSchema} onSubmit={onSubmit} > - {({ isSubmitting, handleReset, initialValues, values }: any) => { + {({ handleReset, initialValues, values }: any) => { const noChanges = JSON.stringify(initialValues) === JSON.stringify(values) return ( @@ -187,7 +180,7 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { handleReset() setOpen(false) }} - disabled={isSubmitting} + disabled={isUpdatingConfig} > Cancel @@ -195,8 +188,8 @@ const ProviderForm = ({ provider }: ProviderFormProps) => { diff --git a/studio/components/interfaces/Auth/AutoSchemaForm.tsx b/studio/components/interfaces/Auth/AutoSchemaForm.tsx index 92e66631322..0c48238e8ad 100644 --- a/studio/components/interfaces/Auth/AutoSchemaForm.tsx +++ b/studio/components/interfaces/Auth/AutoSchemaForm.tsx @@ -1,7 +1,20 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' import { useEffect, useState } from 'react' -import { Button, Form, IconEye, IconEyeOff, Input, InputNumber, Radio, Toggle } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Form, + IconAlertCircle, + IconEye, + IconEyeOff, + Input, + InputNumber, + Radio, + Toggle, +} from 'ui' import { boolean, number, object, string } from 'yup' import { useParams } from 'common' @@ -20,23 +33,29 @@ import { useCheckPermissions, useStore } from 'hooks' const AutoSchemaForm = observer(() => { const { ui } = useStore() const { ref: projectRef } = useParams() - const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const { + data: authConfig, + error: authConfigError, + isLoading, + isError, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-general-form' const [hidden, setHidden] = useState(true) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const INITIAL_VALUES = { - DISABLE_SIGNUP: config?.DISABLE_SIGNUP, - SITE_URL: config?.SITE_URL, - JWT_EXP: config?.JWT_EXP, - REFRESH_TOKEN_ROTATION_ENABLED: config?.REFRESH_TOKEN_ROTATION_ENABLED || false, - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: config?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, - SECURITY_CAPTCHA_ENABLED: config?.SECURITY_CAPTCHA_ENABLED || false, - SECURITY_CAPTCHA_SECRET: config?.SECURITY_CAPTCHA_SECRET || '', - SECURITY_CAPTCHA_PROVIDER: config?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', - MFA_MAX_ENROLLED_FACTORS: config?.MFA_MAX_ENROLLED_FACTORS || 10, + DISABLE_SIGNUP: !authConfig?.DISABLE_SIGNUP, + SITE_URL: authConfig?.SITE_URL, + JWT_EXP: authConfig?.JWT_EXP, + REFRESH_TOKEN_ROTATION_ENABLED: authConfig?.REFRESH_TOKEN_ROTATION_ENABLED || false, + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, + SECURITY_CAPTCHA_ENABLED: authConfig?.SECURITY_CAPTCHA_ENABLED || false, + SECURITY_CAPTCHA_SECRET: authConfig?.SECURITY_CAPTCHA_SECRET || '', + SECURITY_CAPTCHA_PROVIDER: authConfig?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', + MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS || 10, } const schema = object({ @@ -65,11 +84,10 @@ const AutoSchemaForm = observer(() => { .max(30, 'Must be a value less than 30'), }) - const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP - setSubmitting(true) updateAuthConfig( { projectRef: projectRef!, config: payload }, { @@ -86,22 +104,29 @@ const AutoSchemaForm = observer(() => { }) resetForm({ values: values, initialValues: values }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } + if (isError) { + return ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + ) + } + return (
- {({ isSubmitting, handleReset, resetForm, values, initialValues }: any) => { + {({ handleReset, resetForm, values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) // Form is reset once remote data is loaded in store useEffect(() => { - resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [isLoaded]) + if (isSuccess) resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) + }, [isSuccess]) return ( <> @@ -115,7 +140,7 @@ const AutoSchemaForm = observer(() => {
{ } > User Signups}> - + {
User Sessions}> - + {/* Permitted redirects for anything on that domain */} {/* Check with @kangming about this */} { Security and Protection}> - + { Multi Factor Authentication (MFA)} > - + { +const EmailTemplates = () => { + const { ref: projectRef } = useParams() + const { + data: authConfig, + error: authConfigError, + isLoading, + isError, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + return (
@@ -25,26 +44,40 @@ const EmailTemplates = observer(() => {
- - - {TEMPLATES_SCHEMAS.map((template) => { - const panelId = template.title.trim().replace(/\s+/g, '-') - return ( - - - - ) - })} - - + {isError && ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + )} + {isLoading && } + {isSuccess && ( + + + {TEMPLATES_SCHEMAS.map((template) => { + const panelId = template.title.trim().replace(/\s+/g, '-') + return ( + + + + ) + })} + + + )}
) -}) +} export default EmailTemplates diff --git a/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index fe168d7f496..b65d3edbad2 100644 --- a/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -1,6 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' -import { useEffect, useState } from 'react' +import { useMemo, useState } from 'react' import ReactMarkdown from 'react-markdown' import { Form, Input } from 'ui' @@ -9,40 +9,37 @@ import { FormActions, FormSection, FormSectionContent, FormSectionLabel } from ' import InformationBox from 'components/ui/InformationBox' import { useParams } from 'common' -import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions, useStore } from 'hooks' import { FormSchema } from 'types' interface TemplateEditorProps { template: FormSchema + authConfig: Record } -const TemplateEditor = ({ template }: TemplateEditorProps) => { +const TemplateEditor = ({ template, authConfig }: TemplateEditorProps) => { const { ui } = useStore() const { ref: projectRef } = useParams() - const { data, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() - const [bodyValue, setBodyValue] = useState('') + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() + const { id, properties } = template - const config = data as Record | undefined const formId = `auth-config-email-templates-${id}` - const INITIAL_VALUES: { [x: string]: string } = {} + const INITIAL_VALUES = useMemo(() => { + const result: { [x: string]: string } = {} + Object.keys(properties).forEach((key) => { + result[key] = (authConfig && authConfig[key]) ?? '' + }) + return result + }, [authConfig, properties]) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') - useEffect(() => { - if (isLoaded) { - Object.keys(properties).forEach((key) => { - INITIAL_VALUES[key] = (config && config[key]) ?? '' - }) - } - }, [isLoaded]) - const messageSlug = `MAILER_TEMPLATES_${id}_CONTENT` const messageProperty = properties[messageSlug] + const [bodyValue, setBodyValue] = useState((authConfig && authConfig[messageSlug]) ?? '') - const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } // Because the template content uses the code editor which is not a form component @@ -50,7 +47,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { delete payload[messageSlug] if (messageProperty) payload[messageSlug] = bodyValue - setSubmitting(true) updateAuthConfig( { projectRef: projectRef!, config: payload }, { @@ -64,36 +60,21 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { initialValues: values, }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } return ( - {({ isSubmitting, resetForm, values, initialValues }: any) => { - // [Alaister] although this "technically" is breaking the rules of React hooks - // it won't error because the hooks are always rendered in the same order - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - // Form is reset once remote data is loaded in store - resetForm({ - values: INITIAL_VALUES, - initialValues: INITIAL_VALUES, - }) - setBodyValue((config && config[messageSlug]) ?? '') - }, [isLoaded]) - - const message = (config && config[messageSlug]) ?? '' + {({ resetForm, values, initialValues }: any) => { + const message = (authConfig && authConfig[messageSlug]) ?? '' const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) || message !== bodyValue return ( <> - + {Object.keys(properties).map((x: string) => { const property = properties[x] if (property.type === 'string') { @@ -131,7 +112,7 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { - + {messageProperty && ( <>
@@ -152,7 +133,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
{ { resetForm({ - values: config, - initialValues: config, + values: authConfig, + initialValues: authConfig, }) - setBodyValue((config && config[messageSlug]) ?? '') + setBodyValue((authConfig && authConfig[messageSlug]) ?? '') }} form={formId} - isSubmitting={isSubmitting} + isSubmitting={isUpdatingConfig} hasChanges={hasChanges} disabled={!canUpdateConfig} helper={ diff --git a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx index 30e9f13ba10..191ac4de0f7 100644 --- a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx +++ b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx @@ -1,37 +1,24 @@ import { noop } from 'lodash' -import { observer } from 'mobx-react-lite' import { Button, IconGlobe, IconTrash } from 'ui' -import { useParams } from 'common' -import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers' import { EmptyListState } from 'components/ui/States' -import { useAuthConfigQuery } from 'data/auth/auth-config-query' import ValueContainer from './ValueContainer' interface RedirectUrlListProps { + URI_ALLOW_LIST_ARRAY: string[] canUpdate: boolean onSelectUrlToDelete: (url: string) => void } -const RedirectUrlList = ({ canUpdate, onSelectUrlToDelete = noop }: RedirectUrlListProps) => { - const { ref: projectRef } = useParams() - const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - - const URI_ALLOW_LIST_ARRAY = config?.URI_ALLOW_LIST ? config.URI_ALLOW_LIST.split(',') : [] - +const RedirectUrlList = ({ + URI_ALLOW_LIST_ARRAY, + canUpdate, + onSelectUrlToDelete = noop, +}: RedirectUrlListProps) => { return (
- {!isLoaded ? ( - <> - - - - - - - - ) : URI_ALLOW_LIST_ARRAY.length > 0 ? ( - URI_ALLOW_LIST_ARRAY.map((url: string) => { + {URI_ALLOW_LIST_ARRAY.length > 0 ? ( + URI_ALLOW_LIST_ARRAY.map((url) => { return (
@@ -69,4 +56,4 @@ const RedirectUrlList = ({ canUpdate, onSelectUrlToDelete = noop }: RedirectUrlL ) } -export default observer(RedirectUrlList) +export default RedirectUrlList diff --git a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx index 95010a466f4..495fb5d6326 100644 --- a/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx +++ b/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx @@ -1,30 +1,48 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' -import { useState } from 'react' -import { Button, Form, Input, Modal } from 'ui' +import { useMemo, useState } from 'react' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Form, + IconAlertCircle, + Input, + Modal, +} from 'ui' import { object, string } from 'yup' import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms' +import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions, useStore } from 'hooks' import { urlRegex } from '../Auth.constants' import RedirectUrlList from './RedirectUrlList' +import ValueContainer from './ValueContainer' const RedirectUrls = () => { const { ui } = useStore() const { ref: projectRef } = useParams() - const { data: config } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const { + data: authConfig, + error: authConfigError, + isLoading, + isError, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - const URI_ALLOW_LIST_ARRAY = config?.URI_ALLOW_LIST - ? config.URI_ALLOW_LIST.split(/\s*[,]+\s*/).filter((url: string) => url) - : [] + const URI_ALLOW_LIST_ARRAY = useMemo(() => { + return authConfig?.URI_ALLOW_LIST + ? authConfig.URI_ALLOW_LIST.split(/\s*[,]+\s*/).filter((url: string) => url) + : [] + }, [authConfig?.URI_ALLOW_LIST]) const [open, setOpen] = useState(false) - const [isDeleting, setIsDeleting] = useState(false) const [selectedUrlToDelete, setSelectedUrlToDelete] = useState() const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') @@ -33,12 +51,11 @@ const RedirectUrls = () => { url: string().matches(urlRegex, 'URL is not valid').required(), }) - const onAddNewUrl = async (values: any, { setSubmitting }: any) => { + const onAddNewUrl = async (values: any) => { if (!values.url) { return } - setSubmitting(true) const payload = URI_ALLOW_LIST_ARRAY // remove any trailing commas payload.push(values.url.replace(/,\s*$/, '')) @@ -50,8 +67,6 @@ const RedirectUrls = () => { message: 'Too many redirect URLs, please remove some or try to use wildcards', category: 'error', }) - - setSubmitting(false) return } @@ -69,9 +84,6 @@ const RedirectUrls = () => { setOpen(false) ui.setNotification({ category: 'success', message: 'Successfully added URL' }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } @@ -79,8 +91,6 @@ const RedirectUrls = () => { const onConfirmDeleteUrl = async (url?: string) => { if (!url) return - setIsDeleting(true) - // Remove selectedUrl from array and update const payload = URI_ALLOW_LIST_ARRAY.filter((e: string) => e !== url) @@ -98,9 +108,6 @@ const RedirectUrls = () => { setSelectedUrlToDelete(undefined) ui.setNotification({ category: 'success', message: 'Successfully removed URL' }) }, - onSettled: () => { - setIsDeleting(false) - }, } ) } @@ -137,7 +144,30 @@ const RedirectUrls = () => { )}
- + {isLoading && ( + <> + + + + + + + + )} + {isError && ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + )} + {isSuccess && ( + + )} { validationSchema={newUrlSchema} onSubmit={onAddNewUrl} > - {({ isSubmitting }: { isSubmitting: boolean }) => { + {() => { return (
@@ -172,8 +202,8 @@ const RedirectUrls = () => { form="new-redirect-url-form" htmlType="submit" size="medium" - disabled={isSubmitting} - loading={isSubmitting} + disabled={isUpdatingConfig} + loading={isUpdatingConfig} > Add URL @@ -214,10 +244,10 @@ const RedirectUrls = () => { block size="medium" type="warning" - loading={isDeleting} + loading={isUpdatingConfig} onClick={() => onConfirmDeleteUrl(selectedUrlToDelete)} > - {isDeleting ? 'Removing...' : 'Remove URL'} + {isUpdatingConfig ? 'Removing...' : 'Remove URL'}
diff --git a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index 2470fed6324..241f0063fbf 100644 --- a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -1,7 +1,14 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' import { useEffect } from 'react' -import { Form, Input } from 'ui' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Form, + IconAlertCircle, + Input, +} from 'ui' import { boolean, number, object, string } from 'yup' import { useParams } from 'common' @@ -16,48 +23,53 @@ import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions, useStore } from 'hooks' +const schema = object({ + DISABLE_SIGNUP: boolean().required(), + SITE_URL: string().required('Must have a Site URL'), + JWT_EXP: number() + .max(604800, 'Must be less than 604800') + .required('Must have a JWT expiry value'), + REFRESH_TOKEN_ROTATION_ENABLED: boolean().required(), + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number() + .min(0, 'Must be a value more than 0') + .required('Must have a Reuse Interval value'), + SECURITY_CAPTCHA_ENABLED: boolean().required(), + SECURITY_CAPTCHA_SECRET: string().when('SECURITY_CAPTCHA_ENABLED', { + is: true, + then: string().required('Must have a Captcha secret'), + }), +}) + const SiteUrl = observer(() => { const { ui } = useStore() const { ref: projectRef } = useParams() - const { data: config, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const { + data: authConfig, + error: authConfigError, + isLoading, + isError, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const formId = 'auth-config-general-form' const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') const INITIAL_VALUES = { - DISABLE_SIGNUP: config?.DISABLE_SIGNUP, - JWT_EXP: config?.JWT_EXP, - SITE_URL: config?.SITE_URL, - REFRESH_TOKEN_ROTATION_ENABLED: config?.REFRESH_TOKEN_ROTATION_ENABLED || false, - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: config?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, - SECURITY_CAPTCHA_ENABLED: config?.SECURITY_CAPTCHA_ENABLED || false, - SECURITY_CAPTCHA_PROVIDER: config?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', - SECURITY_CAPTCHA_SECRET: config?.SECURITY_CAPTCHA_SECRET || '', + DISABLE_SIGNUP: authConfig?.DISABLE_SIGNUP, + JWT_EXP: authConfig?.JWT_EXP, + SITE_URL: authConfig?.SITE_URL, + REFRESH_TOKEN_ROTATION_ENABLED: authConfig?.REFRESH_TOKEN_ROTATION_ENABLED || false, + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, + SECURITY_CAPTCHA_ENABLED: authConfig?.SECURITY_CAPTCHA_ENABLED || false, + SECURITY_CAPTCHA_PROVIDER: authConfig?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', + SECURITY_CAPTCHA_SECRET: authConfig?.SECURITY_CAPTCHA_SECRET || '', } - const schema = object({ - DISABLE_SIGNUP: boolean().required(), - SITE_URL: string().required('Must have a Site URL'), - JWT_EXP: number() - .max(604800, 'Must be less than 604800') - .required('Must have a JWT expiry value'), - REFRESH_TOKEN_ROTATION_ENABLED: boolean().required(), - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number() - .min(0, 'Must be a value more than 0') - .required('Must have a Reuse Interval value'), - SECURITY_CAPTCHA_ENABLED: boolean().required(), - SECURITY_CAPTCHA_SECRET: string().when('SECURITY_CAPTCHA_ENABLED', { - is: true, - then: string().required('Must have a Captcha secret'), - }), - }) - - const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP - setSubmitting(true) updateAuthConfig( { projectRef: projectRef!, config: payload }, { @@ -74,22 +86,31 @@ const SiteUrl = observer(() => { }) resetForm({ values: values, initialValues: values }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } + if (isError) { + return ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + ) + } + return ( - {({ isSubmitting, handleReset, resetForm, values, initialValues }: any) => { + {({ handleReset, resetForm, values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) // Form is reset once remote data is loaded in store useEffect(() => { - resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [isLoaded]) + if (isSuccess) { + resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) + } + }, [isSuccess]) return ( <> @@ -103,7 +124,7 @@ const SiteUrl = observer(() => {
{ } > - + { const { ui } = useStore() const { ref: projectRef } = useParams() - const { data: config, isLoading, isFetched: isLoaded } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const { + data: authConfig, + error: authConfigError, + isLoading, + isError, + isSuccess, + } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const [enableSmtp, setEnableSmtp] = useState(false) const [hidden, setHidden] = useState(true) const formId = 'auth-config-smtp-form' - const initialValues = generateFormValues(config) + const initialValues = generateFormValues(authConfig) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') useEffect(() => { - if (isLoaded && isSmtpEnabled(config)) { + if (isSuccess && isSmtpEnabled(authConfig)) { setEnableSmtp(true) } - }, [isLoaded, config]) + }, [isSuccess, authConfig]) const schema = object({ SMTP_ADMIN_EMAIL: string().when([], { @@ -115,14 +122,13 @@ const SmtpForm = () => { }), }) - const onSubmit = (values: any, { setSubmitting, resetForm }: any) => { + const onSubmit = (values: any, { resetForm }: any) => { const payload = enableSmtp ? values : defaultDisabledSmtpFormValues // Format payload: Remove redundant value + convert port to string delete payload.ENABLE_SMTP payload.SMTP_PORT = payload.SMTP_PORT ? payload.SMTP_PORT.toString() : payload.SMTP_PORT - setSubmitting(true) updateAuthConfig( { projectRef: projectRef!, config: payload }, { @@ -135,16 +141,23 @@ const SmtpForm = () => { resetForm({ values: updatedFormValues, initialValues: updatedFormValues }) ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) }, - onSettled: () => { - setSubmitting(false) - }, } ) } + if (isError) { + return ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + ) + } + return ( - {({ isSubmitting, resetForm, values }: any) => { + {({ resetForm, values }: any) => { const isValidSmtpConfig = isSmtpEnabled(values) const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) @@ -152,11 +165,11 @@ const SmtpForm = () => { // it won't error because the hooks are always rendered in the same order // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - if (isLoaded) { - const formValues = generateFormValues(config) + if (isSuccess) { + const formValues = generateFormValues(authConfig) resetForm({ values: formValues, initialValues: formValues }) } - }, [isLoaded, config]) + }, [isSuccess, authConfig]) const onResetForm = () => { setEnableSmtp(isSmtpEnabled(initialValues)) @@ -174,7 +187,7 @@ const SmtpForm = () => {
Date: Fri, 11 Aug 2023 15:46:27 +0200 Subject: [PATCH 010/117] Regenerate the openapi types. The UpdateGoTrueConfig type was fixed. --- studio/data/api.d.ts | 697 +++++++++--------- .../data/auth/auth-config-update-mutation.ts | 6 +- 2 files changed, 353 insertions(+), 350 deletions(-) diff --git a/studio/data/api.d.ts b/studio/data/api.d.ts index 835dda10d13..3e531b46edf 100644 --- a/studio/data/api.d.ts +++ b/studio/data/api.d.ts @@ -225,6 +225,14 @@ export interface paths { /** Gets the upcoming invoice */ get: operations["OrgInvoicesController_getUpcomingInvoice"]; }; + "/platform/pg-meta/{ref}/column-privileges": { + /** Retrieve column privileges */ + get: operations["ColumnPrivilegesController_getColumnPrivileges"]; + /** Grant column privileges */ + post: operations["ColumnPrivilegesController_grantColumnPrivileges"]; + /** Revoke column privileges */ + delete: operations["ColumnPrivilegesController_revokeColumnPrivileges"]; + }; "/platform/pg-meta/{ref}/columns": { /** Gets project pg.columns */ get: operations["ColumnsController_getColumns"]; @@ -321,6 +329,14 @@ export interface paths { /** Searches project pg.columns. Return maximum 50 results. */ post: operations["SearchController_searchColumns"]; }; + "/platform/pg-meta/{ref}/table-privileges": { + /** Retrieve table privileges */ + get: operations["TablePrivilegesController_getTablePrivileges"]; + /** Grant table privileges */ + post: operations["TablePrivilegesController_grantTablePrivileges"]; + /** Revoke table privileges */ + delete: operations["TablePrivilegesController_revokeTablePrivileges"]; + }; "/platform/pg-meta/{ref}/tables": { /** Gets project pg.tables or pg.table with the given ID */ get: operations["TablesController_getTables"]; @@ -961,6 +977,14 @@ export interface paths { /** Updates organization member */ patch: operations["MembersController_updateMember"]; }; + "/v0/pg-meta/{ref}/column-privileges": { + /** Retrieve column privileges */ + get: operations["ColumnPrivilegesController_getColumnPrivileges"]; + /** Grant column privileges */ + post: operations["ColumnPrivilegesController_grantColumnPrivileges"]; + /** Revoke column privileges */ + delete: operations["ColumnPrivilegesController_revokeColumnPrivileges"]; + }; "/v0/pg-meta/{ref}/columns": { /** Gets project pg.columns */ get: operations["ColumnsController_getColumns"]; @@ -1057,6 +1081,14 @@ export interface paths { /** Searches project pg.columns. Return maximum 50 results. */ post: operations["SearchController_searchColumns"]; }; + "/v0/pg-meta/{ref}/table-privileges": { + /** Retrieve table privileges */ + get: operations["TablePrivilegesController_getTablePrivileges"]; + /** Grant table privileges */ + post: operations["TablePrivilegesController_grantTablePrivileges"]; + /** Revoke table privileges */ + delete: operations["TablePrivilegesController_revokeTablePrivileges"]; + }; "/v0/pg-meta/{ref}/tables": { /** Gets project pg.tables or pg.table with the given ID */ get: operations["TablesController_getTables"]; @@ -1539,22 +1571,6 @@ export interface paths { /** Create an organization */ post: operations["OrganizationsController_createOrganization"]; }; - "/v1/organizations/{slug}/oauth/apps": { - /** List published or authorized oauth apps */ - get: operations["OAuthAppsController_listOAuthApps"]; - /** Create an oauth app */ - post: operations["OAuthAppsController_createOAuthApp"]; - }; - "/v1/organizations/{slug}/oauth/apps/{id}": { - /** Update an oauth app */ - put: operations["OAuthAppsController_updateOAuthApp"]; - /** Remove a published oauth app */ - delete: operations["OAuthAppsController_removeOAuthApp"]; - }; - "/v1/organizations/{slug}/oauth/apps/{id}/revoke": { - /** Revoke an authorized oauth app */ - post: operations["OAuthAppsController_revokeAuthorizedOAuthApp"]; - }; "/v1/oauth/authorize": { /** Authorize user through oauth */ get: operations["OAuthController_authorize"]; @@ -1563,13 +1579,6 @@ export interface paths { /** Exchange auth code for user's access and refresh token */ post: operations["OAuthController_token"]; }; - "/v1/oauth/authorizations/{id}": { - get: operations["AuthorizationsController_getAuthorizationRequest"]; - /** Approve oauth app authorization request */ - post: operations["AuthorizationsController_approveAuthorizationRequest"]; - /** Decline oauth app authorization request */ - delete: operations["AuthorizationsController_declineAuthorizationRequest"]; - }; } export type webhooks = Record; @@ -1766,126 +1775,126 @@ export interface components { EXTERNAL_ZOOM_SECRET: string; }; UpdateGoTrueConfigBody: { - SITE_URL: string; - DISABLE_SIGNUP: boolean; - JWT_EXP: number; - SMTP_ADMIN_EMAIL: string; - SMTP_HOST: string; - SMTP_PORT: string; - SMTP_USER: string; + SITE_URL?: string; + DISABLE_SIGNUP?: boolean; + JWT_EXP?: number; + SMTP_ADMIN_EMAIL?: string; + SMTP_HOST?: string; + SMTP_PORT?: string; + SMTP_USER?: string; SMTP_PASS?: string | null; SMTP_PASS_ENCRYPTED?: string | null; - SMTP_MAX_FREQUENCY: number; + SMTP_MAX_FREQUENCY?: number; SMTP_SENDER_NAME?: string; - MAILER_AUTOCONFIRM: boolean; - MAILER_SUBJECTS_INVITE: string; - MAILER_SUBJECTS_CONFIRMATION: string; - MAILER_SUBJECTS_RECOVERY: string; - MAILER_SUBJECTS_EMAIL_CHANGE: string; - MAILER_SUBJECTS_MAGIC_LINK: string; - MAILER_TEMPLATES_INVITE_CONTENT: string; - MAILER_TEMPLATES_CONFIRMATION_CONTENT: string; - MAILER_TEMPLATES_RECOVERY_CONTENT: string; - MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: string; - MAILER_TEMPLATES_MAGIC_LINK_CONTENT: string; - MFA_MAX_ENROLLED_FACTORS: number; - URI_ALLOW_LIST: string; - EXTERNAL_EMAIL_ENABLED: boolean; - EXTERNAL_PHONE_ENABLED: boolean; + MAILER_AUTOCONFIRM?: boolean; + MAILER_SUBJECTS_INVITE?: string; + MAILER_SUBJECTS_CONFIRMATION?: string; + MAILER_SUBJECTS_RECOVERY?: string; + MAILER_SUBJECTS_EMAIL_CHANGE?: string; + MAILER_SUBJECTS_MAGIC_LINK?: string; + MAILER_TEMPLATES_INVITE_CONTENT?: string; + MAILER_TEMPLATES_CONFIRMATION_CONTENT?: string; + MAILER_TEMPLATES_RECOVERY_CONTENT?: string; + MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT?: string; + MAILER_TEMPLATES_MAGIC_LINK_CONTENT?: string; + MFA_MAX_ENROLLED_FACTORS?: number; + URI_ALLOW_LIST?: string; + EXTERNAL_EMAIL_ENABLED?: boolean; + EXTERNAL_PHONE_ENABLED?: boolean; SAML_ENABLED?: boolean; - SECURITY_CAPTCHA_ENABLED: boolean; - SECURITY_CAPTCHA_PROVIDER: string; - SECURITY_CAPTCHA_SECRET: string; - RATE_LIMIT_EMAIL_SENT: number; - RATE_LIMIT_SMS_SENT: number; - MAILER_SECURE_EMAIL_CHANGE_ENABLED: boolean; - REFRESH_TOKEN_ROTATION_ENABLED: boolean; - PASSWORD_MIN_LENGTH: number; - SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: boolean; - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number; - MAILER_OTP_EXP: number; - SMS_AUTOCONFIRM: boolean; - SMS_MAX_FREQUENCY: number; - SMS_OTP_EXP: number; - SMS_OTP_LENGTH: number; - SMS_PROVIDER: string; - SMS_MESSAGEBIRD_ACCESS_KEY: string; - SMS_MESSAGEBIRD_ORIGINATOR: string; - SMS_TEXTLOCAL_API_KEY: string; - SMS_TEXTLOCAL_SENDER: string; - SMS_TWILIO_ACCOUNT_SID: string; - SMS_TWILIO_AUTH_TOKEN: string; - SMS_TWILIO_MESSAGE_SERVICE_SID: string; - SMS_TWILIO_VERIFY_ACCOUNT_SID: string; - SMS_TWILIO_VERIFY_AUTH_TOKEN: string; - SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID: string; - SMS_VONAGE_API_KEY: string; - SMS_VONAGE_API_SECRET: string; - SMS_VONAGE_FROM: string; - SMS_TEMPLATE: string; - EXTERNAL_APPLE_ENABLED: boolean; - EXTERNAL_APPLE_CLIENT_ID: string; - EXTERNAL_APPLE_SECRET: string; + SECURITY_CAPTCHA_ENABLED?: boolean; + SECURITY_CAPTCHA_PROVIDER?: string; + SECURITY_CAPTCHA_SECRET?: string; + RATE_LIMIT_EMAIL_SENT?: number; + RATE_LIMIT_SMS_SENT?: number; + MAILER_SECURE_EMAIL_CHANGE_ENABLED?: boolean; + REFRESH_TOKEN_ROTATION_ENABLED?: boolean; + PASSWORD_MIN_LENGTH?: number; + SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION?: boolean; + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL?: number; + MAILER_OTP_EXP?: number; + SMS_AUTOCONFIRM?: boolean; + SMS_MAX_FREQUENCY?: number; + SMS_OTP_EXP?: number; + SMS_OTP_LENGTH?: number; + SMS_PROVIDER?: string; + SMS_MESSAGEBIRD_ACCESS_KEY?: string; + SMS_MESSAGEBIRD_ORIGINATOR?: string; + SMS_TEXTLOCAL_API_KEY?: string; + SMS_TEXTLOCAL_SENDER?: string; + SMS_TWILIO_ACCOUNT_SID?: string; + SMS_TWILIO_AUTH_TOKEN?: string; + SMS_TWILIO_MESSAGE_SERVICE_SID?: string; + SMS_TWILIO_VERIFY_ACCOUNT_SID?: string; + SMS_TWILIO_VERIFY_AUTH_TOKEN?: string; + SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID?: string; + SMS_VONAGE_API_KEY?: string; + SMS_VONAGE_API_SECRET?: string; + SMS_VONAGE_FROM?: string; + SMS_TEMPLATE?: string; + EXTERNAL_APPLE_ENABLED?: boolean; + EXTERNAL_APPLE_CLIENT_ID?: string; + EXTERNAL_APPLE_SECRET?: string; EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS?: string; - EXTERNAL_AZURE_ENABLED: boolean; - EXTERNAL_AZURE_CLIENT_ID: string; - EXTERNAL_AZURE_SECRET: string; - EXTERNAL_AZURE_URL: string; - EXTERNAL_BITBUCKET_ENABLED: boolean; - EXTERNAL_BITBUCKET_CLIENT_ID: string; - EXTERNAL_BITBUCKET_SECRET: string; - EXTERNAL_DISCORD_ENABLED: boolean; - EXTERNAL_DISCORD_CLIENT_ID: string; - EXTERNAL_DISCORD_SECRET: string; - EXTERNAL_FACEBOOK_ENABLED: boolean; - EXTERNAL_FACEBOOK_CLIENT_ID: string; - EXTERNAL_FACEBOOK_SECRET: string; - EXTERNAL_FIGMA_ENABLED: boolean; - EXTERNAL_FIGMA_CLIENT_ID: string; - EXTERNAL_FIGMA_SECRET: string; - EXTERNAL_GITHUB_ENABLED: boolean; - EXTERNAL_GITHUB_CLIENT_ID: string; - EXTERNAL_GITHUB_SECRET: string; - EXTERNAL_GITLAB_ENABLED: boolean; - EXTERNAL_GITLAB_CLIENT_ID: string; - EXTERNAL_GITLAB_SECRET: string; - EXTERNAL_GITLAB_URL: string; - EXTERNAL_GOOGLE_ENABLED: boolean; - EXTERNAL_GOOGLE_CLIENT_ID: string; - EXTERNAL_GOOGLE_SECRET: string; + EXTERNAL_AZURE_ENABLED?: boolean; + EXTERNAL_AZURE_CLIENT_ID?: string; + EXTERNAL_AZURE_SECRET?: string; + EXTERNAL_AZURE_URL?: string; + EXTERNAL_BITBUCKET_ENABLED?: boolean; + EXTERNAL_BITBUCKET_CLIENT_ID?: string; + EXTERNAL_BITBUCKET_SECRET?: string; + EXTERNAL_DISCORD_ENABLED?: boolean; + EXTERNAL_DISCORD_CLIENT_ID?: string; + EXTERNAL_DISCORD_SECRET?: string; + EXTERNAL_FACEBOOK_ENABLED?: boolean; + EXTERNAL_FACEBOOK_CLIENT_ID?: string; + EXTERNAL_FACEBOOK_SECRET?: string; + EXTERNAL_FIGMA_ENABLED?: boolean; + EXTERNAL_FIGMA_CLIENT_ID?: string; + EXTERNAL_FIGMA_SECRET?: string; + EXTERNAL_GITHUB_ENABLED?: boolean; + EXTERNAL_GITHUB_CLIENT_ID?: string; + EXTERNAL_GITHUB_SECRET?: string; + EXTERNAL_GITLAB_ENABLED?: boolean; + EXTERNAL_GITLAB_CLIENT_ID?: string; + EXTERNAL_GITLAB_SECRET?: string; + EXTERNAL_GITLAB_URL?: string; + EXTERNAL_GOOGLE_ENABLED?: boolean; + EXTERNAL_GOOGLE_CLIENT_ID?: string; + EXTERNAL_GOOGLE_SECRET?: string; EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS?: string; - EXTERNAL_KAKAO_ENABLED: boolean; - EXTERNAL_KAKAO_CLIENT_ID: string; - EXTERNAL_KAKAO_SECRET: string; - EXTERNAL_KEYCLOAK_ENABLED: boolean; - EXTERNAL_KEYCLOAK_CLIENT_ID: string; - EXTERNAL_KEYCLOAK_SECRET: string; - EXTERNAL_KEYCLOAK_URL: string; - EXTERNAL_LINKEDIN_ENABLED: boolean; - EXTERNAL_LINKEDIN_CLIENT_ID: string; - EXTERNAL_LINKEDIN_SECRET: string; - EXTERNAL_NOTION_ENABLED: boolean; - EXTERNAL_NOTION_CLIENT_ID: string; - EXTERNAL_NOTION_SECRET: string; - EXTERNAL_SLACK_ENABLED: boolean; - EXTERNAL_SLACK_CLIENT_ID: string; - EXTERNAL_SLACK_SECRET: string; - EXTERNAL_SPOTIFY_ENABLED: boolean; - EXTERNAL_SPOTIFY_CLIENT_ID: string; - EXTERNAL_SPOTIFY_SECRET: string; - EXTERNAL_TWITCH_ENABLED: boolean; - EXTERNAL_TWITCH_CLIENT_ID: string; - EXTERNAL_TWITCH_SECRET: string; - EXTERNAL_TWITTER_ENABLED: boolean; - EXTERNAL_TWITTER_CLIENT_ID: string; - EXTERNAL_TWITTER_SECRET: string; - EXTERNAL_WORKOS_ENABLED: boolean; - EXTERNAL_WORKOS_CLIENT_ID: string; - EXTERNAL_WORKOS_SECRET: string; - EXTERNAL_WORKOS_URL: string; - EXTERNAL_ZOOM_ENABLED: boolean; - EXTERNAL_ZOOM_CLIENT_ID: string; - EXTERNAL_ZOOM_SECRET: string; + EXTERNAL_KAKAO_ENABLED?: boolean; + EXTERNAL_KAKAO_CLIENT_ID?: string; + EXTERNAL_KAKAO_SECRET?: string; + EXTERNAL_KEYCLOAK_ENABLED?: boolean; + EXTERNAL_KEYCLOAK_CLIENT_ID?: string; + EXTERNAL_KEYCLOAK_SECRET?: string; + EXTERNAL_KEYCLOAK_URL?: string; + EXTERNAL_LINKEDIN_ENABLED?: boolean; + EXTERNAL_LINKEDIN_CLIENT_ID?: string; + EXTERNAL_LINKEDIN_SECRET?: string; + EXTERNAL_NOTION_ENABLED?: boolean; + EXTERNAL_NOTION_CLIENT_ID?: string; + EXTERNAL_NOTION_SECRET?: string; + EXTERNAL_SLACK_ENABLED?: boolean; + EXTERNAL_SLACK_CLIENT_ID?: string; + EXTERNAL_SLACK_SECRET?: string; + EXTERNAL_SPOTIFY_ENABLED?: boolean; + EXTERNAL_SPOTIFY_CLIENT_ID?: string; + EXTERNAL_SPOTIFY_SECRET?: string; + EXTERNAL_TWITCH_ENABLED?: boolean; + EXTERNAL_TWITCH_CLIENT_ID?: string; + EXTERNAL_TWITCH_SECRET?: string; + EXTERNAL_TWITTER_ENABLED?: boolean; + EXTERNAL_TWITTER_CLIENT_ID?: string; + EXTERNAL_TWITTER_SECRET?: string; + EXTERNAL_WORKOS_ENABLED?: boolean; + EXTERNAL_WORKOS_CLIENT_ID?: string; + EXTERNAL_WORKOS_SECRET?: string; + EXTERNAL_WORKOS_URL?: string; + EXTERNAL_ZOOM_ENABLED?: boolean; + EXTERNAL_ZOOM_CLIENT_ID?: string; + EXTERNAL_ZOOM_SECRET?: string; }; GoTrueConfig: { SITE_URL: string; @@ -2536,6 +2545,33 @@ export interface components { /** @enum {string} */ tier: "tier_payg" | "tier_pro" | "tier_free" | "tier_team" | "tier_enterprise"; }; + ColumnPrivilege: { + grantor: string; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "REFERENCES"; + is_grantable: boolean; + }; + PostgresColumnPrivileges: { + column_id: string; + relation_schema: string; + relation_name: string; + column_name: string; + privileges: (components["schemas"]["ColumnPrivilege"])[]; + }; + GrantColumnPrivilegesBody: { + is_grantable?: boolean; + column_id: string; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "REFERENCES"; + }; + RevokeColumnPrivilegesBody: { + column_id: string; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "REFERENCES"; + }; PostgresColumn: { table_id: number; schema: string; @@ -2796,6 +2832,32 @@ export interface components { table_id: number; table: string; }; + TablePrivilege: { + grantor: string; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE" | "REFERENCES" | "TRIGGER"; + is_grantable: boolean; + }; + PostgresTablePrivileges: { + schema: string; + name: string; + kind: string; + privileges: (components["schemas"]["TablePrivilege"])[]; + }; + GrantTablePrivilegesBody: { + is_grantable?: boolean; + relation_id: number; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE" | "REFERENCES" | "TRIGGER"; + }; + RevokeTablePrivilegesBody: { + relation_id: number; + grantee: string; + /** @enum {string} */ + privilege_type: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE" | "REFERENCES" | "TRIGGER"; + }; PrimaryKey: { schema: string; table_name: string; @@ -4214,55 +4276,6 @@ export interface components { entrypoint_path?: string; import_map_path?: string; }; - OAuthAppResponse: { - id: string; - name: string; - website: string; - icon?: string; - authorized_at?: string; - created_at?: string; - client_id?: string; - client_secret_alias?: string; - redirect_uris?: (string)[]; - }; - CreateOAuthAppBody: { - name: string; - website: string; - icon?: string; - redirect_uris: (string)[]; - }; - CreateOAuthAppResponse: { - id: string; - client_id: string; - client_secret: string; - }; - PutOAuthAppResponse: { - id: string; - client_id: string; - client_secret_alias: string; - created_at: string; - name: string; - website: string; - icon?: string; - redirect_uris: (string)[]; - }; - RevokeAuthorizedOAuthAppResponse: { - id: string; - name: string; - website: string; - icon?: string; - authorized_at: string; - }; - DeleteOAuthAppResponse: { - id: string; - name: string; - website: string; - icon?: string; - created_at: string; - client_id: string; - client_secret_alias: string; - redirect_uris: (string)[]; - }; OAuthTokenBody: { /** @enum {string} */ grant_type: "authorization_code" | "refresh_token"; @@ -4280,24 +4293,6 @@ export interface components { refresh_token: string; expires_in: number; }; - GetAuthorizationResponse: { - name: string; - website: string; - icon?: string; - domain: string; - expires_at: string; - approved_at?: string; - approved_organization_slug?: string; - }; - AuthorizationsApproveBody: { - organization_id: string; - }; - ApproveAuthorizationResponse: { - url: string; - }; - DeclineAuthorizationResponse: { - id: string; - }; }; responses: never; parameters: never; @@ -5499,6 +5494,82 @@ export interface operations { 500: never; }; }; + /** Retrieve column privileges */ + ColumnPrivilegesController_getColumnPrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + responses: { + 200: { + content: { + "application/json": (components["schemas"]["PostgresColumnPrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to retrieve column privileges */ + 500: never; + }; + }; + /** Grant column privileges */ + ColumnPrivilegesController_grantColumnPrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + requestBody: { + content: { + "application/json": (components["schemas"]["GrantColumnPrivilegesBody"])[]; + }; + }; + responses: { + 201: { + content: { + "application/json": (components["schemas"]["PostgresColumnPrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to grant column privileges */ + 500: never; + }; + }; + /** Revoke column privileges */ + ColumnPrivilegesController_revokeColumnPrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + requestBody: { + content: { + "application/json": (components["schemas"]["RevokeColumnPrivilegesBody"])[]; + }; + }; + responses: { + 200: { + content: { + "application/json": (components["schemas"]["PostgresColumnPrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to revoke column privileges */ + 500: never; + }; + }; /** Gets project pg.columns */ ColumnsController_getColumns: { parameters: { @@ -6413,6 +6484,82 @@ export interface operations { 500: never; }; }; + /** Retrieve table privileges */ + TablePrivilegesController_getTablePrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + responses: { + 200: { + content: { + "application/json": (components["schemas"]["PostgresTablePrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to retrieve table privileges */ + 500: never; + }; + }; + /** Grant table privileges */ + TablePrivilegesController_grantTablePrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + requestBody: { + content: { + "application/json": (components["schemas"]["GrantTablePrivilegesBody"])[]; + }; + }; + responses: { + 201: { + content: { + "application/json": (components["schemas"]["PostgresTablePrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to grant table privileges */ + 500: never; + }; + }; + /** Revoke table privileges */ + TablePrivilegesController_revokeTablePrivileges: { + parameters: { + header: { + "x-connection-encrypted": string; + }; + path: { + /** @description Project ref */ + ref: string; + }; + }; + requestBody: { + content: { + "application/json": (components["schemas"]["RevokeTablePrivilegesBody"])[]; + }; + }; + responses: { + 200: { + content: { + "application/json": (components["schemas"]["PostgresTablePrivileges"])[]; + }; + }; + 403: never; + /** @description Failed to revoke table privileges */ + 500: never; + }; + }; /** Gets project pg.tables or pg.table with the given ID */ TablesController_getTables: { parameters: { @@ -8458,7 +8605,7 @@ export interface operations { "application/json": components["schemas"]["CreateVercelConnectionResponse"]; }; }; - /** @description Failed to create project connections */ + /** @description Failed to create project connection */ 500: never; }; }; @@ -10142,97 +10289,6 @@ export interface operations { 500: never; }; }; - /** List published or authorized oauth apps */ - OAuthAppsController_listOAuthApps: { - parameters: { - query: { - type: "published" | "authorized"; - }; - path: { - slug: string; - }; - }; - responses: { - 200: { - content: { - "application/json": (components["schemas"]["OAuthAppResponse"])[]; - }; - }; - }; - }; - /** Create an oauth app */ - OAuthAppsController_createOAuthApp: { - parameters: { - path: { - slug: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateOAuthAppBody"]; - }; - }; - responses: { - 201: { - content: { - "application/json": components["schemas"]["CreateOAuthAppResponse"]; - }; - }; - }; - }; - /** Update an oauth app */ - OAuthAppsController_updateOAuthApp: { - parameters: { - path: { - slug: string; - id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateOAuthAppBody"]; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["PutOAuthAppResponse"]; - }; - }; - }; - }; - /** Remove a published oauth app */ - OAuthAppsController_removeOAuthApp: { - parameters: { - path: { - slug: string; - id: string; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["DeleteOAuthAppResponse"]; - }; - }; - }; - }; - /** Revoke an authorized oauth app */ - OAuthAppsController_revokeAuthorizedOAuthApp: { - parameters: { - path: { - slug: string; - id: string; - }; - }; - responses: { - 201: { - content: { - "application/json": components["schemas"]["RevokeAuthorizedOAuthAppResponse"]; - }; - }; - }; - }; /** Authorize user through oauth */ OAuthController_authorize: { parameters: { @@ -10266,53 +10322,4 @@ export interface operations { }; }; }; - AuthorizationsController_getAuthorizationRequest: { - parameters: { - path: { - id: string; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["GetAuthorizationResponse"]; - }; - }; - }; - }; - /** Approve oauth app authorization request */ - AuthorizationsController_approveAuthorizationRequest: { - parameters: { - path: { - id: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AuthorizationsApproveBody"]; - }; - }; - responses: { - 201: { - content: { - "application/json": components["schemas"]["ApproveAuthorizationResponse"]; - }; - }; - }; - }; - /** Decline oauth app authorization request */ - AuthorizationsController_declineAuthorizationRequest: { - parameters: { - path: { - id: string; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["DeclineAuthorizationResponse"]; - }; - }; - }; - }; } diff --git a/studio/data/auth/auth-config-update-mutation.ts b/studio/data/auth/auth-config-update-mutation.ts index f05ff956cb5..cda76a4970a 100644 --- a/studio/data/auth/auth-config-update-mutation.ts +++ b/studio/data/auth/auth-config-update-mutation.ts @@ -19,11 +19,7 @@ export async function updateAuthConfig({ projectRef, config }: AuthConfigUpdateV path: { ref: projectRef }, }, body: { - // TODO: The config sent to the API can be partial according to the backend implementation. - // The definition for the body https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/auth/ref/config.dto.ts#L16 is wrong. - // Every property type should be optional (with ?). When that's fixed, the new generated code will have the correct type. - // Can be removed once https://github.com/supabase/infrastructure/pull/14167 goes in. - ...(config as any), + ...config, }, }) From b4e355c37b4614c86339cae7b332b0b6923835b0 Mon Sep 17 00:00:00 2001 From: kishanhitk Date: Mon, 14 Aug 2023 19:48:45 +0530 Subject: [PATCH 011/117] chore: add TODO --- studio/components/interfaces/SQLEditor/SQLEditor.constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 4c020c819c5..56674169a89 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -1256,4 +1256,5 @@ export const sqlAiDisclaimerComment = stripIndent` export const untitledSnippetTitle = 'Untitled query' +// TODO: Update this to exclude commented out code export const destructiveSqlRegex = [/(drop|delete|truncate)\s/i] From b9696c3cc33f38ab6a20461d28755c96507428fc Mon Sep 17 00:00:00 2001 From: kishanhitk Date: Mon, 14 Aug 2023 22:03:03 +0530 Subject: [PATCH 012/117] feat: remove comments from sql before checking for destructive statement --- .../interfaces/SQLEditor/SQLEditor.constants.ts | 1 - .../components/interfaces/SQLEditor/SQLEditor.tsx | 8 ++++++-- studio/lib/helpers.ts | 13 +++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 56674169a89..4c020c819c5 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -1256,5 +1256,4 @@ export const sqlAiDisclaimerComment = stripIndent` export const untitledSnippetTitle = 'Untitled query' -// TODO: Update this to exclude commented out code export const destructiveSqlRegex = [/(drop|delete|truncate)\s/i] diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 9757e2566c8..082a544b246 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -36,7 +36,7 @@ import { useStore, } from 'hooks' import { IS_PLATFORM } from 'lib/constants' -import { uuidv4 } from 'lib/helpers' +import { removeCommentsFromSql, uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' import Telemetry from 'lib/telemetry' import { getSqlEditorStateSnapshot, useSqlEditorStateSnapshot } from 'state/sql-editor' @@ -220,10 +220,14 @@ const SQLEditor = () => { const sql = snippet ? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content.sql : selectedValue || editorRef.current?.getValue() + console.log('sql', sql) - const containsDestructiveOperations = destructiveSqlRegex.some((regex) => regex.test(sql)) + const containsDestructiveOperations = destructiveSqlRegex.some((regex) => + regex.test(removeCommentsFromSql(sql)) + ) if (!force && containsDestructiveOperations) { + console.log('Regex match') setIsConfirmModalOpen(true) return } diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index 71246f591b5..9b480438bc0 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -266,3 +266,16 @@ export const isValidHttpUrl = (value: string) => { } return url.protocol === 'http:' || url.protocol === 'https:' } + +/** + * Helper function to remove comments from SQL. + */ +export const removeCommentsFromSql = (sql: string) => { + // Removing single-line comments: + let cleanedSql = sql.replace(/--.*$/gm, '') + + // Removing multi-line comments: + cleanedSql = cleanedSql.replace(/\/\*[\s\S]*?\*\//gm, '') + + return cleanedSql +} From 7feea7fd686a389389bd5b1697e24e3aa3cc4ec9 Mon Sep 17 00:00:00 2001 From: kishanhitk Date: Mon, 14 Aug 2023 22:25:05 +0530 Subject: [PATCH 013/117] chore: add disclaimer --- studio/lib/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index 9b480438bc0..ec5559c7f42 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -269,6 +269,7 @@ export const isValidHttpUrl = (value: string) => { /** * Helper function to remove comments from SQL. + * Disclaimer: Doesn't work as intended for nested comments. */ export const removeCommentsFromSql = (sql: string) => { // Removing single-line comments: From 59be6b23aeda37c2b7acf3ed8da19c832756e646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Mon, 14 Aug 2023 18:29:44 +0100 Subject: [PATCH 014/117] Update BerriAI customer story. Optimizations to BerriAI customer story. --- apps/www/_customers/berriai.mdx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index 4f6cc7e97d0..8de5cddad53 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -1,8 +1,8 @@ --- name: Berri AI -title: Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector +title: Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector # Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': -# meta_title: +# meta_title: Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector description: Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase. # Use meta_description to add a custom meta description. Otherwise it defaults to {description}: meta_description: Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase. @@ -42,18 +42,20 @@ Initially, Berri AI attempted to develop with AWS RDS to reduce latency and gain ## Why they chose Supabase: -Berri AI migrated their vector database from AWS RDS to Supabase for several compelling reasons. +Berri AI migrated their vector database from AWS RDS to Supabase for several compelling reasons: -1. **Community:** After considering alternatives like pgvector, Pinecone, and Chroma, they prioritized a solution with a strong and accessible community, along with managed infrastructure. +1. **Community:** After considering alternatives like Pinecone and Chroma, they prioritized a solution with a strong and accessible community, along with [managed infrastructure](https://supabase.com/docs/guides/platform). 2. **Fewer services to manage:** Since they were already utilizing Supabase for other purposes, it made sense for Berri AI to consolidate their systems and avoid the complexities of self-hosting. One of the major pain points they faced with their previous setup was managing connection pools and handling errors with the Postgres database hosted on AWS. This involved writing extensive code and dealing with rollbacks, with each database update function requiring 40-50 lines of code. Supabase simplified this process significantly by condensing it to just one line of code, eliminating the burdens of managing dependencies and handling new errors. This significantly improved their update velocity, which had a huge impact on their productivity. 3. **Better tooling:** Berri AI experienced frustration with pg_admin. Supabase's user-friendly interface offered a seamless and efficient way to manage and read tables, eliminating the need for pg_admin and providing a simple alternative. This streamlined their workflow and saved valuable time and effort. The accessibility of Supabase's client SDKs and intuitive interface played a crucial role in their decision as well. Team members who were not database experts found it easy to navigate and make changes, reducing reliance on specialized expertise and improving their overall workflow. 4. **Simplicity:** Supabase provides the ability to add new columns in a no-code manner, which provided flexibility and agility in responding to evolving requirements. This feature empowered the team to make on-the-fly adjustments and meet their immediate needs effectively. 5. **Flexibility:** Berri AI also appreciated Supabase's flexibility in allowing them to choose their preferred embedding method, as it was not tied to a specific embedding model. This ensured compatibility and adaptability for their specific use case. -The integration of pgvector on Supabase, which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. +![BerriAI is backed by YCombinator]() + +The migration to [Supabase Vector](https://supabase.com/vector), which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. - Switching to Supabase was a game-changer for us. The faster view times allowed me to quickly + Switching to Supabase Vector was a game-changer for us. The faster view times allowed me to quickly access and debug data, enhancing my ability to analyze individual customer issues. The no-code option to add new columns on the fly was fantastic, enabling us to adapt swiftly. @@ -64,7 +66,7 @@ Berri AI built a platform that enables developers to create and manage custom Ch With Berri AI, businesses can rapidly scale the power of ChatGPT to cater to the needs of every user. The platform provides an out-of-the-box solution for managing a fleet of ChatGPT instances, offering businesses the ability to deliver personalized responses to user queries. This includes answering specific questions related to order status, monthly revenue, or generating personalized learning experiences in the education technology (EdTech) sector, such as summarizing lecture slides or generating practice questions from textbooks. -The platform's integration with Supabase and pgvector empowers businesses to unlock the full potential of ChatGPT and deliver customized solutions for a wide range of use cases. +The platform's integration with Supabase empowers businesses to unlock the full potential of ChatGPT and deliver customized solutions for a wide range of use cases. With Supabase, my life has become much easier. Previously, managing connection pools and dealing @@ -77,12 +79,12 @@ The platform's integration with Supabase and pgvector empowers businesses to unl ## The Results -Berri AI achieved significant improvements in their operations by choosing Supabase with pgvector. They experienced streamlined database management, increased update velocity, and improved efficiency in accessing and debugging data. The user-friendly interface and flexibility of Supabase empowered non-experts within the team and facilitated agile adaptations to changing requirements. Overall, this decision transformed their workflow, enhancing productivity and positioning Berri AI for continued success and growth. +Berri AI achieved significant improvements in their operations by choosing Supabase Vector. They experienced streamlined database management, increased update velocity, and improved efficiency in accessing and debugging data. The user-friendly interface and flexibility of Supabase empowered non-experts within the team and facilitated agile adaptations to changing requirements. Overall, this decision transformed their workflow, enhancing productivity and positioning Berri AI for continued success and growth. ## Tech stack -- Supabase - Zeet - AWS - OpenAI - Hugging Face +- Supabase From 91105438dcd54355eb9dadb856eb03a5d62e798d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Mon, 14 Aug 2023 18:33:17 +0100 Subject: [PATCH 015/117] Add image. --- apps/www/_customers/berriai.mdx | 2 +- .../customers/berriai/berriai-image.jpeg | Bin 0 -> 120394 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 apps/www/public/images/customers/berriai/berriai-image.jpeg diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index 8de5cddad53..21457c6ed30 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -50,7 +50,7 @@ Berri AI migrated their vector database from AWS RDS to Supabase for several com 4. **Simplicity:** Supabase provides the ability to add new columns in a no-code manner, which provided flexibility and agility in responding to evolving requirements. This feature empowered the team to make on-the-fly adjustments and meet their immediate needs effectively. 5. **Flexibility:** Berri AI also appreciated Supabase's flexibility in allowing them to choose their preferred embedding method, as it was not tied to a specific embedding model. This ensured compatibility and adaptability for their specific use case. -![BerriAI is backed by YCombinator]() +![BerriAI is backed by YCombinator](/images/customers/berriai/berriai-image.jpeg) The migration to [Supabase Vector](https://supabase.com/vector), which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. diff --git a/apps/www/public/images/customers/berriai/berriai-image.jpeg b/apps/www/public/images/customers/berriai/berriai-image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b84a88ca7252990b2397f2d9d966772250332121 GIT binary patch literal 120394 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

; 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 z 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 TzpNiJt2Usk2VegHdw>7gNwyN| literal 0 HcmV?d00001 From 8e5e9a2dae59a69559bb8150e885d5a94e6ba126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Mon, 14 Aug 2023 18:50:30 +0100 Subject: [PATCH 016/117] Update mendableai.mdx --- apps/www/_customers/mendableai.mdx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 44d70141b7e..752e7a0f9e3 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -1,11 +1,11 @@ --- name: Mendable -title: Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings. +title: Mendable switches from Pinecone to Supabase Vector for PostgreSQL vector embeddings. # Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': -# meta_title: -description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector. +# meta_title: Mendable switches from Pinecone to Supabase Vector for PostgreSQL vector embeddings. +description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase Vector. # Use meta_description to add a custom meta description. Otherwise it defaults to {description}: -meta_description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector. +meta_description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase Vector. author: paul_copplestone author_title: Supabase author_url: https://github.com/kiwicopple @@ -32,11 +32,11 @@ about: Mendable is Chat Powered Search for Documentation. ## The Challenge -Mendable was experiencing tremendous success, growing Weekly Active Users nearly 300% since March. They needed a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations. They tried Faiss, Weaviate, and Pinecone, but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors. +Mendable was experiencing tremendous success, growing Weekly Active Users by nearly 300% since March. They needed a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations. They tried Faiss, Weaviate, and Pinecone, but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors. ## Why they chose Supabase -Mendable discovered that Supabase supports [pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) and found it to be a simple and cost-effective solution. They were impressed with the open source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use. +Mendable lear that Supabase supports [pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) and found it to be a simple and cost-effective solution. They were impressed with the open source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use. We tried other vector databases - we tried Faiss, we tried Weaviate, we tried Pinecone. We found @@ -50,14 +50,18 @@ Using [Supabase Vector](https://supabase.com/vector), Mendable was able to build ## The Results -Thanks to Supabase and pgvector, Mendable was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack. +Thanks to Supabase Vector, Mendable was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack. We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for - all the ways we need use it, Supabase has been just as performant - if not more performant - than + all the ways we need to use it, Supabase has been just as performant - if not more performant - than the other vector databases. ## Tech stack -Mendable's tech stack includes React, Next.js, Express, Vercel, and Supabase. +- React +- Vercel +- Next.js +- Express +- Supabase From e0cab5edd0cf3a4958a4dbdbd226a64720904bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Mon, 14 Aug 2023 18:55:19 +0100 Subject: [PATCH 017/117] Add image. --- apps/www/_customers/mendableai.mdx | 2 ++ .../images/customers/mendable/mendable.png | Bin 0 -> 187862 bytes 2 files changed, 2 insertions(+) create mode 100644 apps/www/public/images/customers/mendable/mendable.png diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 752e7a0f9e3..280cbfa77dc 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -48,6 +48,8 @@ Mendable lear that Supabase supports [pgvector](https://supabase.com/docs/guides Using [Supabase Vector](https://supabase.com/vector), Mendable was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost. +![Mendable -build AI chat search applications](/images/customers/mendable/mendable.png) + ## The Results Thanks to Supabase Vector, Mendable was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack. diff --git a/apps/www/public/images/customers/mendable/mendable.png b/apps/www/public/images/customers/mendable/mendable.png new file mode 100644 index 0000000000000000000000000000000000000000..2008a95037e6f4b9b04196fe9cf917ff9413200c GIT binary patch literal 187862 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 z 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

yKzs1bO&`JhyXD&QSGQS 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 $In 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 Date: Mon, 14 Aug 2023 19:25:43 +0100 Subject: [PATCH 018/117] Add CTA. --- apps/www/_customers/berriai.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index 21457c6ed30..336fc674c58 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -50,8 +50,6 @@ Berri AI migrated their vector database from AWS RDS to Supabase for several com 4. **Simplicity:** Supabase provides the ability to add new columns in a no-code manner, which provided flexibility and agility in responding to evolving requirements. This feature empowered the team to make on-the-fly adjustments and meet their immediate needs effectively. 5. **Flexibility:** Berri AI also appreciated Supabase's flexibility in allowing them to choose their preferred embedding method, as it was not tied to a specific embedding model. This ensured compatibility and adaptability for their specific use case. -![BerriAI is backed by YCombinator](/images/customers/berriai/berriai-image.jpeg) - The migration to [Supabase Vector](https://supabase.com/vector), which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. @@ -64,6 +62,8 @@ The migration to [Supabase Vector](https://supabase.com/vector), which facilitat Berri AI built a platform that enables developers to create and manage custom ChatGPT instances for each individual user, integrating seamlessly with their unique data. +![BerriAI is backed by YCombinator](/images/customers/berriai/berriai-image.jpeg) + With Berri AI, businesses can rapidly scale the power of ChatGPT to cater to the needs of every user. The platform provides an out-of-the-box solution for managing a fleet of ChatGPT instances, offering businesses the ability to deliver personalized responses to user queries. This includes answering specific questions related to order status, monthly revenue, or generating personalized learning experiences in the education technology (EdTech) sector, such as summarizing lecture slides or generating practice questions from textbooks. The platform's integration with Supabase empowers businesses to unlock the full potential of ChatGPT and deliver customized solutions for a wide range of use cases. @@ -81,6 +81,8 @@ The platform's integration with Supabase empowers businesses to unlock the full Berri AI achieved significant improvements in their operations by choosing Supabase Vector. They experienced streamlined database management, increased update velocity, and improved efficiency in accessing and debugging data. The user-friendly interface and flexibility of Supabase empowered non-experts within the team and facilitated agile adaptations to changing requirements. Overall, this decision transformed their workflow, enhancing productivity and positioning Berri AI for continued success and growth. +To learn more about how Supabas Vector can help you store vector embeddings at scale and build AI apps with ease, [reach out to us](https://forms.supabase.com/enterprise). + ## Tech stack - Zeet From 10522fe40bda60eee7a5f56fde8f7df4cb743509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Mon, 14 Aug 2023 19:29:29 +0100 Subject: [PATCH 019/117] Add CTA. --- apps/www/_customers/mendableai.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 280cbfa77dc..9fc986fad65 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -48,7 +48,7 @@ Mendable lear that Supabase supports [pgvector](https://supabase.com/docs/guides Using [Supabase Vector](https://supabase.com/vector), Mendable was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost. -![Mendable -build AI chat search applications](/images/customers/mendable/mendable.png) +![Mendable - build AI chat search applications](/images/customers/mendable/mendable.png) ## The Results @@ -60,6 +60,8 @@ Thanks to Supabase Vector, Mendable was able to significantly improve the effici the other vector databases. +To learn more about how Supabas Vector can help you store vector embeddings at scale and build AI apps with ease, [reach out to us](https://forms.supabase.com/enterprise). + ## Tech stack - React From b098dc25cd392a47255bd5c0bf58f66f3dc0ea45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E8=B6=85?= <2325690622@qq.com> Date: Tue, 15 Aug 2023 17:43:13 +0800 Subject: [PATCH 020/117] Introduce Nuxt3 todo list example --- examples/todo-list/nuxt3-todo-list/readme.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/todo-list/nuxt3-todo-list/readme.md diff --git a/examples/todo-list/nuxt3-todo-list/readme.md b/examples/todo-list/nuxt3-todo-list/readme.md new file mode 100644 index 00000000000..baf548365b8 --- /dev/null +++ b/examples/todo-list/nuxt3-todo-list/readme.md @@ -0,0 +1,3 @@ +# Nuxt3 TODO list example + +Please refer to https://github.com/nuxt-modules/supabase/tree/main/demo From 8129c64e11d6164c51c7dab050cebd38729ed440 Mon Sep 17 00:00:00 2001 From: kishanhitk Date: Tue, 15 Aug 2023 15:46:39 +0530 Subject: [PATCH 021/117] chore: remove logs --- studio/components/interfaces/SQLEditor/SQLEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 082a544b246..f9efcecda73 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -220,14 +220,12 @@ const SQLEditor = () => { const sql = snippet ? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content.sql : selectedValue || editorRef.current?.getValue() - console.log('sql', sql) const containsDestructiveOperations = destructiveSqlRegex.some((regex) => regex.test(removeCommentsFromSql(sql)) ) if (!force && containsDestructiveOperations) { - console.log('Regex match') setIsConfirmModalOpen(true) return } From 47a8746e7a74cb0ca3a0ebb6b07732948d918c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Tue, 15 Aug 2023 12:12:12 +0100 Subject: [PATCH 022/117] Added internal link. --- apps/www/_customers/berriai.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index 336fc674c58..57e2805dd5e 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -48,7 +48,7 @@ Berri AI migrated their vector database from AWS RDS to Supabase for several com 2. **Fewer services to manage:** Since they were already utilizing Supabase for other purposes, it made sense for Berri AI to consolidate their systems and avoid the complexities of self-hosting. One of the major pain points they faced with their previous setup was managing connection pools and handling errors with the Postgres database hosted on AWS. This involved writing extensive code and dealing with rollbacks, with each database update function requiring 40-50 lines of code. Supabase simplified this process significantly by condensing it to just one line of code, eliminating the burdens of managing dependencies and handling new errors. This significantly improved their update velocity, which had a huge impact on their productivity. 3. **Better tooling:** Berri AI experienced frustration with pg_admin. Supabase's user-friendly interface offered a seamless and efficient way to manage and read tables, eliminating the need for pg_admin and providing a simple alternative. This streamlined their workflow and saved valuable time and effort. The accessibility of Supabase's client SDKs and intuitive interface played a crucial role in their decision as well. Team members who were not database experts found it easy to navigate and make changes, reducing reliance on specialized expertise and improving their overall workflow. 4. **Simplicity:** Supabase provides the ability to add new columns in a no-code manner, which provided flexibility and agility in responding to evolving requirements. This feature empowered the team to make on-the-fly adjustments and meet their immediate needs effectively. -5. **Flexibility:** Berri AI also appreciated Supabase's flexibility in allowing them to choose their preferred embedding method, as it was not tied to a specific embedding model. This ensured compatibility and adaptability for their specific use case. +5. **Flexibility:** Berri AI also appreciated Supabase's flexibility in allowing them to choose their [preferred embedding method](https://supabase.com/docs/guides/ai/quickstarts/generate-text-embeddings), as it was not tied to a specific embedding model. This ensured compatibility and adaptability for their specific use case. The migration to [Supabase Vector](https://supabase.com/vector), which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. From 9e8b02a6843001fb4bab5e5ee89663f2e3c98177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Tue, 15 Aug 2023 12:48:40 +0100 Subject: [PATCH 023/117] Update markprompt --- apps/www/_customers/markprompt.mdx | 36 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/www/_customers/markprompt.mdx b/apps/www/_customers/markprompt.mdx index 49808e441ca..d5679d27b8f 100644 --- a/apps/www/_customers/markprompt.mdx +++ b/apps/www/_customers/markprompt.mdx @@ -1,7 +1,7 @@ --- -title: Markprompt and Supabase - GDPR-Compliant AI Chatbots for Docs and Websites. +title: Markprompt and Supabase - GDPR-compliant AI chatbots for docs and websites. name: Markprompt -description: AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions. +description: AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused vector database and authentication solutions. author: paul_copplestone author_title: Supabase author_url: https://github.com/kiwicopple @@ -33,18 +33,18 @@ Markprompt is an AI-powered chatbot platform that simplifies the process of find ## The Challenge -When developing the platform, Markprompt considered GDPR compliance crucial as it enabled them to handle user data responsibly, maintain data privacy, and foster trust and confidence among their customers. As a result, the choice of a robust database and authentication solution became vital in ensuring adherence to GDPR requirements. +When developing the platform, Markprompt considered GDPR compliance crucial as it enabled them to handle user data responsibly, maintain data privacy, and foster trust and confidence among their customers. As a result, the choice of a robust vector database and authentication solution became vital in ensuring adherence to GDPR requirements. -Markprompt has successfully indexed over half a million sections of content, with a steady influx of 10,000 - 50,000 new sections daily. This emphasizes the significance of their vector database solution in terms of reliability and scalability. +Markprompt has successfully indexed over half a million sections of content, with a steady influx of 10,000 - 50,000 new sections daily. This emphasizes the significance of [Supabase Vector](https://supabase.com/vector) in terms of reliability and scalability. We decided to use Supabase over other specialized vector databases because it enabled us to be GDPR compliant from day one with little effort. -## Why they chose Supabase: +## Why they chose Supabase -By utilizing a trusted solution like Supabase for their database and authentication needs, Markprompt can implement access controls, user consent management, and audit trails, which are essential elements for GDPR compliance. This ensures that personal data is handled with care, protected from unauthorized access, and enables Markprompt to meet the strict data protection standards set forth by the GDPR. +By utilizing a trusted solution like Supabase for their vector database and authentication needs, Markprompt can implement access controls, user consent management, and audit trails, which are essential elements for GDPR compliance. This ensures that personal data is handled with care, protected from unauthorized access, and enables Markprompt to meet the strict data protection standards set forth by the GDPR. Markprompt were also impressed with the ease of use and scalability of Supabase. The platform allowed them to focus on building their product, knowing that they had a powerful and reliable database solution backing them up. @@ -55,7 +55,7 @@ Markprompt were also impressed with the ease of use and scalability of Supabase. ## What They Built -Markprompt is a set of API endpoints that allow you to train your content and create a prompt to ask questions to it, for instance for a docs site. Markprompt also designed a user-friendly web dashboard that simplifies content training, synchronization with GitHub repos or websites, access key management, and provides insights into user query statistics. Furthermore, Markprompt's UI components, available as React and Web Components, allow integration of prompts at all touch points of a company, from public websites to internal knowledge bases with just a few lines of code. +Markprompt is a set of API endpoints that allow you to train your content and create a prompt to ask questions to it, for instance for a docs site. Markprompt also designed a user-friendly web dashboard that simplifies content training, synchronization with GitHub repos or websites, access key management, and provides insights into user query statistics. Furthermore, Markprompt's UI components, available as React and Web Components, allow the integration of prompts at all touch points of a company, from public websites to internal knowledge bases with just a few lines of code. Like Supabase, Markprompt is an open-source platform, allowing customers the freedom to host the dashboard and model backend on their own premises. Markprompt warmly welcomes contributions from the community, fostering collaboration and continuous improvement of the platform to meet the evolving needs of developers and technical users. @@ -66,16 +66,18 @@ Like Supabase, Markprompt is an open-source platform, allowing customers the fre ## The Results -Choosing Supabase as their database and authentication solution empowered Markprompt to focus on developing innovative AI-powered solutions without compromising data security or impeding scalability. The seamless integration between Supabase and pgvector enabled them to meet the increasing demands of their expanding user base while upholding stringent GDPR requirements. Consequently, companies gain trust and confidence in the platform, solidifying its reputation as a robust enterprise offering. +Choosing Supabase as their vector database and authentication solution empowered Markprompt to focus on developing innovative AI-powered solutions without compromising data security or impeding scalability. The seamless integration between Supabase and pgvector enabled them to meet the increasing demands of their expanding user base while upholding stringent GDPR requirements. Consequently, companies gain trust and confidence in the platform, solidifying its reputation as a robust enterprise offering. + +To learn more about how Supabase Vector can help you store vector embeddings at scale and build GDPR-compliant apps, [reach out to us](https://forms.supabase.com/enterprise). ## Tech stack -- Next.js - framework -- Vercel - hosting -- Typescript - language -- Tailwind - CSS -- Upstash - Redis and rate limiting -- Supabase - database and auth -- Stripe - payments -- Plain - support chat -- Fathom - analytics +- Next.js +- Vercel +- Typescript +- Tailwind +- Upstash +- Supabase +- Stripe +- Plain +- Fathom From 3cdad1e07baf535f50c8eec09ff49b98d7911e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Tue, 15 Aug 2023 12:51:06 +0100 Subject: [PATCH 024/117] Fixed typo. --- apps/www/_customers/mendableai.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 9fc986fad65..bc4ebf979d8 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -60,7 +60,7 @@ Thanks to Supabase Vector, Mendable was able to significantly improve the effici the other vector databases. -To learn more about how Supabas Vector can help you store vector embeddings at scale and build AI apps with ease, [reach out to us](https://forms.supabase.com/enterprise). +To learn more about how Supabase Vector can help you store vector embeddings at scale and build AI apps with ease, [reach out to us](https://forms.supabase.com/enterprise). ## Tech stack From b0a77ccfcfb7c5333dfd6e9e4f0e3828f7baccaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Tue, 15 Aug 2023 13:03:52 +0100 Subject: [PATCH 025/117] Update happyteams.mdx Added CTA. --- apps/www/_customers/happyteams.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/www/_customers/happyteams.mdx b/apps/www/_customers/happyteams.mdx index 78f713bd753..a1846ccf47f 100644 --- a/apps/www/_customers/happyteams.mdx +++ b/apps/www/_customers/happyteams.mdx @@ -2,7 +2,7 @@ name: HappyTeams title: HappyTeams unlocks better performance and reduces cost with Supabase. # Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': -# meta_title: +# meta_title: HappyTeams unlocks better performance and reduces cost with Supabase. description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. # Use meta_description to add a custom meta description. Otherwise it defaults to {description}: meta_description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. @@ -32,11 +32,11 @@ about: HappyTeams is a purpose-built solution that empowers management to unlock > Finding a painless and hands-free database experience to support growth -[HappyTeams](https://happyteams.io/) is a growing startup with an ambitious vision. The company is bootstrapped, so technical resources are scarce. Devs need to do more with less. This means staying laser-focused on delivering features, rather than database timeouts or performance issues. +HappyTeams is a growing startup with an ambitious vision. The company is bootstrapped, so technical resources are scarce. Devs need to do more with less. This means staying laser-focused on delivering features, rather than database timeouts or performance issues. The company launched its app on Heroku Postgres. But one year in, they were experiencing scaling issues. They needed to handle thousands of users while also shipping new features and building out integrations. Issues with database slowness and concurrent connections were holding them back. -Michael Dever, the founder and CTO of HappyTeams, realized it was time for a change. A large overhaul of the app in early 2022 presented the opportunity to do so: "When you're in that first sprint of building a product, you're kind of locked with that first database decision you make. When we did a rebuild, it was time to look at something like Supabase." +Michael Dever, founder and CTO of HappyTeams, realized it was time for a change. A large overhaul of the app in early 2022 presented the opportunity to do so: "When you're in that first sprint of building a product, you're kind of locked with that first database decision you make. When we did a rebuild, it was time to look at something like Supabase." ![HappyTeams Home Page](/images/blog/2023-02-16-case-study-happyteams/happyteams-home-page.png) @@ -58,7 +58,7 @@ There were four main factors that led HappyTeams to switch from Heroku Postgres ## Supabase provides a fast and reliable production database -After a 30 minute migration, HappyTeams can iterate quickly, add new features, and improve their product based on customer feedback +After a 30-minute migration, HappyTeams can iterate quickly, add new features, and improve their product based on customer feedback Once they made the decision, migrating from Heroku to Supabase was simple. HappyTeams completed their database migration in less than an hour using pg_dump and pg_restore. @@ -74,3 +74,5 @@ Thanks to Supabase's ease of use and reliability, HappyTeams can iterate quickly When we needed to add a comments feature to our app, we could get it done in just two days. Supabase is really like a dream come true for a developer. + +Supabase is one of the best free alternatives to Heroku Postgres. Migrate your Heroku Postgres database to Supabase with this [guide](https://supabase.com/docs/guides/resources/migrating-to-supabase/heroku). From 0d72fe7c8c8f128a9a09a425461db0983128a9f5 Mon Sep 17 00:00:00 2001 From: Suhel Abdulla Date: Tue, 15 Aug 2023 19:19:25 +0530 Subject: [PATCH 026/117] bug fix: notification count not showing in light mode --- .../ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index d11b4f510a2..5bd67cf03ae 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -255,7 +255,7 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { className={alt ? 'px-1' : ''} icon={ hasNewNotifications ? ( -

+

{newNotifications.length}

) : alt ? ( From 2e56f5598a95609a945daab7d575c007f8614f88 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Tue, 15 Aug 2023 10:08:54 -0500 Subject: [PATCH 027/117] fix(blog): missing import in example codeblock --- apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx b/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx index d9899ee5152..506720ad48c 100644 --- a/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx +++ b/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx @@ -265,7 +265,7 @@ import 'https://deno.land/x/xhr@0.2.1/mod.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0' import GPT3Tokenizer from 'https://esm.sh/gpt3-tokenizer@1.1.5' import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0' -import { stripIndent } from 'https://esm.sh/common-tags@1.8.2' +import { oneLine, stripIndent } from 'https://esm.sh/common-tags@1.8.2' import { supabaseClient } from './lib/supabase' export const corsHeaders = { From d5724edd6aa7e3563a943959ce8a3f742828f578 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 14:06:10 +0700 Subject: [PATCH 028/117] Convert POST setup intent in AddNewPaymentMethod to RQ, and update API spec --- .../AddNewPaymentMethodModal.tsx | 50 +-- .../Tier/AddNewPaymentMethodForm.tsx | 90 ------ .../Tier/AddNewPaymentMethodModal.tsx | 162 ---------- .../Tier/PaymentMethodSelection.tsx | 2 +- studio/data/api.d.ts | 297 +++++++++--------- ...on-payment-method-setup-intent-mutation.ts | 59 ++++ 6 files changed, 226 insertions(+), 434 deletions(-) delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodForm.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodModal.tsx create mode 100644 studio/data/organizations/organization-payment-method-setup-intent-mutation.ts diff --git a/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx b/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx index 6b1753d3086..c4c427db142 100644 --- a/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx +++ b/studio/components/interfaces/BillingV2/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx @@ -1,14 +1,14 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { Elements } from '@stripe/react-stripe-js' import { loadStripe } from '@stripe/stripe-js' -import { FC, useCallback, useEffect, useState } from 'react' - import { useTheme } from 'common' -import { useSelectedOrganization, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants' -import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' +import { useCallback, useEffect, useState } from 'react' import { Modal } from 'ui' + +import { useOrganizationPaymentMethodSetupIntent } from 'data/organizations/organization-payment-method-setup-intent-mutation' +import { useSelectedOrganization, useStore } from 'hooks' +import { STRIPE_PUBLIC_KEY } from 'lib/constants' +import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' import AddPaymentMethodForm from './AddPaymentMethodForm' interface AddNewPaymentMethodModalProps { @@ -28,12 +28,26 @@ const AddNewPaymentMethodModal = ({ }: AddNewPaymentMethodModalProps) => { const { ui } = useStore() const [intent, setIntent] = useState() + const selectedOrganization = useSelectedOrganization() const captchaLoaded = useIsHCaptchaLoaded() const [captchaToken, setCaptchaToken] = useState(null) const [captchaRef, setCaptchaRef] = useState(null) + const { mutate: setupIntent } = useOrganizationPaymentMethodSetupIntent({ + onSuccess: (intent) => { + setIntent(intent) + }, + onError: (error) => { + ui.setNotification({ + category: 'error', + error: intent.error, + message: `Failed to setup intent: ${error.message}`, + }) + }, + }) + const captchaRefCallback = useCallback((node) => { setCaptchaRef(node) }, []) @@ -52,7 +66,7 @@ const AddNewPaymentMethodModal = ({ return } - await setupIntent(token ?? undefined) + await initSetupIntent(token ?? undefined) resetCaptcha() } } @@ -65,25 +79,13 @@ const AddNewPaymentMethodModal = ({ captchaRef?.resetCaptcha() } - const selectedOrganization = useSelectedOrganization() + const initSetupIntent = async (hcaptchaToken: string | undefined) => { + const slug = selectedOrganization?.slug + if (!slug) return console.error('Slug is required') + if (!hcaptchaToken) return console.error('HCaptcha token required') - const setupIntent = async (hcaptchaToken: string | undefined) => { setIntent(undefined) - - const orgSlug = selectedOrganization?.slug ?? '' - const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, { - hcaptchaToken, - }) - - if (intent.error) { - return ui.setNotification({ - category: 'error', - error: intent.error, - message: `Failed to setup intent: ${intent.error.message}`, - }) - } else { - setIntent(intent) - } + setupIntent({ slug, hcaptchaToken }) } const { isDarkMode } = useTheme() diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodForm.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodForm.tsx deleted file mode 100644 index bf192b0f350..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodForm.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useState } from 'react' -import { Button, Modal } from 'ui' -import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js' -import { useStore } from 'hooks' - -interface AddNewPaymentMethodFormProps { - returnUrl: string - onCancel: () => void - onConfirm: () => void -} - -// [Joshen] Directly brought over from old Billing folder, so we can deprecate that folder easily next time - -// Stripe docs recommend to use the new SetupIntent flow over -// manually creating and attaching payment methods via the API -// Small UX annoyance here, that the page will be refreshed - -const AddNewPaymentMethodForm = ({ - returnUrl, - onCancel, - onConfirm, -}: AddNewPaymentMethodFormProps) => { - const { ui } = useStore() - const stripe = useStripe() - const elements = useElements() - - const [isSaving, setIsSaving] = useState(false) - - const handleSubmit = async (event: any) => { - event.preventDefault() - - if (!stripe || !elements) { - console.error('Stripe.js has not loaded') - return - } - - setIsSaving(true) - - if (document !== undefined) { - // [Joshen] This is to ensure that any 3DS popup from Stripe remains clickable - document.body.classList.add('!pointer-events-auto') - } - - const { error } = await stripe.confirmSetup({ - elements, - redirect: 'if_required', - confirmParams: { return_url: returnUrl }, - }) - - if (error) { - setIsSaving(false) - ui.setNotification({ - category: 'error', - message: error?.message ?? ' Failed to save card details', - }) - } else { - setIsSaving(false) - onConfirm() - } - - if (document !== undefined) { - document.body.classList.remove('!pointer-events-auto') - } - } - - return ( - - -
- -
-
- - -
- - -
-
- - ) -} - -export default AddNewPaymentMethodForm diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodModal.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodModal.tsx deleted file mode 100644 index d970f551535..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/AddNewPaymentMethodModal.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import HCaptcha from '@hcaptcha/react-hcaptcha' -import { Elements } from '@stripe/react-stripe-js' -import { loadStripe } from '@stripe/stripe-js' -import { useCallback, useEffect, useState } from 'react' - -import { useTheme } from 'common' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useSelectedOrganization, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants' -import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' -import { Modal } from 'ui' -import AddNewPaymentMethodForm from './AddNewPaymentMethodForm' - -// [Joshen] Directly brought over from old Billing folder, so we can deprecate that folder easily next time - -interface AddNewPaymentMethodModalProps { - visible: boolean - returnUrl: string - onCancel: () => void - onConfirm: () => void -} - -const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) - -const AddNewPaymentMethodModal = ({ - visible, - returnUrl, - onCancel, - onConfirm, -}: AddNewPaymentMethodModalProps) => { - const { ui } = useStore() - const [intent, setIntent] = useState() - - const captchaLoaded = useIsHCaptchaLoaded() - const [captchaToken, setCaptchaToken] = useState(null) - const [captchaRef, setCaptchaRef] = useState(null) - - const captchaRefCallback = useCallback((node) => { - setCaptchaRef(node) - }, []) - - useEffect(() => { - const loadPaymentForm = async () => { - if (visible && captchaRef && captchaLoaded) { - let token = captchaToken - - try { - if (!token) { - const captchaResponse = await captchaRef.execute({ async: true }) - token = captchaResponse?.response ?? null - } - } catch (error) { - return - } - - await setupIntent(token ?? undefined) - resetCaptcha() - } - } - - loadPaymentForm() - }, [visible, captchaRef, captchaLoaded]) - - const resetCaptcha = () => { - setCaptchaToken(null) - captchaRef?.resetCaptcha() - } - - const selectedOrganization = useSelectedOrganization() - - const setupIntent = async (hcaptchaToken: string | undefined) => { - setIntent(undefined) - - const orgSlug = selectedOrganization?.slug ?? '' - const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, { - hcaptchaToken, - }) - - if (intent.error) { - return ui.setNotification({ - category: 'error', - error: intent.error, - message: `Failed to setup intent: ${intent.error.message}`, - }) - } else { - setIntent(intent) - } - } - - const { isDarkMode } = useTheme() - - const options = { - clientSecret: intent ? intent.client_secret : '', - appearance: { theme: isDarkMode ? 'night' : 'flat', labels: 'floating' }, - } as any - - const onLocalCancel = () => { - setIntent(undefined) - return onCancel() - } - - const onLocalConfirm = () => { - setIntent(undefined) - return onConfirm() - } - - return ( - <> - { - // [Joshen] This is to ensure that hCaptcha popup remains clickable - if (document !== undefined) document.body.classList.add('!pointer-events-auto') - }} - onClose={() => { - onLocalCancel() - if (document !== undefined) document.body.classList.remove('!pointer-events-auto') - }} - onVerify={(token) => { - setCaptchaToken(token) - if (document !== undefined) document.body.classList.remove('!pointer-events-auto') - }} - onExpire={() => { - setCaptchaToken(null) - }} - /> - - onLocalCancel()} - className="PAYMENT" - > -
- {intent === undefined ? ( -
- - - -
- ) : ( - - - - )} -
-
- - ) -} - -export default AddNewPaymentMethodModal diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx index 877d1cc9f94..c1e5151a91d 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/Tier/PaymentMethodSelection.tsx @@ -9,7 +9,7 @@ import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' import { Button, IconAlertCircle, IconCreditCard, IconLoader, IconPlus, Listbox } from 'ui' -import AddNewPaymentMethodModal from './AddNewPaymentMethodModal' +import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2' export interface PaymentMethodSelectionProps { selectedPaymentMethod?: string diff --git a/studio/data/api.d.ts b/studio/data/api.d.ts index 0ee3535b234..30c7c2551c9 100644 --- a/studio/data/api.d.ts +++ b/studio/data/api.d.ts @@ -1493,10 +1493,8 @@ export interface paths { put: operations["AuthPostgresConfigController_updateConfig"]; }; "/v1/projects/{ref}/config/database/pgbouncer": { - /** Gets project's pgbouncer config */ - get: operations["V1PgbouncerConfigController_getPgbouncerConfig"]; - /** Updates project's pgbouncer config */ - patch: operations["V1PgbouncerConfigController_updatePgbouncerConfig"]; + /** Get project's pgbouncer config */ + get: operations["V1PgbouncerConfigController_v1GetPgbouncerConfig"]; }; "/v1/projects/{ref}/config/auth": { /** Gets project's auth config */ @@ -1775,126 +1773,126 @@ export interface components { EXTERNAL_ZOOM_SECRET: string; }; UpdateGoTrueConfigBody: { - SITE_URL: string; - DISABLE_SIGNUP: boolean; - JWT_EXP: number; - SMTP_ADMIN_EMAIL: string; - SMTP_HOST: string; - SMTP_PORT: string; - SMTP_USER: string; + SITE_URL?: string; + DISABLE_SIGNUP?: boolean; + JWT_EXP?: number; + SMTP_ADMIN_EMAIL?: string; + SMTP_HOST?: string; + SMTP_PORT?: string; + SMTP_USER?: string; SMTP_PASS?: string | null; SMTP_PASS_ENCRYPTED?: string | null; - SMTP_MAX_FREQUENCY: number; + SMTP_MAX_FREQUENCY?: number; SMTP_SENDER_NAME?: string; - MAILER_AUTOCONFIRM: boolean; - MAILER_SUBJECTS_INVITE: string; - MAILER_SUBJECTS_CONFIRMATION: string; - MAILER_SUBJECTS_RECOVERY: string; - MAILER_SUBJECTS_EMAIL_CHANGE: string; - MAILER_SUBJECTS_MAGIC_LINK: string; - MAILER_TEMPLATES_INVITE_CONTENT: string; - MAILER_TEMPLATES_CONFIRMATION_CONTENT: string; - MAILER_TEMPLATES_RECOVERY_CONTENT: string; - MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: string; - MAILER_TEMPLATES_MAGIC_LINK_CONTENT: string; - MFA_MAX_ENROLLED_FACTORS: number; - URI_ALLOW_LIST: string; - EXTERNAL_EMAIL_ENABLED: boolean; - EXTERNAL_PHONE_ENABLED: boolean; + MAILER_AUTOCONFIRM?: boolean; + MAILER_SUBJECTS_INVITE?: string; + MAILER_SUBJECTS_CONFIRMATION?: string; + MAILER_SUBJECTS_RECOVERY?: string; + MAILER_SUBJECTS_EMAIL_CHANGE?: string; + MAILER_SUBJECTS_MAGIC_LINK?: string; + MAILER_TEMPLATES_INVITE_CONTENT?: string; + MAILER_TEMPLATES_CONFIRMATION_CONTENT?: string; + MAILER_TEMPLATES_RECOVERY_CONTENT?: string; + MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT?: string; + MAILER_TEMPLATES_MAGIC_LINK_CONTENT?: string; + MFA_MAX_ENROLLED_FACTORS?: number; + URI_ALLOW_LIST?: string; + EXTERNAL_EMAIL_ENABLED?: boolean; + EXTERNAL_PHONE_ENABLED?: boolean; SAML_ENABLED?: boolean; - SECURITY_CAPTCHA_ENABLED: boolean; - SECURITY_CAPTCHA_PROVIDER: string; - SECURITY_CAPTCHA_SECRET: string; - RATE_LIMIT_EMAIL_SENT: number; - RATE_LIMIT_SMS_SENT: number; - MAILER_SECURE_EMAIL_CHANGE_ENABLED: boolean; - REFRESH_TOKEN_ROTATION_ENABLED: boolean; - PASSWORD_MIN_LENGTH: number; - SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: boolean; - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number; - MAILER_OTP_EXP: number; - SMS_AUTOCONFIRM: boolean; - SMS_MAX_FREQUENCY: number; - SMS_OTP_EXP: number; - SMS_OTP_LENGTH: number; - SMS_PROVIDER: string; - SMS_MESSAGEBIRD_ACCESS_KEY: string; - SMS_MESSAGEBIRD_ORIGINATOR: string; - SMS_TEXTLOCAL_API_KEY: string; - SMS_TEXTLOCAL_SENDER: string; - SMS_TWILIO_ACCOUNT_SID: string; - SMS_TWILIO_AUTH_TOKEN: string; - SMS_TWILIO_MESSAGE_SERVICE_SID: string; - SMS_TWILIO_VERIFY_ACCOUNT_SID: string; - SMS_TWILIO_VERIFY_AUTH_TOKEN: string; - SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID: string; - SMS_VONAGE_API_KEY: string; - SMS_VONAGE_API_SECRET: string; - SMS_VONAGE_FROM: string; - SMS_TEMPLATE: string; - EXTERNAL_APPLE_ENABLED: boolean; - EXTERNAL_APPLE_CLIENT_ID: string; - EXTERNAL_APPLE_SECRET: string; + SECURITY_CAPTCHA_ENABLED?: boolean; + SECURITY_CAPTCHA_PROVIDER?: string; + SECURITY_CAPTCHA_SECRET?: string; + RATE_LIMIT_EMAIL_SENT?: number; + RATE_LIMIT_SMS_SENT?: number; + MAILER_SECURE_EMAIL_CHANGE_ENABLED?: boolean; + REFRESH_TOKEN_ROTATION_ENABLED?: boolean; + PASSWORD_MIN_LENGTH?: number; + SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION?: boolean; + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL?: number; + MAILER_OTP_EXP?: number; + SMS_AUTOCONFIRM?: boolean; + SMS_MAX_FREQUENCY?: number; + SMS_OTP_EXP?: number; + SMS_OTP_LENGTH?: number; + SMS_PROVIDER?: string; + SMS_MESSAGEBIRD_ACCESS_KEY?: string; + SMS_MESSAGEBIRD_ORIGINATOR?: string; + SMS_TEXTLOCAL_API_KEY?: string; + SMS_TEXTLOCAL_SENDER?: string; + SMS_TWILIO_ACCOUNT_SID?: string; + SMS_TWILIO_AUTH_TOKEN?: string; + SMS_TWILIO_MESSAGE_SERVICE_SID?: string; + SMS_TWILIO_VERIFY_ACCOUNT_SID?: string; + SMS_TWILIO_VERIFY_AUTH_TOKEN?: string; + SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID?: string; + SMS_VONAGE_API_KEY?: string; + SMS_VONAGE_API_SECRET?: string; + SMS_VONAGE_FROM?: string; + SMS_TEMPLATE?: string; + EXTERNAL_APPLE_ENABLED?: boolean; + EXTERNAL_APPLE_CLIENT_ID?: string; + EXTERNAL_APPLE_SECRET?: string; EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS?: string; - EXTERNAL_AZURE_ENABLED: boolean; - EXTERNAL_AZURE_CLIENT_ID: string; - EXTERNAL_AZURE_SECRET: string; - EXTERNAL_AZURE_URL: string; - EXTERNAL_BITBUCKET_ENABLED: boolean; - EXTERNAL_BITBUCKET_CLIENT_ID: string; - EXTERNAL_BITBUCKET_SECRET: string; - EXTERNAL_DISCORD_ENABLED: boolean; - EXTERNAL_DISCORD_CLIENT_ID: string; - EXTERNAL_DISCORD_SECRET: string; - EXTERNAL_FACEBOOK_ENABLED: boolean; - EXTERNAL_FACEBOOK_CLIENT_ID: string; - EXTERNAL_FACEBOOK_SECRET: string; - EXTERNAL_FIGMA_ENABLED: boolean; - EXTERNAL_FIGMA_CLIENT_ID: string; - EXTERNAL_FIGMA_SECRET: string; - EXTERNAL_GITHUB_ENABLED: boolean; - EXTERNAL_GITHUB_CLIENT_ID: string; - EXTERNAL_GITHUB_SECRET: string; - EXTERNAL_GITLAB_ENABLED: boolean; - EXTERNAL_GITLAB_CLIENT_ID: string; - EXTERNAL_GITLAB_SECRET: string; - EXTERNAL_GITLAB_URL: string; - EXTERNAL_GOOGLE_ENABLED: boolean; - EXTERNAL_GOOGLE_CLIENT_ID: string; - EXTERNAL_GOOGLE_SECRET: string; + EXTERNAL_AZURE_ENABLED?: boolean; + EXTERNAL_AZURE_CLIENT_ID?: string; + EXTERNAL_AZURE_SECRET?: string; + EXTERNAL_AZURE_URL?: string; + EXTERNAL_BITBUCKET_ENABLED?: boolean; + EXTERNAL_BITBUCKET_CLIENT_ID?: string; + EXTERNAL_BITBUCKET_SECRET?: string; + EXTERNAL_DISCORD_ENABLED?: boolean; + EXTERNAL_DISCORD_CLIENT_ID?: string; + EXTERNAL_DISCORD_SECRET?: string; + EXTERNAL_FACEBOOK_ENABLED?: boolean; + EXTERNAL_FACEBOOK_CLIENT_ID?: string; + EXTERNAL_FACEBOOK_SECRET?: string; + EXTERNAL_FIGMA_ENABLED?: boolean; + EXTERNAL_FIGMA_CLIENT_ID?: string; + EXTERNAL_FIGMA_SECRET?: string; + EXTERNAL_GITHUB_ENABLED?: boolean; + EXTERNAL_GITHUB_CLIENT_ID?: string; + EXTERNAL_GITHUB_SECRET?: string; + EXTERNAL_GITLAB_ENABLED?: boolean; + EXTERNAL_GITLAB_CLIENT_ID?: string; + EXTERNAL_GITLAB_SECRET?: string; + EXTERNAL_GITLAB_URL?: string; + EXTERNAL_GOOGLE_ENABLED?: boolean; + EXTERNAL_GOOGLE_CLIENT_ID?: string; + EXTERNAL_GOOGLE_SECRET?: string; EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS?: string; - EXTERNAL_KAKAO_ENABLED: boolean; - EXTERNAL_KAKAO_CLIENT_ID: string; - EXTERNAL_KAKAO_SECRET: string; - EXTERNAL_KEYCLOAK_ENABLED: boolean; - EXTERNAL_KEYCLOAK_CLIENT_ID: string; - EXTERNAL_KEYCLOAK_SECRET: string; - EXTERNAL_KEYCLOAK_URL: string; - EXTERNAL_LINKEDIN_ENABLED: boolean; - EXTERNAL_LINKEDIN_CLIENT_ID: string; - EXTERNAL_LINKEDIN_SECRET: string; - EXTERNAL_NOTION_ENABLED: boolean; - EXTERNAL_NOTION_CLIENT_ID: string; - EXTERNAL_NOTION_SECRET: string; - EXTERNAL_SLACK_ENABLED: boolean; - EXTERNAL_SLACK_CLIENT_ID: string; - EXTERNAL_SLACK_SECRET: string; - EXTERNAL_SPOTIFY_ENABLED: boolean; - EXTERNAL_SPOTIFY_CLIENT_ID: string; - EXTERNAL_SPOTIFY_SECRET: string; - EXTERNAL_TWITCH_ENABLED: boolean; - EXTERNAL_TWITCH_CLIENT_ID: string; - EXTERNAL_TWITCH_SECRET: string; - EXTERNAL_TWITTER_ENABLED: boolean; - EXTERNAL_TWITTER_CLIENT_ID: string; - EXTERNAL_TWITTER_SECRET: string; - EXTERNAL_WORKOS_ENABLED: boolean; - EXTERNAL_WORKOS_CLIENT_ID: string; - EXTERNAL_WORKOS_SECRET: string; - EXTERNAL_WORKOS_URL: string; - EXTERNAL_ZOOM_ENABLED: boolean; - EXTERNAL_ZOOM_CLIENT_ID: string; - EXTERNAL_ZOOM_SECRET: string; + EXTERNAL_KAKAO_ENABLED?: boolean; + EXTERNAL_KAKAO_CLIENT_ID?: string; + EXTERNAL_KAKAO_SECRET?: string; + EXTERNAL_KEYCLOAK_ENABLED?: boolean; + EXTERNAL_KEYCLOAK_CLIENT_ID?: string; + EXTERNAL_KEYCLOAK_SECRET?: string; + EXTERNAL_KEYCLOAK_URL?: string; + EXTERNAL_LINKEDIN_ENABLED?: boolean; + EXTERNAL_LINKEDIN_CLIENT_ID?: string; + EXTERNAL_LINKEDIN_SECRET?: string; + EXTERNAL_NOTION_ENABLED?: boolean; + EXTERNAL_NOTION_CLIENT_ID?: string; + EXTERNAL_NOTION_SECRET?: string; + EXTERNAL_SLACK_ENABLED?: boolean; + EXTERNAL_SLACK_CLIENT_ID?: string; + EXTERNAL_SLACK_SECRET?: string; + EXTERNAL_SPOTIFY_ENABLED?: boolean; + EXTERNAL_SPOTIFY_CLIENT_ID?: string; + EXTERNAL_SPOTIFY_SECRET?: string; + EXTERNAL_TWITCH_ENABLED?: boolean; + EXTERNAL_TWITCH_CLIENT_ID?: string; + EXTERNAL_TWITCH_SECRET?: string; + EXTERNAL_TWITTER_ENABLED?: boolean; + EXTERNAL_TWITTER_CLIENT_ID?: string; + EXTERNAL_TWITTER_SECRET?: string; + EXTERNAL_WORKOS_ENABLED?: boolean; + EXTERNAL_WORKOS_CLIENT_ID?: string; + EXTERNAL_WORKOS_SECRET?: string; + EXTERNAL_WORKOS_URL?: string; + EXTERNAL_ZOOM_ENABLED?: boolean; + EXTERNAL_ZOOM_CLIENT_ID?: string; + EXTERNAL_ZOOM_SECRET?: string; }; GoTrueConfig: { SITE_URL: string; @@ -3232,6 +3230,7 @@ export interface components { organization_id: number; subscription_id: string; region: string; + cloud_provider: string; back_ups: (components["schemas"]["BackupId"])[]; }; RestartServiceRequest: { @@ -3338,22 +3337,23 @@ export interface components { }, string]>; result?: (Record)[]; }; - ProjectPgBouncerConfig: { - db_dns_name: string; - db_host: string; - db_name: string; - db_port: number; - db_ssl: boolean; - db_user: string; + PgbouncerConfigResponse: { default_pool_size?: number; - ignore_startup_parameters: string; + ignore_startup_parameters?: string; + max_client_conn?: number; + /** @enum {string} */ + pool_mode?: "transaction" | "session" | "statement"; inserted_at: string; + db_dns_name: string; + db_user: string; + db_host: string; + db_port: number; + db_name: string; + db_ssl: boolean; pgbouncer_enabled: boolean; + supavisor_enabled: boolean; /** @enum {string} */ pgbouncer_status: "COMING_DOWN" | "COMING_UP" | "DISABLED" | "ENABLED" | "RELOADING"; - /** @enum {string} */ - pool_mode: "transaction" | "session" | "statement"; - max_client_conn?: number | null; connectionString: string; }; UpdatePgbouncerConfigBody: { @@ -4164,6 +4164,13 @@ export interface components { override_enabled: boolean; override_active_until: string; }; + V1PgbouncerConfigResponse: { + /** @enum {string} */ + pool_mode?: "transaction" | "session" | "statement"; + default_pool_size?: number; + ignore_startup_parameters?: string; + max_client_conn?: number; + }; AuthConfigResponse: { smtp_admin_email?: string; smtp_host?: string; @@ -7673,7 +7680,7 @@ export interface operations { responses: { 200: { content: { - "application/json": components["schemas"]["ProjectPgBouncerConfig"]; + "application/json": components["schemas"]["PgbouncerConfigResponse"]; }; }; /** @description Failed to retrieve project's pgbouncer config */ @@ -9938,8 +9945,8 @@ export interface operations { 500: never; }; }; - /** Gets project's pgbouncer config */ - V1PgbouncerConfigController_getPgbouncerConfig: { + /** Get project's pgbouncer config */ + V1PgbouncerConfigController_v1GetPgbouncerConfig: { parameters: { path: { /** @description Project ref */ @@ -9949,37 +9956,13 @@ export interface operations { responses: { 200: { content: { - "application/json": components["schemas"]["ProjectPgBouncerConfig"]; + "application/json": components["schemas"]["V1PgbouncerConfigResponse"]; }; }; /** @description Failed to retrieve project's pgbouncer config */ 500: never; }; }; - /** Updates project's pgbouncer config */ - V1PgbouncerConfigController_updatePgbouncerConfig: { - parameters: { - path: { - /** @description Project ref */ - ref: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdatePgbouncerConfigBody"]; - }; - }; - responses: { - 200: { - content: { - "application/json": components["schemas"]["UpdatePoolingConfigResponse"]; - }; - }; - 403: never; - /** @description Failed to update project's pgbouncer config */ - 500: never; - }; - }; /** Gets project's auth config */ V1AuthConfigController_getV1AuthConfig: { parameters: { diff --git a/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts b/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts new file mode 100644 index 00000000000..6e5749aab23 --- /dev/null +++ b/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts @@ -0,0 +1,59 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type OrganizationPaymentMethodSetupIntentVariables = { + slug: string + hcaptchaToken: string +} + +export async function setupPaymentMethodIntent({ + slug, + hcaptchaToken, +}: OrganizationPaymentMethodSetupIntentVariables) { + const { data, error } = await post('/platform/organizations/{slug}/payments/setup-intent', { + // @ts-ignore [Joshen] API seems to be having the wrong spec + params: { path: { slug } }, + // @ts-ignore [Joshen] API seems to be having the wrong spec + body: { hcaptchaToken }, + }) + if (error) throw error + return data +} + +type OrganizationPaymentMethodSetupIntentData = Awaited> + +export const useOrganizationPaymentMethodSetupIntent = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + OrganizationPaymentMethodSetupIntentData, + ResponseError, + OrganizationPaymentMethodSetupIntentVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + OrganizationPaymentMethodSetupIntentData, + ResponseError, + OrganizationPaymentMethodSetupIntentVariables + >((vars) => setupPaymentMethodIntent(vars), { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to setup intent: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} From ddcfacc125b65d4adffcf667238e406b7240dd79 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 14:33:31 +0700 Subject: [PATCH 029/117] Convert all independent requests in organization join page to RQ --- studio/data/organizations/keys.ts | 2 + .../organization-join-decline-mutation.ts | 50 +++++++++ .../organization-join-mutation.ts | 50 +++++++++ ...rganization-join-token-validation-query.ts | 52 +++++++++ studio/pages/join/index.tsx | 100 +++++++++--------- 5 files changed, 204 insertions(+), 50 deletions(-) create mode 100644 studio/data/organizations/organization-join-decline-mutation.ts create mode 100644 studio/data/organizations/organization-join-mutation.ts create mode 100644 studio/data/organizations/organization-join-token-validation-query.ts diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 3d8493ce9b0..f4ef5c59c67 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -15,4 +15,6 @@ export const organizationKeys = { migrateBillingPreview: (slug: string | undefined) => ['organizations', slug, 'migrate-billing', 'preview'] as const, taxIds: (slug: string | undefined) => ['organizations', slug, 'tax-ids'] as const, + tokenValidation: (slug: string | undefined, token: string | undefined) => + ['organizations', slug, 'validate-token', token] as const, } diff --git a/studio/data/organizations/organization-join-decline-mutation.ts b/studio/data/organizations/organization-join-decline-mutation.ts new file mode 100644 index 00000000000..480bcd1a57e --- /dev/null +++ b/studio/data/organizations/organization-join-decline-mutation.ts @@ -0,0 +1,50 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { del } from 'data/fetchers' +import { ResponseError } from 'types' + +export type OrganizationJoinDeclineVariables = { + slug: string + invited_id: string +} + +export async function declineJoinOrganization({ + slug, + invited_id, +}: OrganizationJoinDeclineVariables) { + const { data, error } = await del('/platform/organizations/{slug}/members/invite', { + // @ts-ignore [Joshen] API spec might be wrong here + params: { path: { slug }, query: { invited_id } }, + }) + if (error) throw error + return data +} + +type OrganizationJoinDeclineData = Awaited> + +export const useOrganizationJoinDeclineMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => declineJoinOrganization(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to decline organization invite: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/data/organizations/organization-join-mutation.ts b/studio/data/organizations/organization-join-mutation.ts new file mode 100644 index 00000000000..0a7c407e69c --- /dev/null +++ b/studio/data/organizations/organization-join-mutation.ts @@ -0,0 +1,50 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationJoinVariables = { + slug: string + token: string +} + +export async function joinOrganization({ slug, token }: OrganizationJoinVariables) { + const { data, error } = await post('/platform/organizations/{slug}/members/join', { + params: { path: { slug }, query: { token } }, + }) + if (error) throw error + return data +} + +type OrganizationJoinData = Awaited> + +export const useOrganizationJoinMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => joinOrganization(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries(organizationKeys.list()) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to join organization: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/data/organizations/organization-join-token-validation-query.ts b/studio/data/organizations/organization-join-token-validation-query.ts new file mode 100644 index 00000000000..87cce40f05e --- /dev/null +++ b/studio/data/organizations/organization-join-token-validation-query.ts @@ -0,0 +1,52 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'data/fetchers' +import { components } from 'data/api' +import { ResponseError } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationJoinTokenValidationVariables = { + slug: string + token: string +} + +export type TokenInfo = components['schemas']['InviteResponse'] | undefined + +export async function validateTokenInformation( + { slug, token }: OrganizationJoinTokenValidationVariables, + signal?: AbortSignal +) { + const { data, error } = await get(`/platform/organizations/{slug}/members/join`, { + params: { path: { slug }, query: { token } }, + signal, + }) + + if (error) throw error + return data +} + +export type OrganizationJoinTokenValidationData = Awaited< + ReturnType +> +export type OrganizationJoinTokenValidationError = ResponseError + +export const useOrganizationJoinTokenValidationQuery = < + TData = OrganizationJoinTokenValidationData +>( + { slug, token }: OrganizationJoinTokenValidationVariables, + { + enabled = true, + ...options + }: UseQueryOptions< + OrganizationJoinTokenValidationData, + OrganizationJoinTokenValidationError, + TData + > = {} +) => + useQuery( + organizationKeys.tokenValidation(slug, token), + ({ signal }) => validateTokenInformation({ slug, token }, signal), + { + enabled: enabled && typeof slug !== 'undefined', + ...options, + } + ) diff --git a/studio/pages/join/index.tsx b/studio/pages/join/index.tsx index dc9b553a419..54a5c395993 100644 --- a/studio/pages/join/index.tsx +++ b/studio/pages/join/index.tsx @@ -1,30 +1,20 @@ -import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { Button, IconCheckSquare, Loading } from 'ui' -import { useParams } from 'common' -import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' +import { useOrganizationJoinDeclineMutation } from 'data/organizations/organization-join-decline-mutation' +import { useOrganizationJoinMutation } from 'data/organizations/organization-join-mutation' +import { + TokenInfo, + validateTokenInformation, +} from 'data/organizations/organization-join-token-validation-query' import { useStore } from 'hooks' import { useSignOut } from 'lib/auth' -import { delete_, get, post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import { useProfile } from 'lib/profile' -interface ITokenInfo { - organization_name?: string | undefined - token_does_not_exist?: boolean - email_match?: boolean - authorized_user?: boolean - expired_token?: boolean - invite_id?: number -} - -type TokenInfo = ITokenInfo | undefined - const JoinOrganizationPage = () => { - const queryClient = useQueryClient() const router = useRouter() const { slug, token, name } = useParams() const { ui } = useStore() @@ -40,16 +30,48 @@ const JoinOrganizationPage = () => { const loginRedirectLink = `/?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}` + const { mutate: joinOrganization } = useOrganizationJoinMutation({ + onSuccess: () => { + setIsSubmitting(false) + router.push('/') + }, + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to join organization: ${error.message}`, + }) + setIsSubmitting(false) + }, + }) + + const { mutate: declineOrganization } = useOrganizationJoinDeclineMutation({ + onSuccess: () => { + setIsSubmitting(false) + router.push('/') + }, + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to decline invitation: ${error.message}`, + }) + setIsSubmitting(false) + }, + }) + useEffect(() => { const fetchTokenInfo = async () => { - const response = await get(`${API_URL}/organizations/${slug}/members/join?token=${token}`) + if (!slug) return console.error('Slug is required') + if (!token) return console.error('Token is required') - if (response.error) { - setError(response.error) - setTokenInfoLoaded(true) - } else { + try { + const response = await validateTokenInformation({ slug, token }) setTokenInfoLoaded(true) setTokenValidationInfo(response) + } catch (error) { + setError(error) + setTokenInfoLoaded(true) } } @@ -65,39 +87,17 @@ const JoinOrganizationPage = () => { }, [token, router.asPath]) async function handleJoinOrganization() { + if (!slug) return console.error('Slug is required') + if (!token) return console.error('Token is required') setIsSubmitting(true) - const response = await post(`${API_URL}/organizations/${slug}/members/join?token=${token}`, {}) - - if (response.error) { - ui.setNotification({ - error: response.error, - category: 'error', - message: `Failed to join organization: ${response.error.message}`, - }) - setIsSubmitting(false) - } else { - setIsSubmitting(false) - await invalidateOrganizationsQuery(queryClient) - router.push('/') - } + joinOrganization({ slug, token }) } async function handleDeclineJoinOrganization() { + if (!slug) return console.error('Slug is required') + if (!invite_id) return console.error('Invite ID is required') setIsSubmitting(true) - const response = await delete_( - `${API_URL}/organizations/${slug}/members/invite?invited_id=${invite_id}`, - {} - ) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to decline invitation: ${response.error.message}`, - }) - setIsSubmitting(false) - } else { - setIsSubmitting(false) - router.push('/') - } + declineOrganization({ slug, invited_id: invite_id }) } const isError = From d01ce64d1f1c2991d9408679639bef43337034e9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 14:54:43 +0700 Subject: [PATCH 030/117] Remove all restore useEffects, and convert restore project to RQ mutation --- .../ProjectLayout/ProjectPausedState.tsx | 35 ++++++-------- .../data/projects/project-restore-mutation.ts | 46 +++++++++++++++++++ studio/pages/project/[ref]/reports/index.tsx | 8 ---- .../pages/project/[ref]/settings/storage.tsx | 11 +---- .../[ref]/storage/buckets/[bucketId].tsx | 16 ++----- .../project/[ref]/storage/buckets/index.tsx | 12 +---- .../pages/project/[ref]/storage/policies.tsx | 7 +-- 7 files changed, 67 insertions(+), 68 deletions(-) create mode 100644 studio/data/projects/project-restore-mutation.ts diff --git a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx index 8f56ef4002a..3967ddb4f47 100644 --- a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx @@ -1,19 +1,19 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'common' import Link from 'next/link' import { useState } from 'react' +import { Button, IconPauseCircle, Modal } from 'ui' -import { useParams } from 'common' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' +import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' -import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' -import { Button, IconPauseCircle, Modal } from 'ui' -import { useProjectContext } from './ProjectContext' import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' +import { PROJECT_STATUS } from 'lib/constants' +import { useProjectContext } from './ProjectContext' export interface ProjectPausedStateProps { product?: string @@ -36,6 +36,13 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const [showConfirmRestore, setShowConfirmRestore] = useState(false) const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false) + const { mutate: restoreProject } = useProjectRestoreMutation({ + onSuccess: (res, variables) => { + setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING) + ui.setNotification({ category: 'success', message: 'Restoring project' }) + }, + }) + const canResumeProject = useCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume' @@ -51,7 +58,7 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { else setShowConfirmRestore(true) } - const onConfirmRestore = async () => { + const onConfirmRestore = () => { if (!project) { return ui.setNotification({ error: 'Project is required', @@ -59,19 +66,7 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { message: 'Unable to restore: project is required', }) } - - const { error } = await post(`${API_URL}/projects/${project.ref}/restore`, {}) - - if (error) { - ui.setNotification({ - category: 'error', - error, - message: `Failed to restore project: ${error.message}`, - }) - } else { - setProjectStatus(queryClient, project.ref, PROJECT_STATUS.RESTORING) - ui.setNotification({ category: 'success', message: 'Restoring project' }) - } + restoreProject({ ref: project.ref }) } return ( diff --git a/studio/data/projects/project-restore-mutation.ts b/studio/data/projects/project-restore-mutation.ts new file mode 100644 index 00000000000..87c2626eb86 --- /dev/null +++ b/studio/data/projects/project-restore-mutation.ts @@ -0,0 +1,46 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type ProjectRestoreVariables = { + ref: string +} + +export async function restoreProject({ ref }: ProjectRestoreVariables) { + const { data, error } = await post('/platform/projects/{ref}/restore', { + params: { path: { ref } }, + body: {}, + }) + if (error) throw error + return data +} + +type ProjectRestoreData = Awaited> + +export const useProjectRestoreMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => restoreProject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to restore project: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/pages/project/[ref]/reports/index.tsx b/studio/pages/project/[ref]/reports/index.tsx index c03b01da996..4a0f7d9fc26 100644 --- a/studio/pages/project/[ref]/reports/index.tsx +++ b/studio/pages/project/[ref]/reports/index.tsx @@ -9,8 +9,6 @@ import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { createReport } from 'components/to-be-cleaned/Reports/Reports.utils' import Loading from 'components/ui/Loading' import { useCheckPermissions, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' import { useProfile } from 'lib/profile' import { useProjectContentStore } from 'stores/projectContentStore' import { NextPageWithLayout } from 'types' @@ -31,12 +29,6 @@ export const UserReportPage: NextPageWithLayout = () => { subject: { id: profile?.id }, }) - useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${ref}/restore`, {}) - } - }, [project]) - async function loadReports() { await contentStore.load() const reports = contentStore.reports() diff --git a/studio/pages/project/[ref]/settings/storage.tsx b/studio/pages/project/[ref]/settings/storage.tsx index 09dd18e9582..728a44bd4b7 100644 --- a/studio/pages/project/[ref]/settings/storage.tsx +++ b/studio/pages/project/[ref]/settings/storage.tsx @@ -1,23 +1,14 @@ -import { useEffect } from 'react' +import { useParams } from 'common' -import { useParams } from 'common/hooks' import { SettingsLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { StorageSettings } from 'components/to-be-cleaned/Storage' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() - useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${projectRef}/restore`, {}) - } - }, [project]) - return (
diff --git a/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx b/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx index 1dc44fe2deb..99dde9c3777 100644 --- a/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx +++ b/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx @@ -1,15 +1,12 @@ import { find } from 'lodash' import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' - import { useParams } from 'common' + import { StorageLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import StorageBucketsError from 'components/layouts/StorageLayout/StorageBucketsError' import { StorageExplorer } from 'components/to-be-cleaned/Storage' import { useBucketsQuery } from 'data/storage/buckets-query' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { @@ -18,17 +15,10 @@ const PageLayout: NextPageWithLayout = () => { const { data, isSuccess, isError, error } = useBucketsQuery({ projectRef: ref }) const buckets = data ?? [] - - useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${ref}/restore`, {}) - } - }, [ref, project]) - - if (!project) return
- const bucket = find(buckets, { id: bucketId }) + if (!project) return null + return (
{isError && } diff --git a/studio/pages/project/[ref]/storage/buckets/index.tsx b/studio/pages/project/[ref]/storage/buckets/index.tsx index 4d5ef33b303..cb611229f85 100644 --- a/studio/pages/project/[ref]/storage/buckets/index.tsx +++ b/studio/pages/project/[ref]/storage/buckets/index.tsx @@ -1,13 +1,10 @@ -import { useEffect } from 'react' +import { useParams } from 'common' -import { useParams } from 'common/hooks' import { StorageLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import StorageBucketsError from 'components/layouts/StorageLayout/StorageBucketsError' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { useBucketsQuery } from 'data/storage/buckets-query' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' import { NextPageWithLayout } from 'types' /** @@ -16,15 +13,8 @@ import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { ref } = useParams() const { project } = useProjectContext() - const { error, isError } = useBucketsQuery({ projectRef: ref }) - useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${ref}/restore`, {}) - } - }, [ref, project]) - if (!project) return
if (isError) diff --git a/studio/pages/project/[ref]/storage/policies.tsx b/studio/pages/project/[ref]/storage/policies.tsx index 5b42fbbb18d..c858200857f 100644 --- a/studio/pages/project/[ref]/storage/policies.tsx +++ b/studio/pages/project/[ref]/storage/policies.tsx @@ -1,13 +1,11 @@ +import { useParams } from 'common' import { observer } from 'mobx-react-lite' import { useEffect } from 'react' -import { useParams } from 'common' import { StorageLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { StoragePolicies } from 'components/to-be-cleaned/Storage' import { useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' import { NextPageWithLayout } from 'types' /** @@ -20,9 +18,6 @@ const PageLayout: NextPageWithLayout = () => { const { project } = useProjectContext() useEffect(() => { - if (project && project.status === PROJECT_STATUS.INACTIVE) { - post(`${API_URL}/projects/${ref}/restore`, {}) - } meta.roles.load() }, [project]) From 9bd52ca1acd508bf792ab5a8c05a13763e44462d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 15:47:20 +0700 Subject: [PATCH 031/117] Convert pause project request to RQ and upadte PauseProjectButton --- .../Infrastructure/PauseProjectButton.tsx | 49 +++++++------------ .../data/projects/project-pause-mutation.ts | 45 +++++++++++++++++ 2 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 studio/data/projects/project-pause-mutation.ts diff --git a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx index f8c4a5d7d98..b93b7210896 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx @@ -3,32 +3,28 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/router' import { useState } from 'react' +import { Button, IconPause } from 'ui' import { useIsProjectActive, useProjectContext, } from 'components/layouts/ProjectLayout/ProjectContext' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { useProjectPauseMutation } from 'data/projects/project-pause-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useCheckPermissions, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS } from 'lib/constants' -import { Button, IconPause } from 'ui' +import { PROJECT_STATUS } from 'lib/constants' export interface PauseProjectButtonProps {} const PauseProjectButton = () => { - const queryClient = useQueryClient() const { ui } = useStore() - const { project } = useProjectContext() const router = useRouter() + const queryClient = useQueryClient() + const { project } = useProjectContext() const isProjectActive = useIsProjectActive() - const [loading, setLoading] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) - const openModal = () => setIsModalOpen(true) - const closeModal = () => setIsModalOpen(false) - const projectRef = project?.ref ?? '' const isPaused = project?.status === PROJECT_STATUS.INACTIVE const canPauseProject = useCheckPermissions( @@ -36,31 +32,22 @@ const PauseProjectButton = () => { 'queue_jobs.projects.pause' ) - const requestPauseProject = async () => { + const { mutate: pauseProject, isLoading: isPausing } = useProjectPauseMutation({ + onSuccess: (res, variables) => { + setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.PAUSING) + ui.setNotification({ category: 'success', message: 'Pausing project...' }) + router.push(`/project/${projectRef}`) + }, + }) + + const requestPauseProject = () => { if (!canPauseProject) { return ui.setNotification({ category: 'error', message: 'You do not have the required permissions to pause this project', }) } - - setLoading(true) - const res = await post(`${API_URL}/projects/${projectRef}/pause`, {}) - - if (res.error) { - ui.setNotification({ - error: res.error, - category: 'error', - message: `Failed to pause project: ${res.error.message}`, - }) - setLoading(false) - } else { - setProjectStatus(queryClient, projectRef, PROJECT_STATUS.PAUSING) - - ui.setNotification({ category: 'success', message: 'Pausing project' }) - router.push(`/project/${projectRef}`) - } - closeModal() + pauseProject({ ref: projectRef }) } return ( @@ -70,8 +57,8 @@ const PauseProjectButton = () => { -
diff --git a/studio/data/projects/project-upgrade-mutation.ts b/studio/data/projects/project-upgrade-mutation.ts new file mode 100644 index 00000000000..8bec6df6c10 --- /dev/null +++ b/studio/data/projects/project-upgrade-mutation.ts @@ -0,0 +1,47 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type ProjectUpgradeVariables = { + ref: string + target_version: number +} + +export async function upgradeProject({ ref, target_version }: ProjectUpgradeVariables) { + const { data, error } = await post('/v1/projects/{ref}/upgrade', { + params: { path: { ref } }, + body: { target_version }, + }) + if (error) throw error + return data +} + +type ProjectUpgradeData = Awaited> + +export const useProjectUpgradeMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => upgradeProject(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to upgrade project: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} From 1d7f51f51501b74a827b446d94425b19562a92ba Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 17:09:52 +0700 Subject: [PATCH 036/117] Use project services restart mutation in NotificationsPopover and leave comments on the other independent requests there --- .../NotificationsPopover/index.tsx | 48 ++++++++----------- .../project-restart-services-mutation.ts | 3 ++ 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index d8f536d297e..d73abf69f85 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -21,6 +21,7 @@ import { delete_, post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { Project } from 'types' import NotificationRow from './NotificationRow' +import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation' interface NotificationsPopoverProps { alt?: boolean @@ -40,6 +41,17 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { const [projectToRollbackMigration, setProjectToRollbackMigration] = useState() const [targetNotification, setTargetNotification] = useState() + const { mutate: restartProjectServices } = useProjectRestartServicesMutation({ + onSuccess: (res, variables) => { + setProjectToRestart(undefined) + setTargetNotification(undefined) + + setProjectPostgrestStatus(queryClient, variables.ref, 'OFFLINE') + ui.setNotification({ category: 'success', message: `Restarting services` }) + router.push(`/project/${variables.ref}`) + }, + }) + const newNotifications = notifications?.filter( (notification) => notification.notification_status === NotificationStatus.New @@ -72,41 +84,22 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { .map((action) => action.action_type) .filter((actionName) => Object.keys(serviceNamesByActionName).indexOf(actionName) !== -1) .map((actionName) => serviceNamesByActionName[actionName]) - const { error } = await post(`${API_URL}/projects/${ref}/restart-services`, { - restartRequest: { - region, - source_notification_id: id, - services: services, - }, - }) - if (error) { - ui.setNotification({ - category: 'error', - message: `Failed to restart project: ${error.message}`, - error, - }) - } else { - setProjectPostgrestStatus(queryClient, ref, 'OFFLINE') - ui.setNotification({ category: 'success', message: `Restarting services` }) - router.push(`/project/${ref}`) - } - - setProjectToRestart(undefined) - setTargetNotification(undefined) + restartProjectServices({ ref, region, services: services as any, source_notification_id: id }) } const onConfirmProjectApplyMigration = async () => { if (!projectToApplyMigration) return + const data = targetNotification?.data as PostgresqlUpgradeData const { resource } = data.additional as any if (!resource) return + + // [Joshen] Leaving this as an exception for RQ due to dynamic URL const res = await post(`${API_URL}/database/${projectToApplyMigration.ref}/${resource}`, {}) if (!res.error) { const project = await getProjectDetail({ ref: projectToApplyMigration.ref }) - if (project) { - meta.setProjectDetails(project) - } + if (project) meta.setProjectDetails(project) ui.setNotification({ category: 'success', @@ -124,18 +117,19 @@ const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => { const onConfirmProjectRollbackMigration = async () => { if (!projectToRollbackMigration) return + const data = targetNotification?.data as PostgresqlUpgradeData const { resource } = data.additional as any if (!resource) return + + // [Joshen] Leaving this as an exception for RQ due to dynamic URL const res = await delete_( `${API_URL}/database/${projectToRollbackMigration.ref}/${resource}`, {} ) if (!res.error) { const project = await getProjectDetail({ ref: projectToRollbackMigration.ref }) - if (project) { - meta.setProjectDetails(project) - } + if (project) meta.setProjectDetails(project) ui.setNotification({ category: 'success', diff --git a/studio/data/projects/project-restart-services-mutation.ts b/studio/data/projects/project-restart-services-mutation.ts index c3e0d4f3383..0739ab2e42b 100644 --- a/studio/data/projects/project-restart-services-mutation.ts +++ b/studio/data/projects/project-restart-services-mutation.ts @@ -22,12 +22,14 @@ export type ProjectRestartServicesVariables = { | 'walg' | 'autoshutdown' )[] + source_notification_id?: string } export async function restartProjectServices({ ref, region, services = ['postgresql'], + source_notification_id, }: ProjectRestartServicesVariables) { const { data, error } = await post('/platform/projects/{ref}/restart-services', { params: { path: { ref } }, @@ -35,6 +37,7 @@ export async function restartProjectServices({ restartRequest: { region, services, + source_notification_id, }, }, }) From 6e70fda77b786609a894b00b64735ecfb826a5f0 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 17:15:57 +0700 Subject: [PATCH 037/117] Convert password check to RQ and update lib/helpers --- .../data/profile/password-check-mutation.ts | 43 +++++++++++++++++++ studio/lib/helpers.ts | 27 ++++++------ 2 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 studio/data/profile/password-check-mutation.ts diff --git a/studio/data/profile/password-check-mutation.ts b/studio/data/profile/password-check-mutation.ts new file mode 100644 index 00000000000..32e6bc7acd6 --- /dev/null +++ b/studio/data/profile/password-check-mutation.ts @@ -0,0 +1,43 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type PasswordCheckVariables = { + password: string +} + +export async function checkPasswordStrength({ password }: PasswordCheckVariables) { + const { data, error } = await post('/platform/profile/password-check', { body: { password } }) + if (error) throw error + return data +} + +type PasswordCheckData = Awaited> + +export const usePasswordCheckMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => checkPasswordStrength(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to check password: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index 71246f591b5..a03468b00de 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -1,3 +1,4 @@ +import { checkPasswordStrength } from 'data/profile/password-check-mutation' import { post } from 'lib/common/fetch' import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH, PASSWORD_STRENGTH } from 'lib/constants' import { toast } from 'react-hot-toast' @@ -180,37 +181,35 @@ export const copyToClipboard = async (str: string | Promise, callback = } export async function passwordStrength(value: string) { - let message = '' - let warning = '' - let strength = 0 + let message: string = '' + let warning: string = '' + let strength: number = 0 if (value && value !== '') { if (value.length > 99) { message = `${PASSWORD_STRENGTH[0]} Maximum length of password exceeded` warning = `Password should be less than 100 characters` } else { - const response = await post(`${API_URL}/profile/password-check`, { password: value }) - if (!response.error) { - const { result } = response - const score = (PASSWORD_STRENGTH as any)[result.score] + try { + const { result } = await checkPasswordStrength({ password: value }) + const resultScore = result?.score ?? 0 + + const score = (PASSWORD_STRENGTH as any)[resultScore] const suggestions = result.feedback?.suggestions ? result.feedback.suggestions.join(' ') : '' - // set message :string message = `${score} ${suggestions}` - - // set strength :number - strength = result.score + strength = resultScore // warning message for anything below 4 strength :string - if (result.score < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { + if (resultScore < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { warning = `${ result?.feedback?.warning ? result?.feedback?.warning + '.' : '' } You need a stronger password.` } - } else { - toast.error(`Failed to check password strength: ${response.error.message}`) + } catch (error: any) { + toast.error(`Failed to check password strength: ${error.message}`) } } } From 4427a20d1e994445607a77b01d586b3694f65548 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 17:27:30 +0700 Subject: [PATCH 038/117] Convert set up intent in new-with-subscription to RQ mutation --- ...on-payment-method-setup-intent-mutation.ts | 4 +- studio/data/stripe/setup-intent-mutation.ts | 46 +++++++++++++++++ studio/pages/new-with-subscription.tsx | 49 +++++++------------ 3 files changed, 66 insertions(+), 33 deletions(-) create mode 100644 studio/data/stripe/setup-intent-mutation.ts diff --git a/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts b/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts index 6e5749aab23..e442afa877f 100644 --- a/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts +++ b/studio/data/organizations/organization-payment-method-setup-intent-mutation.ts @@ -1,4 +1,4 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { toast } from 'react-hot-toast' import { post } from 'data/fetchers' @@ -37,8 +37,6 @@ export const useOrganizationPaymentMethodSetupIntent = ({ >, 'mutationFn' > = {}) => { - const queryClient = useQueryClient() - return useMutation< OrganizationPaymentMethodSetupIntentData, ResponseError, diff --git a/studio/data/stripe/setup-intent-mutation.ts b/studio/data/stripe/setup-intent-mutation.ts new file mode 100644 index 00000000000..e6fab19727f --- /dev/null +++ b/studio/data/stripe/setup-intent-mutation.ts @@ -0,0 +1,46 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type SetupIntentVariables = { + hcaptchaToken: string +} + +export async function setupIntent({ hcaptchaToken }: SetupIntentVariables) { + const { data, error } = await post('/platform/stripe/setup-intent', { + // @ts-ignore [Joshen] API seems to be having the wrong spec + body: { hcaptchaToken }, + }) + if (error) throw error + return data +} + +type SetupIntentData = Awaited> + +export const useSetupIntent = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => setupIntent(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to setup intent: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/pages/new-with-subscription.tsx b/studio/pages/new-with-subscription.tsx index 48ac8eb952a..c2d7d982ee9 100644 --- a/studio/pages/new-with-subscription.tsx +++ b/studio/pages/new-with-subscription.tsx @@ -1,18 +1,18 @@ -import { useCallback, useEffect, useState } from 'react' -import { observer } from 'mobx-react-lite' - -import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants' -import { useFlag, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { WizardLayout } from 'components/layouts' -import { NextPageWithLayout } from 'types' +import HCaptcha from '@hcaptcha/react-hcaptcha' import { Elements } from '@stripe/react-stripe-js' import { loadStripe } from '@stripe/stripe-js' -import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' -import HCaptcha from '@hcaptcha/react-hcaptcha' -import { NewOrgForm } from 'components/interfaces/Organization' import { useTheme } from 'common' +import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' +import { useCallback, useEffect, useState } from 'react' + +import { NewOrgForm } from 'components/interfaces/Organization' +import { WizardLayout } from 'components/layouts' +import { useSetupIntent } from 'data/stripe/setup-intent-mutation' +import { useFlag, useStore } from 'hooks' +import { STRIPE_PUBLIC_KEY } from 'lib/constants' +import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' +import { NextPageWithLayout } from 'types' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) @@ -22,16 +22,16 @@ const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) const Wizard: NextPageWithLayout = () => { const { ui } = useStore() const router = useRouter() + const { isDarkMode } = useTheme() const [intent, setIntent] = useState() const captchaLoaded = useIsHCaptchaLoaded() + const orgCreationV2 = useFlag('orgcreationv2') const [captchaToken, setCaptchaToken] = useState(null) const [captchaRef, setCaptchaRef] = useState(null) - const orgCreationV2 = useFlag('orgcreationv2') - - const { isDarkMode } = useTheme() + const { mutate: setupIntent } = useSetupIntent({ onSuccess: (res) => setIntent(res) }) const captchaRefCallback = useCallback((node) => { setCaptchaRef(node) @@ -43,23 +43,12 @@ const Wizard: NextPageWithLayout = () => { } }, [orgCreationV2]) - const setupIntent = async (hcaptchaToken: string | undefined) => { + const initSetupIntent = async (hcaptchaToken: string | undefined) => { + if (!hcaptchaToken) return console.error('Hcaptcha token is required') + // Force a reload of Elements, necessary for Stripe setIntent(undefined) - - const intent = await post(`${API_URL}/stripe/setup-intent`, { - hcaptchaToken, - }) - - if (intent.error) { - return ui.setNotification({ - category: 'error', - message: intent.error.message, - error: `Failed to retrieve intent: ${intent.error}`, - }) - } else { - setIntent(intent) - } + setupIntent({ hcaptchaToken }) } const options = { @@ -80,7 +69,7 @@ const Wizard: NextPageWithLayout = () => { return } - await setupIntent(token ?? undefined) + await initSetupIntent(token ?? undefined) resetCaptcha() } } From 3dcb9632f9a3196a667f124b6d509168ad46b5aa Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 17:44:40 +0700 Subject: [PATCH 039/117] Convert create project in /new/slug to use RQ --- .../data/projects/project-create-mutation.ts | 15 ++++- studio/pages/new/[slug].tsx | 59 +++++++++---------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/studio/data/projects/project-create-mutation.ts b/studio/data/projects/project-create-mutation.ts index 09ab5b925cd..b280ce12348 100644 --- a/studio/data/projects/project-create-mutation.ts +++ b/studio/data/projects/project-create-mutation.ts @@ -12,7 +12,10 @@ export type ProjectCreateVariables = { dbPass: string dbRegion: string dbSql?: string + dbPricingTierId?: string + cloudProvider?: string configurationId?: string + customSupabaseRequest?: object } export async function createProject({ @@ -21,18 +24,24 @@ export async function createProject({ dbPass, dbRegion, dbSql, + dbPricingTierId = PRICING_TIER_PRODUCT_IDS.FREE, + cloudProvider = PROVIDERS.AWS.id, configurationId, + customSupabaseRequest, }: ProjectCreateVariables) { const response = await post(`${API_URL}/projects`, { - cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS + cloud_provider: cloudProvider, org_id: organizationId, name, db_pass: dbPass, db_region: dbRegion, db_sql: dbSql, - db_pricing_tier_id: PRICING_TIER_PRODUCT_IDS.FREE, + db_pricing_tier_id: dbPricingTierId, // auth_site_url: _store.selectedVercelProjectUrl, vercel_configuration_id: configurationId, + ...(customSupabaseRequest !== undefined && { + custom_supabase_internal_requests: customSupabaseRequest, + }), }) if (response.error) throw response.error return response as ProjectBase @@ -59,7 +68,7 @@ export const useProjectCreateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to create project: ${data.message}`) + toast.error(`Failed to create new project: ${data.message}`) } else { onError(data, variables, context) } diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 70f25698dde..068c9ae2fae 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -4,7 +4,7 @@ import generator from 'generate-password' import { debounce, isUndefined } from 'lodash' import { observer } from 'mobx-react-lite' import Link from 'next/link' -import Router, { useRouter } from 'next/router' +import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { Alert, @@ -32,12 +32,14 @@ import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { + ProjectCreateVariables, + useProjectCreateMutation, +} from 'data/projects/project-create-mutation' import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions, useFlag, useStore, withAuth } from 'hooks' -import { post } from 'lib/common/fetch' import { - API_URL, AWS_REGIONS, CloudProvider, DEFAULT_MINIMUM_PASSWORD_STRENGTH, @@ -70,7 +72,6 @@ const Wizard: NextPageWithLayout = () => { const [dbPass, setDbPass] = useState('') const [dbRegion, setDbRegion] = useState(PROVIDERS[cloudProvider].default_region) const [dbPricingTierKey, setDbPricingTierKey] = useState(PRICING_TIER_DEFAULT_KEY) - const [newProjectedLoading, setNewProjectLoading] = useState(false) const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('') const [passwordStrengthScore, setPasswordStrengthScore] = useState(0) @@ -93,6 +94,12 @@ const Wizard: NextPageWithLayout = () => { enabled: currentOrg?.subscription_id != undefined, }) + const { mutate: createProject, isLoading: isCreatingNewProject } = useProjectCreateMutation({ + onSuccess: (res) => { + router.push(`/project/${res.ref}/building`) + }, + }) + const orgProjectCount = (allProjects || []).filter( (proj) => proj.organization_id === currentOrg?.id ).length @@ -186,43 +193,31 @@ const Wizard: NextPageWithLayout = () => { } const onClickNext = async () => { - setNewProjectLoading(true) + if (!currentOrg) return console.error('Unable to retrieve current organization') const dbTier = dbPricingTierKey === 'PRO' && !isSpendCapEnabled ? 'PAYG' : dbPricingTierKey - - const data: Record = { - cloud_provider: cloudProvider, - org_id: currentOrg?.id, + const data: ProjectCreateVariables = { + cloudProvider, + organizationId: currentOrg.id, name: projectName.trim(), - db_pass: dbPass, - db_region: dbRegion, - db_pricing_tier_id: (PRICING_TIER_PRODUCT_IDS as any)[dbTier], + dbPass: dbPass, + dbRegion: dbRegion, + dbPricingTierId: (PRICING_TIER_PRODUCT_IDS as any)[dbTier], } if (postgresVersion) { if (!postgresVersion.match(/1[2-9]\..*/)) { - setNewProjectLoading(false) - ui.setNotification({ + return ui.setNotification({ category: 'error', message: `Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3`, }) - return } - data['custom_supabase_internal_requests'] = { + data['customSupabaseRequest'] = { ami: { search_tags: { 'tag:postgresVersion': postgresVersion } }, } } - const response = await post(`${API_URL}/projects`, data) - if (response.error) { - setNewProjectLoading(false) - ui.setNotification({ - error: response.error, - category: 'error', - message: `Failed to create new project: ${response.error.message}`, - }) - } else { - router.push(`/project/${response.ref}/building`) - } + + createProject(data) } // [Joshen] Refactor: DB Password could be a common component @@ -263,7 +258,11 @@ const Wizard: NextPageWithLayout = () => { } footer={
-
@@ -272,8 +271,8 @@ const Wizard: NextPageWithLayout = () => { )} From fa1620674b2db2448dd37e96e749549e02120be0 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 18:09:05 +0700 Subject: [PATCH 040/117] Convert create organization to RQ mutation --- .../organization-create-mutation.ts | 53 +++++++++++++++++++ studio/pages/new/index.tsx | 33 ++++-------- 2 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 studio/data/organizations/organization-create-mutation.ts diff --git a/studio/data/organizations/organization-create-mutation.ts b/studio/data/organizations/organization-create-mutation.ts new file mode 100644 index 00000000000..7e1fb5fe28d --- /dev/null +++ b/studio/data/organizations/organization-create-mutation.ts @@ -0,0 +1,53 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationCreateVariables = { + name: string + kind: string + size?: string +} + +export async function createOrganization({ name, kind, size }: OrganizationCreateVariables) { + const { data, error } = await post('/platform/organizations', { + // @ts-ignore [Joshen] Generated API spec is wrong? + body: { name, kind, ...(kind == 'COMPANY' ? { size } : {}) }, + }) + + if (error) throw error + return data +} + +type OrganizationCreateData = Awaited> + +export const useOrganizationCreateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => createOrganization(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries(organizationKeys.list()) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create new organization: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/pages/new/index.tsx b/studio/pages/new/index.tsx index f89c1d4883f..be52ba18366 100644 --- a/studio/pages/new/index.tsx +++ b/studio/pages/new/index.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query' import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' import { useState } from 'react' @@ -6,10 +5,8 @@ import { Button, Input, Listbox } from 'ui' import { WizardLayout } from 'components/layouts' import Panel from 'components/ui/Panel' -import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' +import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' import { useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import { NextPageWithLayout } from 'types' const ORG_KIND_TYPES = { @@ -35,14 +32,19 @@ const ORG_SIZE_DEFAULT = '1' * No org selected yet, create a new one */ const Wizard: NextPageWithLayout = () => { - const queryClient = useQueryClient() const { ui } = useStore() const router = useRouter() const [orgName, setOrgName] = useState('') const [orgKind, setOrgKind] = useState(ORG_KIND_DEFAULT) const [orgSize, setOrgSize] = useState(ORG_SIZE_DEFAULT) - const [newOrgLoading, setNewOrgLoading] = useState(false) + + const { mutate: createOrganization, isLoading: newOrgLoading } = useOrganizationCreateMutation({ + onSuccess: async (org: any) => { + // [Joshen] API spec is wrong? its returning org type as only having id and name + router.push(`/new/${org.slug}`) + }, + }) function validateOrgName(name: any) { return name.length >= 1 @@ -65,29 +67,14 @@ const Wizard: NextPageWithLayout = () => { const trimmedOrgName = orgName ? orgName.trim() : '' const isOrgNameValid = validateOrgName(trimmedOrgName) if (!isOrgNameValid) { - ui.setNotification({ category: 'error', message: 'Organization name is empty' }) - return + return ui.setNotification({ category: 'error', message: 'Organization name is empty' }) } - setNewOrgLoading(true) - const response = await post(`${API_URL}/organizations`, { + createOrganization({ name: trimmedOrgName, kind: orgKind, ...(orgKind == 'COMPANY' ? { size: orgSize } : {}), }) - - if (response.error) { - setNewOrgLoading(false) - ui.setNotification({ - error: response.error, - category: 'error', - message: `Failed to create organization: ${response.error?.message ?? response.error}`, - }) - } else { - const org = response - await invalidateOrganizationsQuery(queryClient) - router.push(`/new/${org.slug}`) - } } return ( From ab9fb3dfdc869c44969737f57eae7f9537a16c98 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 18:21:56 +0700 Subject: [PATCH 041/117] Convert enabling webhooks to RQ mutation --- studio/data/database/hooks-enable-mutation.ts | 45 +++++++++++++++++++ .../project/[ref]/database/hooks/index.tsx | 34 +++++++------- 2 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 studio/data/database/hooks-enable-mutation.ts diff --git a/studio/data/database/hooks-enable-mutation.ts b/studio/data/database/hooks-enable-mutation.ts new file mode 100644 index 00000000000..2d350a3be10 --- /dev/null +++ b/studio/data/database/hooks-enable-mutation.ts @@ -0,0 +1,45 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type HooksEnableVariables = { + ref: string +} + +export async function enableDatabaseWebhooks({ ref }: HooksEnableVariables) { + const { data, error } = await post('/platform/database/{ref}/hook-enable', { + params: { path: { ref } }, + }) + if (error) throw error + return data +} + +type HooksEnableData = Awaited> + +export const useHooksEnableMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => enableDatabaseWebhooks(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to enable webhooks: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/studio/pages/project/[ref]/database/hooks/index.tsx b/studio/pages/project/[ref]/database/hooks/index.tsx index 12e370a7d77..71f7f6bdf0d 100644 --- a/studio/pages/project/[ref]/database/hooks/index.tsx +++ b/studio/pages/project/[ref]/database/hooks/index.tsx @@ -1,25 +1,22 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import clsx from 'clsx' +import { useParams } from 'common' import { observer } from 'mobx-react-lite' import { useEffect, useState } from 'react' +import { IconLoader } from 'ui' -import { useParams } from 'common' import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal' import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel' import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList' import { DatabaseLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import NoPermission from 'components/ui/NoPermission' +import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation' import { useCheckPermissions, useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import { NextPageWithLayout } from 'types' -import { IconLoader } from 'ui' const HooksPage: NextPageWithLayout = () => { const { meta, ui } = useStore() - const { project } = useProjectContext() const { ref } = useParams() const schemas = meta.schemas.list() @@ -33,24 +30,23 @@ const HooksPage: NextPageWithLayout = () => { const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') - useEffect(() => { - if (ui.selectedProjectRef) meta.hooks.load() - }, [ui.selectedProjectRef]) - - const enableHooksForProject = async () => { - const res = await post(`${API_URL}/database/${ref}/hook-enable`, {}) - if (res.error) { - ui.setNotification({ - category: 'error', - message: `Failed to enable webhooks: ${res.error.message}`, - }) - } else { + const { mutate: enableHooks } = useHooksEnableMutation({ + onSuccess: () => { meta.schemas.load() ui.setNotification({ category: 'success', message: `Successfully enabled webhooks`, }) - } + }, + }) + + useEffect(() => { + if (ui.selectedProjectRef) meta.hooks.load() + }, [ui.selectedProjectRef]) + + const enableHooksForProject = async () => { + if (!ref) return console.error('Project ref is required') + enableHooks({ ref }) } const createHook = () => { From 2350efa815800c67d2bd0793aa2a4ca9855e59db Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 18:34:17 +0700 Subject: [PATCH 042/117] Update setupOrg to use create organization mutation --- .../organization-create-mutation.ts | 4 +- studio/pages/vercel/setupOrg.tsx | 43 ++++++------------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/studio/data/organizations/organization-create-mutation.ts b/studio/data/organizations/organization-create-mutation.ts index 7e1fb5fe28d..d3aa1e97de7 100644 --- a/studio/data/organizations/organization-create-mutation.ts +++ b/studio/data/organizations/organization-create-mutation.ts @@ -7,7 +7,7 @@ import { organizationKeys } from './keys' export type OrganizationCreateVariables = { name: string - kind: string + kind?: string size?: string } @@ -42,7 +42,7 @@ export const useOrganizationCreateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to create new organization: ${data.message}`) + toast.error(`Failed to create organization: ${data.message}`) } else { onError(data, variables, context) } diff --git a/studio/pages/vercel/setupOrg.tsx b/studio/pages/vercel/setupOrg.tsx index fa47213c7ab..e2911b40e59 100644 --- a/studio/pages/vercel/setupOrg.tsx +++ b/studio/pages/vercel/setupOrg.tsx @@ -2,13 +2,11 @@ import { runInAction } from 'mobx' import { observer, useLocalObservable } from 'mobx-react-lite' import { useRouter } from 'next/router' import { ChangeEvent, createContext, useContext, useEffect, useState } from 'react' +import { Button, Input, Select } from 'ui' import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' +import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' import { useOrganizationsQuery } from 'data/organizations/organizations-query' -import { useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import { Button, Input, Select } from 'ui' const PageContext = createContext(null) function SetupOrg() { @@ -74,38 +72,19 @@ function SetupOrg() { export default observer(SetupOrg) const CreateOrganization = observer(({}) => { - const PageState: any = useContext(PageContext) const router = useRouter() - - const { ui, app } = useStore() + const PageState: any = useContext(PageContext) const { data: organizations } = useOrganizationsQuery() const [orgName, setOrgName] = useState('') - const [loading, setLoading] = useState(false) - async function onClick() { - setLoading(true) - const response = await post(`${API_URL}/organizations`, { - name: orgName, - }) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to set up organization: ${response.error.message}`, - }) - setLoading(false) - } else { - const org = response + const { mutate: createOrganization, isLoading: isCreating } = useOrganizationCreateMutation({ + onSuccess: (org) => { PageState.selectedOrg = org - const query = new URLSearchParams(PageState.queryParams).toString() router.push(`/vercel/setupProject?${query}`) - } - } - - function onOrgNameChange(e: any) { - setOrgName(e.target.value) - } + }, + }) return (
@@ -126,10 +105,14 @@ const CreateOrganization = observer(({}) => { placeholder="Organization name" descriptionText="What's the name of your company or team?" value={orgName} - onChange={onOrgNameChange} + onChange={(e: any) => setOrgName(e.target.value)} />
-
From 05c2a2d424896d717e73c69044075f58920b9a2b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 19:37:25 +0700 Subject: [PATCH 043/117] Update setupProject to use create organization mutation --- .../data/projects/project-create-mutation.ts | 4 +- studio/pages/vercel/setupProject.tsx | 142 ++++++++---------- 2 files changed, 67 insertions(+), 79 deletions(-) diff --git a/studio/data/projects/project-create-mutation.ts b/studio/data/projects/project-create-mutation.ts index b280ce12348..6fa51da915c 100644 --- a/studio/data/projects/project-create-mutation.ts +++ b/studio/data/projects/project-create-mutation.ts @@ -15,6 +15,7 @@ export type ProjectCreateVariables = { dbPricingTierId?: string cloudProvider?: string configurationId?: string + authSiteUrl?: string customSupabaseRequest?: object } @@ -27,6 +28,7 @@ export async function createProject({ dbPricingTierId = PRICING_TIER_PRODUCT_IDS.FREE, cloudProvider = PROVIDERS.AWS.id, configurationId, + authSiteUrl, customSupabaseRequest, }: ProjectCreateVariables) { const response = await post(`${API_URL}/projects`, { @@ -37,7 +39,7 @@ export async function createProject({ db_region: dbRegion, db_sql: dbSql, db_pricing_tier_id: dbPricingTierId, - // auth_site_url: _store.selectedVercelProjectUrl, + auth_site_url: authSiteUrl, vercel_configuration_id: configurationId, ...(customSupabaseRequest !== undefined && { custom_supabase_internal_requests: customSupabaseRequest, diff --git a/studio/pages/vercel/setupProject.tsx b/studio/pages/vercel/setupProject.tsx index 6ed3968626f..8b950d33569 100644 --- a/studio/pages/vercel/setupProject.tsx +++ b/studio/pages/vercel/setupProject.tsx @@ -1,31 +1,30 @@ -import { toast } from 'react-hot-toast' -import { createContext, useEffect, useContext, useRef, useState, ChangeEvent } from 'react' -import { useRouter } from 'next/router' -import { observer, useLocalObservable } from 'mobx-react-lite' -import { makeAutoObservable } from 'mobx' -import { debounce } from 'lodash' -import { Button, Input, Listbox } from 'ui' -import { Dictionary } from 'components/grid' import generator from 'generate-password' -import { useStore } from 'hooks' -import { post } from 'lib/common/fetch' -import { passwordStrength } from 'lib/helpers' -import { - PROVIDERS, - DEFAULT_MINIMUM_PASSWORD_STRENGTH, - API_URL, - PRICING_TIER_PRODUCT_IDS, - AWS_REGIONS, -} from 'lib/constants' -import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs' +import { debounce } from 'lodash' +import { makeAutoObservable } from 'mobx' +import { observer, useLocalObservable } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { ChangeEvent, createContext, useContext, useEffect, useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { Button, Input, Listbox } from 'ui' + +import { Dictionary } from 'components/grid' +import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' import { createVercelEnv, fetchVercelProject, prepareVercelEvns, } from 'components/to-be-cleaned/Integration/Vercel.utils' -import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' import Loading from 'components/ui/Loading' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' +import { useProjectCreateMutation } from 'data/projects/project-create-mutation' +import { + AWS_REGIONS, + DEFAULT_MINIMUM_PASSWORD_STRENGTH, + PRICING_TIER_PRODUCT_IDS, + PROVIDERS, +} from 'lib/constants' +import { passwordStrength } from 'lib/helpers' +import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs' interface ISetupProjectStore { token: string @@ -144,19 +143,49 @@ const Connecting = () => ( ) const CreateProject = observer(() => { - const _store = useContext(PageContext) const router = useRouter() - const { ui } = useStore() + const _store = useContext(PageContext) + const [projectName, setProjectName] = useState('') const [dbPass, setDbPass] = useState('') const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') const [passwordStrengthScore, setPasswordStrengthScore] = useState(-1) const [dbRegion, setDbRegion] = useState(PROVIDERS.AWS.default_region) - const [loading, setLoading] = useState(false) + const delayedCheckPasswordStrength = useRef( debounce((value: string) => checkPasswordStrength(value), 300) ).current + const { mutate: createProject, isLoading } = useProjectCreateMutation({ + onSuccess: async (res) => { + const project = { ...res, db_host: `db.${res.ref}.supabase.co`, db_password: dbPass } + _store.supabaseProjectRef = project.ref + + const requiredEnvs = + VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.envs || [] + const envs = prepareVercelEvns(requiredEnvs, project as any) + + await Promise.allSettled( + envs.map(async (env: any) => { + try { + const data = await createVercelEnv({ + ...env, + vercelProjectId: _store.selectedVercelProject?.id, + vercelTeamId: _store.teamId, + vercelToken: _store.token, + }) + return data + } catch (err) { + console.error(`Error: ${err}`) + } + }) + ) + + const query = new URLSearchParams(_store.queryParams).toString() + router.push(`/vercel/complete?${query}`) + }, + }) + const canSubmit = projectName != '' && passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH && @@ -193,67 +222,24 @@ const CreateProject = observer(() => { delayedCheckPasswordStrength(password) } - async function createSupabaseProject(dbSql: string) { - const data = { - cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS - org_id: Number(_store.supabaseOrgId), - name: projectName, - db_pass: dbPass, - db_region: dbRegion, - db_sql: dbSql || '', - db_pricing_tier_id: PRICING_TIER_PRODUCT_IDS.FREE, - auth_site_url: _store.selectedVercelProjectUrl, - vercel_configuration_id: _store.configurationId, - } - const project = await post(`${API_URL}/projects`, data) - return { ...project, db_host: `db.${project.ref}.supabase.co`, db_password: dbPass } - } - async function onCreateProject() { - setLoading(true) - try { - const requiredEnvs = - VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.envs || [] const dbSql = VERCEL_INTEGRATION_CONFIGS.find((x) => x.id == _store.externalId)?.template?.sql || '' - const response = await createSupabaseProject(dbSql) - if (response.error) { - setLoading(false) - ui.setNotification({ - category: 'error', - message: `Failed to create project: ${response.error.message}`, - }) - return - } - - const project = response - _store.supabaseProjectRef = project.ref - - const envs = prepareVercelEvns(requiredEnvs, project) - - await Promise.allSettled( - envs.map(async (env: any) => { - try { - const data = await createVercelEnv({ - ...env, - vercelProjectId: _store.selectedVercelProject?.id, - vercelTeamId: _store.teamId, - vercelToken: _store.token, - }) - return data - } catch (err) { - console.error(`Error: ${err}`) - } - }) - ) - - const query = new URLSearchParams(_store.queryParams).toString() - router.push(`/vercel/complete?${query}`) + createProject({ + cloudProvider: PROVIDERS.AWS.id, + organizationId: Number(_store.supabaseOrgId), + name: projectName, + dbPass: dbPass, + dbRegion: dbRegion, + dbSql: dbSql || '', + dbPricingTierId: PRICING_TIER_PRODUCT_IDS.FREE, + configurationId: _store.configurationId, + authSiteUrl: _store.selectedVercelProjectUrl, + }) } catch (error) { console.error('Error', error) - setLoading(false) } } @@ -322,7 +308,7 @@ const CreateProject = observer(() => {
-
From ee0e9f13c2bdd711be7aaac0a2d0ac7c204066fa Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 20:35:52 +0700 Subject: [PATCH 044/117] Remove unnecessaary import --- studio/lib/common/fetch/base.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/studio/lib/common/fetch/base.ts b/studio/lib/common/fetch/base.ts index 42c4d1fc8be..c7140a3820d 100644 --- a/studio/lib/common/fetch/base.ts +++ b/studio/lib/common/fetch/base.ts @@ -1,7 +1,6 @@ import { getAccessToken } from 'lib/gotrue' import { isUndefined } from 'lodash' import { SupaResponse } from 'types/base' -import { Session } from '@supabase/gotrue-js' export function handleError(e: any, requestId: string): SupaResponse { const message = e?.message ? `An error has occurred: ${e.message}` : 'An error has occurred' From b849a0df7995a4d74875d3105ccec25ac1893960 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 21:16:45 +0700 Subject: [PATCH 045/117] Use organization create mutation in NewOrgForm --- .../Organization/NewOrg/NewOrgForm.tsx | 57 +++++++------------ .../organization-create-mutation.ts | 21 ++++++- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 09568fe8846..9bc561e046b 100644 --- a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -1,4 +1,7 @@ +import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' +import type { PaymentMethod } from '@stripe/stripe-js' import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' import { @@ -12,18 +15,14 @@ import { Toggle, } from 'ui' -import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' -import type { PaymentMethod } from '@stripe/stripe-js' +import { SpendCapModal } from 'components/interfaces/BillingV2' import InformationBox from 'components/ui/InformationBox' import Panel from 'components/ui/Panel' +import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' import { useStore } from 'hooks' -import { isResponseOk, post } from 'lib/common/fetch' -import { API_URL, BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' +import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' -import Link from 'next/link' -import { SpendCapModal } from 'components/interfaces/BillingV2' -import { Organization } from 'types' const ORG_KIND_TYPES = { PERSONAL: 'Personal', @@ -61,6 +60,7 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const [orgName, setOrgName] = useState('') const [orgKind, setOrgKind] = useState(ORG_KIND_DEFAULT) const [orgSize, setOrgSize] = useState(ORG_SIZE_DEFAULT) + // [Joshen] Separate loading state here as there's 2 async processes const [newOrgLoading, setNewOrgLoading] = useState(false) const [paymentMethod, setPaymentMethod] = useState() @@ -69,6 +69,13 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(true) + const { mutateAsync: createOrganization } = useOrganizationCreateMutation({ + onSuccess: async (org: any) => { + await invalidateOrganizationsQuery(queryClient) + router.push(`/new/${org.slug}`) + }, + }) + function validateOrgName(name: any) { const value = name ? name.trim() : '' return value.length >= 1 @@ -92,36 +99,18 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { async function createOrg(paymentMethodId?: string) { const dbTier = dbPricingTierKey === 'PRO' && !isSpendCapEnabled ? 'PAYG' : dbPricingTierKey - - const response = await post( - `${API_URL}/organizations`, - { + try { + await createOrganization({ name: orgName, kind: orgKind, - payment_method: paymentMethodId, tier: 'tier_' + dbTier.toLowerCase(), ...(orgKind == 'COMPANY' ? { size: orgSize } : {}), - }, - // Call new V2 endpoint from API - { - headers: { - Version: '2', - }, - } - ) - - if (!isResponseOk(response)) { - ui.setNotification({ - error: response.error, - category: 'error', - message: `Failed to create organization: ${response.error?.message ?? response.error}`, + payment_method: paymentMethodId, + V2: true, }) - } else { - const org = response - await invalidateOrganizationsQuery(queryClient) - router.push(`/new/${org.slug}`) + } finally { + setNewOrgLoading(false) } - setNewOrgLoading(false) } const handleSubmit = async (event: any) => { @@ -129,13 +118,11 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const isOrgNameValid = validateOrgName(orgName) if (!isOrgNameValid) { - ui.setNotification({ category: 'error', message: 'Organization name is empty' }) - return + return ui.setNotification({ category: 'error', message: 'Organization name is empty' }) } if (!stripe || !elements) { - console.error('Stripe.js has not loaded') - return + return console.error('Stripe.js has not loaded') } setNewOrgLoading(true) diff --git a/studio/data/organizations/organization-create-mutation.ts b/studio/data/organizations/organization-create-mutation.ts index d3aa1e97de7..f0594df216f 100644 --- a/studio/data/organizations/organization-create-mutation.ts +++ b/studio/data/organizations/organization-create-mutation.ts @@ -9,12 +9,29 @@ export type OrganizationCreateVariables = { name: string kind?: string size?: string + tier?: string + payment_method?: string + V2?: boolean } -export async function createOrganization({ name, kind, size }: OrganizationCreateVariables) { +export async function createOrganization({ + name, + kind, + size, + tier, + payment_method, + V2 = false, +}: OrganizationCreateVariables) { const { data, error } = await post('/platform/organizations', { // @ts-ignore [Joshen] Generated API spec is wrong? - body: { name, kind, ...(kind == 'COMPANY' ? { size } : {}) }, + body: { + name, + kind, + ...(kind == 'COMPANY' ? { size } : {}), + ...(tier !== undefined ? { tier } : {}), + ...(payment_method !== undefined ? { payment_method } : {}), + }, + ...(V2 ? { headers: { Version: '2' } } : {}), }) if (error) throw error From 3a6a2ac66a486adb305f2e94a9dc9d5786fc8254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Uribe?= Date: Wed, 16 Aug 2023 09:20:51 -0500 Subject: [PATCH 046/117] Update apps/docs/pages/guides/auth/social-login/auth-google.mdx Co-authored-by: Kang Ming --- .../guides/auth/social-login/auth-google.mdx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index ec767f1f8ba..6dae41b12e5 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -291,15 +291,15 @@ Use of nonce is recommended, though optional. Make sure each nonce is generated ### Important Note on Nonce Validation -When implementing nonce validation there is a specific requirement to understand: +Supabase Auth expects the nonce in Google's ID token to be hashed (specifically with SHA-256 and represented as a hexadecimal string). -Supabase's GoTrue expects the nonce to be hashed (specifically with SHA-256, and then converted to a hexadecimal string), however Google's ID token will contain the raw value of the provided nonce. - -To succeed with nonce validation, you will need to provide a hashed version of the nonce to Google and the non-hashed version to the Supabase client. +To succeed with nonce validation, you will need to provide a hashed version of the nonce to Google and the non-hashed version to the `signInWithIdToken` method. The following code snippet provides an example of how to create both versions in a web application: -``` +```js +// Adapted from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string + import crypto from 'crypto'; ... @@ -312,13 +312,9 @@ const bytes = new Uint8Array(hash); const hashedNonce = Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string .join(''); - -// Use 'nonce' when passing to Supabase's client -// Use 'hashedNonce' when passing to Google's authentication request -``` - -By following this approach, you'll ensure that the nonces are handled in a way that is compatible with both Google's and Supabase's expectations, allowing successful authentication. - + +// Use 'hashedNonce' when making the authentication request to Google +// Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method ### Configuration [#configuration-personalized-sign-in-button] From 434b0ab556b4380121be0b12232a6d55b658f45f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 16 Aug 2023 21:23:50 +0700 Subject: [PATCH 047/117] Refactor reset password to RQ and update ForgotPasswordForm to use it --- .../interfaces/SignIn/ForgotPasswordForm.tsx | 62 +++++++++---------- studio/data/misc/reset-password-mutation.ts | 49 +++++++++++++++ 2 files changed, 78 insertions(+), 33 deletions(-) create mode 100644 studio/data/misc/reset-password-mutation.ts diff --git a/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx b/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx index ff75d6bf90c..08f519c3053 100644 --- a/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx +++ b/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx @@ -1,12 +1,13 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' -import { useStore } from 'hooks' -import { isResponseOk, post } from 'lib/common/fetch' -import { API_URL, BASE_PATH } from 'lib/constants' import { useRouter } from 'next/router' import { useRef, useState } from 'react' import { Button, Form, Input } from 'ui' import { object, string } from 'yup' +import { useResetPasswordMutation } from 'data/misc/reset-password-mutation' +import { useStore } from 'hooks' +import { BASE_PATH } from 'lib/constants' + const forgotPasswordSchema = object({ email: string().email('Must be a valid email').required('Email is required'), }) @@ -18,46 +19,41 @@ const ForgotPasswordForm = () => { const [captchaToken, setCaptchaToken] = useState(null) const captchaRef = useRef(null) - const onForgotPassword = async ({ email }: { email: string }) => { - const toastId = ui.setNotification({ - category: 'loading', - message: `Sending password reset email...`, - }) + const { mutate: resetPassword, isLoading } = useResetPasswordMutation({ + onSuccess: async () => { + ui.setNotification({ + category: 'success', + message: `If you registered using your email and password, you will receive a password reset email.`, + }) + await router.push('/sign-in') + }, + onError: (error) => { + setCaptchaToken(null) + captchaRef.current?.resetCaptcha() + ui.setNotification({ + category: 'error', + message: `Failed to send reset email: ${error.message}`, + }) + }, + }) + + const onForgotPassword = async ({ email }: { email: string }) => { let token = captchaToken if (!token) { const captchaResponse = await captchaRef.current?.execute({ async: true }) token = captchaResponse?.response ?? null } - const response = await post(`${API_URL}/reset-password`, { + resetPassword({ email, - hcaptchaToken: token ?? undefined, + hcaptchaToken: token, redirectTo: `${ process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ? location.origin : process.env.NEXT_PUBLIC_SITE_URL }${BASE_PATH}/reset-password`, }) - - if (isResponseOk(response)) { - ui.setNotification({ - id: toastId, - category: 'success', - message: `If you registered using your email and password, you will receive a password reset email.`, - }) - - await router.push('/sign-in') - } else { - setCaptchaToken(null) - captchaRef.current?.resetCaptcha() - - ui.setNotification({ - id: toastId, - category: 'error', - message: `Failed to send reset email: ${response.error.message}`, - }) - } } return ( @@ -68,7 +64,7 @@ const ForgotPasswordForm = () => { validationSchema={forgotPasswordSchema} onSubmit={onForgotPassword} > - {({ isSubmitting }: { isSubmitting: boolean }) => { + {() => { return ( -
+
)}
-
+
{ return ( <> - +
diff --git a/packages/ui/src/layout/banners/LWAnnouncement.tsx b/packages/ui/src/layout/banners/LWAnnouncement.tsx index c424722ad59..20e456de5ce 100644 --- a/packages/ui/src/layout/banners/LWAnnouncement.tsx +++ b/packages/ui/src/layout/banners/LWAnnouncement.tsx @@ -53,7 +53,7 @@ const LWAnnouncement = ({ className={['border hover:border-scale-800 transition-colors', cardClassName].join(' ')} innerClassName={['!bg-opacity-70', cardInnerClassName].join(' ')} > - +
diff --git a/packages/ui/src/layout/banners/data/Announcement.json b/packages/ui/src/layout/banners/data/Announcement.json index 541012dc0bc..abb5cec8362 100644 --- a/packages/ui/src/layout/banners/data/Announcement.json +++ b/packages/ui/src/layout/banners/data/Announcement.json @@ -2,6 +2,6 @@ "show": true, "text": "Supabase Launch Week 8", "launchDate": "2023-08-07T09:00:00.000-07:00", - "link": "https://supabase.com/launch-week#today", + "link": "https://supabase.com/launch-week", "badge": "Get your ticket" } From 45434bc8abe226a939e40dfe7a72340b4c525af7 Mon Sep 17 00:00:00 2001 From: Bernard Polidario Date: Mon, 21 Aug 2023 15:53:52 +0200 Subject: [PATCH 105/117] Fix typo --- apps/docs/pages/guides/cli/getting-started.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/cli/getting-started.mdx b/apps/docs/pages/guides/cli/getting-started.mdx index dd01d4ffcf7..91d69bfe7f9 100644 --- a/apps/docs/pages/guides/cli/getting-started.mdx +++ b/apps/docs/pages/guides/cli/getting-started.mdx @@ -161,7 +161,7 @@ The CLI provides a number of commands to help you develop your project locally a - [Project](/docs/reference/cli/supabase-projects) and [Organization](/docs/reference/cli/supabase-orgs) management - [Database management](/docs/reference/cli/supabase-db) - [Database migrations](/docs/reference/cli/supabase-migration) and [Database Branching](/docs/reference/cli/supabase-branches) -- [Database debugigng tools](/docs/reference/cli/supabase-inspect-db-calls) +- [Database debugging tools](/docs/reference/cli/supabase-inspect-db-calls) - [Edge Function management](/docs/reference/cli/supabase-functions) - [Auth management](/docs/reference/cli/supabase-functions) - [Types Generators](/docs/reference/cli/supabase-gen) From c05a62eddf2b7fc8f7397e805da00e4ecfe315e2 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 21 Aug 2023 16:56:51 +0300 Subject: [PATCH 106/117] update lw archive links with native a tag --- .../www/components/LaunchWeek/8/LWArchive.tsx | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/apps/www/components/LaunchWeek/8/LWArchive.tsx b/apps/www/components/LaunchWeek/8/LWArchive.tsx index 3c3979af0f8..ba4aa6eb336 100644 --- a/apps/www/components/LaunchWeek/8/LWArchive.tsx +++ b/apps/www/components/LaunchWeek/8/LWArchive.tsx @@ -1,17 +1,8 @@ import React from 'react' import { StyledArticleBadge } from './Releases/components' import Image from 'next/image' -import { useRouter } from 'next/router' const LWArchive = () => { - const router = useRouter() - - const handleNavigate = (link: string) => { - if (typeof window === 'undefined') return - window.scrollTo(0, 0) - router.push(link) - } - return ( - +
) From 0850a859a980e62382067098f78ffbd437d99012 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 21 Aug 2023 17:02:43 +0300 Subject: [PATCH 107/117] switch supabase-logo icon --- studio/public/img/supabase-logo.svg | 31 ++++++++++------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/studio/public/img/supabase-logo.svg b/studio/public/img/supabase-logo.svg index 060c10eb536..2b69d42e7b3 100644 --- a/studio/public/img/supabase-logo.svg +++ b/studio/public/img/supabase-logo.svg @@ -1,26 +1,15 @@ - - - - - + + + + - - - - - - - - - - - - - + + + - - - + + + From 9cae3261eaefb6fdd505052949e6cca36568032b Mon Sep 17 00:00:00 2001 From: Mark Burggraf Date: Mon, 21 Aug 2023 08:57:29 -0700 Subject: [PATCH 108/117] Update migrating-and-upgrading-projects.mdx Added `--use-copy` to the data dump command. It can be an order of magnitude faster for larger databases, and there's no downside that I'm aware of. --- .../pages/guides/platform/migrating-and-upgrading-projects.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx index 6472e628044..f425d5d260d 100644 --- a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx +++ b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx @@ -42,7 +42,7 @@ Migrating projects can be achieved using the Supabase CLI. This is particularly ```bash supabase db dump --db-url "$OLD_DB_URL" -f roles.sql --role-only supabase db dump --db-url "$OLD_DB_URL" -f schema.sql -supabase db dump --db-url "$OLD_DB_URL" -f data.sql --data-only +supabase db dump --db-url "$OLD_DB_URL" -f data.sql --use-copy --data-only ``` ### Restore to your new project From 1052b5e858f81f7adc423aa203086493a9b0df03 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 21 Aug 2023 11:00:15 -0500 Subject: [PATCH 109/117] Rename readme.md to README.md --- examples/todo-list/nuxt3-todo-list/{readme.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/todo-list/nuxt3-todo-list/{readme.md => README.md} (100%) diff --git a/examples/todo-list/nuxt3-todo-list/readme.md b/examples/todo-list/nuxt3-todo-list/README.md similarity index 100% rename from examples/todo-list/nuxt3-todo-list/readme.md rename to examples/todo-list/nuxt3-todo-list/README.md From 4f9874b430e3ca075719fd2739f59480d8d4a8c2 Mon Sep 17 00:00:00 2001 From: Neeraj Walia Date: Mon, 21 Aug 2023 21:25:52 +0530 Subject: [PATCH 110/117] Fix double quote issue in docs search --- packages/ui/src/components/Command/CommandMenu.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Command/CommandMenu.tsx b/packages/ui/src/components/Command/CommandMenu.tsx index 360ef5bc421..0d41f360fac 100644 --- a/packages/ui/src/components/Command/CommandMenu.tsx +++ b/packages/ui/src/components/Command/CommandMenu.tsx @@ -67,6 +67,12 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { useCommandMenu() const showCommandInput = !currentPage || !CHAT_ROUTES.includes(currentPage) + // This function has been added to prevent the use of double quotes in the search docs input due to an issue with the cmdk-supabase module. This function can be removed when we transition to using cmdk. + const handleInputChange = (value: string) => { + const newValue = value.replace(/"/g, '') // Remove double quotes + setSearch(newValue) + } + return ( <> { ref={commandInputRef} placeholder="Type a command or search..." value={search} - onValueChange={setSearch} + onValueChange={handleInputChange} /> )} From b7ce59a567ba2062e05f5e7ab1f1fe23920d93f1 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 21 Aug 2023 16:27:53 -0500 Subject: [PATCH 111/117] fix(docs): markdown rendering in self hosted config docs --- .../guides/self-hosting/analytics/config.mdx | 22 +++++++++++++---- .../pages/guides/self-hosting/auth/config.mdx | 22 +++++++++++++---- .../guides/self-hosting/realtime/config.mdx | 22 +++++++++++++---- .../guides/self-hosting/storage/config.mdx | 24 +++++++++++++------ spec/storage_v0_config.yaml | 2 ++ 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/apps/docs/pages/guides/self-hosting/analytics/config.mdx b/apps/docs/pages/guides/self-hosting/analytics/config.mdx index 8267f6a913e..97dc3bf3a97 100644 --- a/apps/docs/pages/guides/self-hosting/analytics/config.mdx +++ b/apps/docs/pages/guides/self-hosting/analytics/config.mdx @@ -1,18 +1,19 @@ import Layout from '~/layouts/DefaultGuideLayout' import { getAnalyticsConfigV0 } from '~/lib/mdx/getConfig' import Param from '~/components/Params' - -export const spec = getAnalyticsConfigV0() +import { serialize } from 'next-mdx-remote/serialize' +import components from '~/components' +import { MDXRemote } from 'next-mdx-remote' export const meta = { title: 'Analytics Self-hosting Config', description: 'How to configure and deploy Supabase Analytics.', } -
{spec.info.description}
+
-{spec.info.tags.map(tag => { +{props.spec.info.tags.map(tag => { return ( <>

{tag.title}

@@ -20,7 +21,7 @@ export const meta = {
Parameters
    - {spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { + {props.spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { return })} @@ -36,3 +37,14 @@ export const meta = { export const Page = ({ children }) => export default Page + +export const getStaticProps = async () => { + const spec = getAnalyticsConfigV0() + const descriptionSource = await serialize(spec.info.description) + return { + props: { + descriptionSource, + spec, + }, + } +} diff --git a/apps/docs/pages/guides/self-hosting/auth/config.mdx b/apps/docs/pages/guides/self-hosting/auth/config.mdx index 0e894114c65..e62b9746ad9 100644 --- a/apps/docs/pages/guides/self-hosting/auth/config.mdx +++ b/apps/docs/pages/guides/self-hosting/auth/config.mdx @@ -1,18 +1,19 @@ import Layout from '~/layouts/DefaultGuideLayout' import { getAuthConfigV1 } from '~/lib/mdx/getConfig' import Param from '~/components/Params' - -export const spec = getAuthConfigV1() +import { serialize } from 'next-mdx-remote/serialize' +import components from '~/components' +import { MDXRemote } from 'next-mdx-remote' export const meta = { title: 'Auth Self-hosting Config', description: 'How to configure and deploy Supabase.', } -
    {spec.info.description}
    +
    -{spec.info.tags.map(tag => { +{props.spec.info.tags.map(tag => { return ( <>

    {tag.title}

    @@ -20,7 +21,7 @@ export const meta = {
    Parameters
      - {spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { + {props.spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { return })} @@ -36,3 +37,14 @@ export const meta = { export const Page = ({ children }) => export default Page + +export const getStaticProps = async () => { + const spec = getAuthConfigV1() + const descriptionSource = await serialize(spec.info.description) + return { + props: { + descriptionSource, + spec, + }, + } +} diff --git a/apps/docs/pages/guides/self-hosting/realtime/config.mdx b/apps/docs/pages/guides/self-hosting/realtime/config.mdx index f576951b201..d960852fe5d 100644 --- a/apps/docs/pages/guides/self-hosting/realtime/config.mdx +++ b/apps/docs/pages/guides/self-hosting/realtime/config.mdx @@ -1,20 +1,21 @@ import Layout from '~/layouts/DefaultGuideLayout' import { getRealtimeConfigV0 } from '~/lib/mdx/getConfig' import Param from '~/components/Params' - -export const spec = getRealtimeConfigV0() +import { serialize } from 'next-mdx-remote/serialize' +import components from '~/components' +import { MDXRemote } from 'next-mdx-remote' export const meta = { title: 'Realtime Self-hosting Config', description: 'How to configure and deploy Supabase.', } -
      {spec.info.description}
      + See [this guide](/docs/guides/realtime/bring-your-own-database) if you want to configure your database to work with Realtime.
      -{spec.info.tags.map(tag => { +{props.spec.info.tags.map(tag => { return ( <>

      {tag.title}

      @@ -22,7 +23,7 @@ See [this guide](/docs/guides/realtime/bring-your-own-database) if you want to c
      Parameters
        - {spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { + {props.spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { return })} @@ -38,3 +39,14 @@ See [this guide](/docs/guides/realtime/bring-your-own-database) if you want to c export const Page = ({ children }) => export default Page + +export const getStaticProps = async () => { + const spec = getRealtimeConfigV0() + const descriptionSource = await serialize(spec.info.description) + return { + props: { + descriptionSource, + spec, + }, + } +} diff --git a/apps/docs/pages/guides/self-hosting/storage/config.mdx b/apps/docs/pages/guides/self-hosting/storage/config.mdx index c6d637300b2..76f04369ad5 100644 --- a/apps/docs/pages/guides/self-hosting/storage/config.mdx +++ b/apps/docs/pages/guides/self-hosting/storage/config.mdx @@ -1,20 +1,19 @@ import Layout from '~/layouts/DefaultGuideLayout' import { getStorageConfigV0 } from '~/lib/mdx/getConfig' import Param from '~/components/Params' - -export const spec = getStorageConfigV0() +import { serialize } from 'next-mdx-remote/serialize' +import components from '~/components' +import { MDXRemote } from 'next-mdx-remote' export const meta = { title: 'Storage Self-hosting Config', description: 'How to configure and deploy Supabase.', } -A sample `.env` file is located in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). - -
        {spec.info.description}
        +
        -{spec.info.tags.map(tag => { +{props.spec.info.tags.map(tag => { return ( <>

        {tag.title}

        @@ -22,7 +21,7 @@ A sample `.env` file is located in the [storage repository](https://github.com/s
        Parameters
          - {spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { + {props.spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => { return })} @@ -38,3 +37,14 @@ A sample `.env` file is located in the [storage repository](https://github.com/s export const Page = ({ children }) => export default Page + +export const getStaticProps = async () => { + const spec = getStorageConfigV0() + const descriptionSource = await serialize(spec.info.description) + return { + props: { + descriptionSource, + spec, + }, + } +} diff --git a/spec/storage_v0_config.yaml b/spec/storage_v0_config.yaml index c39b7073fe1..fd45b2d862f 100644 --- a/spec/storage_v0_config.yaml +++ b/spec/storage_v0_config.yaml @@ -9,6 +9,8 @@ info: bugs: 'https://github.com/supabase/storage-api/issues' # {string} Where developers can file bugs. spec: 'https://github.com/supabase/supabase/blob/master/spec/storage_v1_config.yml' # {string} Where developers can find this spec (to link directly in the docs). description: | + A sample `.env` file is located in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). + Use this file to configure your environment variables for your Storage server. tags: - id: general From 4f231f8226498e6b522427eb5e2256c21dc464f4 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Aug 2023 10:30:24 +0800 Subject: [PATCH 112/117] Remove unnecessary prefetch for table columns query --- studio/data/database/table-columns-query.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/studio/data/database/table-columns-query.ts b/studio/data/database/table-columns-query.ts index e4d7f9a5c46..8049252252f 100644 --- a/studio/data/database/table-columns-query.ts +++ b/studio/data/database/table-columns-query.ts @@ -97,18 +97,3 @@ export const useTableColumnsQuery = { - const prefetch = useExecuteSqlPrefetch() - - return useCallback( - ({ projectRef, connectionString }: TableColumnsVariables) => - prefetch({ - projectRef, - connectionString, - sql: getTableColumnsQuery(), - queryKey: ['table-columns'], - }), - [prefetch] - ) -} From b82e1368dd506ec29a11291f19582371f878268d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Aug 2023 10:37:10 +0800 Subject: [PATCH 113/117] Fix --- .../interfaces/Database/Indexes/CreateIndexSidePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index ed43d312130..80a825545af 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -51,7 +51,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = isLoading: isLoadingTableColumns, isSuccess: isSuccessTableColumns, } = useTableColumnsQuery({ - table: 'countries', + table: selectedEntity, projectRef: project?.ref, connectionString: project?.connectionString, }) From 31cf9d7dbbada9a3863cc247630890db2bf028ab Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Tue, 22 Aug 2023 06:12:01 +0200 Subject: [PATCH 114/117] chore: update image versions for docker/docker-compose.yml --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c3274ae7212..e51fac0489a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -275,7 +275,7 @@ services: meta: container_name: supabase-meta - image: supabase/postgres-meta:v0.66.3 + image: supabase/postgres-meta:v0.68.0 depends_on: db: # Disable this if you are using an external Postgres database From 3977551e13ada31cb6cf09fc91161d7d502269cf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Aug 2023 14:32:27 +0800 Subject: [PATCH 115/117] Fix loading state --- .../components/to-be-cleaned/Dropdown/OrganizationDropdown.js | 3 +-- studio/pages/projects.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js index da5981aec14..fab3de0d5d6 100644 --- a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js +++ b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js @@ -3,9 +3,8 @@ import { useRouter } from 'next/router' import { useFlag } from 'hooks' import { Button, Dropdown, IconPlus } from 'ui' -const OrganizationDropdown = ({ organizations }) => { +const OrganizationDropdown = ({ organizations = [] }) => { const router = useRouter() - const orgCreationV2 = useFlag('orgcreationv2') return ( diff --git a/studio/pages/projects.tsx b/studio/pages/projects.tsx index 5b26b3ccdb1..c7e183eced8 100644 --- a/studio/pages/projects.tsx +++ b/studio/pages/projects.tsx @@ -42,7 +42,7 @@ const ProjectsPage: NextPageWithLayout = () => { return ( <> - {navLayoutV2 && isLoading && ( + {(navLayoutV2 || isLoading) && (
          From 73b54c1a742c21d489c6199d9c3fde54dd0e6226 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 22 Aug 2023 14:35:38 +0800 Subject: [PATCH 116/117] Fix --- .../components/to-be-cleaned/Dropdown/OrganizationDropdown.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js index fab3de0d5d6..f522414b564 100644 --- a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js +++ b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js @@ -2,8 +2,9 @@ import { useRouter } from 'next/router' import { useFlag } from 'hooks' import { Button, Dropdown, IconPlus } from 'ui' +import { EMPTY_ARR } from 'lib/void' -const OrganizationDropdown = ({ organizations = [] }) => { +const OrganizationDropdown = ({ organizations = EMPTY_ARR }) => { const router = useRouter() const orgCreationV2 = useFlag('orgcreationv2') From b7e502ae22ae745e75bda12490cbbabc95ff4ade Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Tue, 22 Aug 2023 08:42:13 +0200 Subject: [PATCH 117/117] feat: use single `getAccessToken` in react query --- studio/data/auth/session-access-token-query.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/studio/data/auth/session-access-token-query.ts b/studio/data/auth/session-access-token-query.ts index 46068987ff2..89843071276 100644 --- a/studio/data/auth/session-access-token-query.ts +++ b/studio/data/auth/session-access-token-query.ts @@ -2,15 +2,18 @@ import { useCallback } from 'react' import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import { authKeys } from './keys' -import { auth } from 'lib/gotrue' +import { auth, getAccessToken } from 'lib/gotrue' export async function getSessionAccessToken() { // ignore if server-side if (typeof window === 'undefined') return '' - const { - data: { session }, - } = await auth.getSession() - return session?.access_token + + try { + return await getAccessToken() + } catch (e: any) { + // ignore the error + return null + } } export type SessionAccessTokenData = Awaited>
{ type="email" label="Email" placeholder="you@example.com" - disabled={isSubmitting} + disabled={isLoading} autoComplete="email" /> @@ -102,8 +98,8 @@ const ForgotPasswordForm = () => { form="forgot-password-form" htmlType="submit" size="medium" - disabled={isSubmitting} - loading={isSubmitting} + disabled={isLoading} + loading={isLoading} > Send Reset Email diff --git a/studio/data/misc/reset-password-mutation.ts b/studio/data/misc/reset-password-mutation.ts new file mode 100644 index 00000000000..c9f1e6cbb12 --- /dev/null +++ b/studio/data/misc/reset-password-mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type ResetPasswordVariables = { + email: string + hcaptchaToken: string | null + redirectTo: string +} + +export async function resetPassword({ email, hcaptchaToken, redirectTo }: ResetPasswordVariables) { + const { data, error } = await post('/platform/reset-password', { + // @ts-ignore + body: { email, hcaptchaToken, redirectTo }, + }) + + if (error) throw error + return data +} + +type ResetPasswordData = Awaited> + +export const useResetPasswordMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => resetPassword(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to reset password: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} From af81210ba353500e3e57cc11cf54defec9b265bb Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 17 Aug 2023 00:29:25 +0800 Subject: [PATCH 048/117] Update apps/docs/pages/guides/auth/social-login/auth-google.mdx --- apps/docs/pages/guides/auth/social-login/auth-google.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index 6dae41b12e5..1c71ed2b408 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -291,6 +291,7 @@ Use of nonce is recommended, though optional. Make sure each nonce is generated ### Important Note on Nonce Validation + Supabase Auth expects the nonce in Google's ID token to be hashed (specifically with SHA-256 and represented as a hexadecimal string). To succeed with nonce validation, you will need to provide a hashed version of the nonce to Google and the non-hashed version to the `signInWithIdToken` method. From 8baa1e137a4a98fc787a1d314c6ece5cd3d3cf14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Uribe?= Date: Wed, 16 Aug 2023 11:32:15 -0500 Subject: [PATCH 049/117] Fix prettier inside code-block --- .../guides/auth/social-login/auth-google.mdx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index 1c71ed2b408..ee774b24fbc 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -301,21 +301,22 @@ The following code snippet provides an example of how to create both versions in ```js // Adapted from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string -import crypto from 'crypto'; +import crypto from "crypto"; -... +// ... -const nonce = crypto.randomBytes(16).toString('base64'); // Generate a random nonce +const nonce = crypto.randomBytes(16).toString("base64"); // Generate a random nonce const encoder = new TextEncoder(); const encodedNonce = encoder.encode(nonce); // Encode the nonce -const hash = await crypto.subtle.digest('SHA-256', encodedNonce); // Hash it with SHA-256 +const hash = await crypto.subtle.digest("SHA-256", encodedNonce); // Hash it with SHA-256 const bytes = new Uint8Array(hash); const hashedNonce = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string - .join(''); - + .map((b) => b.toString(16).padStart(2, "0")) // Convert the hash to a hexadecimal string + .join(""); + // Use 'hashedNonce' when making the authentication request to Google // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method +``` ### Configuration [#configuration-personalized-sign-in-button] From f49ffcde256d8991252b9f21875d1a9858f71c84 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 16 Aug 2023 11:35:01 -0500 Subject: [PATCH 050/117] Update maturity-model.mdx Fix broken link --- apps/docs/pages/guides/platform/maturity-model.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/platform/maturity-model.mdx b/apps/docs/pages/guides/platform/maturity-model.mdx index 8bd38da5cf7..b86279f3953 100644 --- a/apps/docs/pages/guides/platform/maturity-model.mdx +++ b/apps/docs/pages/guides/platform/maturity-model.mdx @@ -26,7 +26,7 @@ Resources: Once your application is live, you should never change your database using the Dashboard. Supabase provides various [access levels](https://supabase.com/docs/guides/platform/access-control) for the Dashboard that can help enforce this. Some other important things to consider at this point include: -- Read the [Production Checklist](docs/guides/platform/going-into-prod). +- Read the [Production Checklist](/docs/guides/platform/going-into-prod). - Make your team aware of the [Shared Responsibilities](/docs/guides/platform/shared-responsibility-model) between your organization and Supabase. - Design a [safe workflow](https://supabase.com/docs/guides/platform/shared-responsibility-model#you-decide-your-own-workflow) for managing your database. We strongly recommend running [multiple environments](/docs/guides/cli/managing-environments) as part of your development workflow (`local` -> `staging` -> `prod`). - As your database to grows, we strongly recommend moving to [Point-in-Time Recovery](/docs/guides/platform/backups#point-in-time-recovery). This is safer and has less impact on your database performance during maintenance windows. From 7a8b65fc0724ac0405636ded68bfea94a825b637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Uribe?= Date: Wed, 16 Aug 2023 11:45:21 -0500 Subject: [PATCH 051/117] Use single quotes to comply with custom prettier config --- .../guides/auth/social-login/auth-google.mdx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index ee774b24fbc..e1c4a8f9516 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -301,21 +301,22 @@ The following code snippet provides an example of how to create both versions in ```js // Adapted from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string -import crypto from "crypto"; +import crypto from 'crypto' // ... -const nonce = crypto.randomBytes(16).toString("base64"); // Generate a random nonce -const encoder = new TextEncoder(); -const encodedNonce = encoder.encode(nonce); // Encode the nonce -const hash = await crypto.subtle.digest("SHA-256", encodedNonce); // Hash it with SHA-256 -const bytes = new Uint8Array(hash); +const nonce = crypto.randomBytes(16).toString('base64') // Generate a random nonce +const encoder = new TextEncoder() +const encodedNonce = encoder.encode(nonce) // Encode the nonce +const hash = await crypto.subtle.digest('SHA-256', encodedNonce) // Hash it with SHA-256 +const bytes = new Uint8Array(hash) const hashedNonce = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) // Convert the hash to a hexadecimal string - .join(""); + .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string + .join('') // Use 'hashedNonce' when making the authentication request to Google // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method + ``` ### Configuration [#configuration-personalized-sign-in-button] From 33c34cfd11afb5984624758de83ffd8a2fb3a841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Uribe?= Date: Wed, 16 Aug 2023 12:02:47 -0500 Subject: [PATCH 052/117] Fix indentations and whitespace to make Prettier happy --- apps/docs/pages/guides/auth/social-login/auth-google.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index e1c4a8f9516..b066163eefb 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -289,7 +289,6 @@ async function handleSignInWithGoogle(response) { Use of nonce is recommended, though optional. Make sure each nonce is generated randomly and available both in the `data-nonce` attribute as well as in the `handleSignInWithGoogle` callback function; otherwise the ID token will not be accepted. - ### Important Note on Nonce Validation Supabase Auth expects the nonce in Google's ID token to be hashed (specifically with SHA-256 and represented as a hexadecimal string). @@ -311,12 +310,11 @@ const encodedNonce = encoder.encode(nonce) // Encode the nonce const hash = await crypto.subtle.digest('SHA-256', encodedNonce) // Hash it with SHA-256 const bytes = new Uint8Array(hash) const hashedNonce = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string - .join('') + .map((b) => b.toString(16).padStart(2, '0')) // Convert the hash to a hexadecimal string + .join('') // Use 'hashedNonce' when making the authentication request to Google // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method - ``` ### Configuration [#configuration-personalized-sign-in-button] From 4d29ba037c1f8c761ab69bde70d82614c91024c7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 10:55:29 +0700 Subject: [PATCH 053/117] Refactor send support ticket to RQ and update SupportForm to use it --- .../interfaces/Support/SupportForm.tsx | 41 ++++++---- studio/data/feedback/support-ticket-send.ts | 81 +++++++++++++++++++ 2 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 studio/data/feedback/support-ticket-send.ts diff --git a/studio/components/interfaces/Support/SupportForm.tsx b/studio/components/interfaces/Support/SupportForm.tsx index f77f92b2efc..bf2bddef769 100644 --- a/studio/components/interfaces/Support/SupportForm.tsx +++ b/studio/components/interfaces/Support/SupportForm.tsx @@ -1,3 +1,4 @@ +import { useParams } from 'common' import { CLIENT_LIBRARIES } from 'common/constants' import { observer } from 'mobx-react-lite' import Link from 'next/link' @@ -16,17 +17,17 @@ import { Listbox, } from 'ui' -import { useParams } from 'common' import Divider from 'components/ui/Divider' import InformationBox from 'components/ui/InformationBox' import MultiSelect from 'components/ui/MultiSelect' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useFlag, useStore } from 'hooks' import useLatest from 'hooks/misc/useLatest' -import { get, isResponseOk, post } from 'lib/common/fetch' +import { get } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { detectBrowser } from 'lib/helpers' import { useProfile } from 'lib/profile' @@ -48,6 +49,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { const uploadButtonRef = useRef() const enableFreeSupport = useFlag('enableFreeSupport') + const [isSubmitting, setIsSubmitting] = useState(false) const [uploadedFiles, setUploadedFiles] = useState([]) const [uploadedDataUrls, setUploadedDataUrls] = useState([]) const [selectedServices, setSelectedServices] = useState([]) @@ -68,6 +70,21 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { isSuccess: isSuccessProjects, } = useProjectsQuery() + const { mutate: submitSupportTicket } = useSendSupportTicketMutation({ + onSuccess: (res, variables) => { + ui.setNotification({ category: 'success', message: 'Support request sent. Thank you!' }) + setSentCategory(variables.category) + }, + onError: (error) => { + ui.setNotification({ + error, + category: 'error', + message: `Failed to submit support ticket: ${error.message}`, + }) + setIsSubmitting(false) + }, + }) + const projectDefaults: Partial[] = [{ ref: 'no-project', name: 'No specific project' }] const projects = [...(allProjects ?? []), ...projectDefaults] @@ -160,8 +177,8 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { return errors } - const onSubmit = async (values: any, { setSubmitting }: any) => { - setSubmitting(true) + const onSubmit = async (values: any) => { + setIsSubmitting(true) const attachments = uploadedFiles.length > 0 ? await uploadAttachments(values.projectRef, uploadedFiles) : [] const selectedLibrary = CLIENT_LIBRARIES.find((library) => library.language === values.library) @@ -183,6 +200,8 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { if (values.projectRef !== 'no-project') { const URL = `${API_URL}/auth/${values.projectRef}/config` + // [Joshen] Will refactor another time once + // https://github.com/supabase/supabase/pull/16227 goes in const authConfig = await get(URL) if (!authConfig.error) { payload.siteUrl = authConfig.SITE_URL @@ -190,22 +209,12 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => { } } - const response = await post(`${API_URL}/feedback/send`, payload) - if (!isResponseOk(response)) { - ui.setNotification({ - category: 'error', - message: `Failed to submit support ticket: ${response.error.message}`, - }) - setSubmitting(false) - } else { - ui.setNotification({ category: 'success', message: 'Support request sent. Thank you!' }) - setSentCategory(values.category) - } + submitSupportTicket(payload) } return ( - {({ isSubmitting, resetForm, values }: any) => { + {({ resetForm, values }: any) => { const selectedCategory = CATEGORY_OPTIONS.find( (category) => category.value === values.category ) diff --git a/studio/data/feedback/support-ticket-send.ts b/studio/data/feedback/support-ticket-send.ts new file mode 100644 index 00000000000..ce9118afded --- /dev/null +++ b/studio/data/feedback/support-ticket-send.ts @@ -0,0 +1,81 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type sendSupportTicketVariables = { + subject: string + message: string + category: string + severity: string + projectRef?: string + organizationSlug?: string + library?: string + affectedServices?: string + browserInformation?: string + allowSupportAccess: boolean +} + +export async function sendSupportTicket({ + subject, + message, + category, + severity, + projectRef, + organizationSlug, + library, + affectedServices, + browserInformation, + allowSupportAccess, +}: sendSupportTicketVariables) { + const { data, error } = await post('/platform/feedback/send', { + body: { + subject, + message, + category, + severity, + projectRef, + organizationSlug, + library, + verified: true, + tags: ['dashboard-support-form'], + siteUrl: '', + additionalRedirectUrls: '', + affectedServices, + browserInformation, + allowSupportAccess, + }, + }) + + if (error) throw error + return data +} + +type sendSupportTicketData = Awaited> + +export const useSendSupportTicketMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => sendSupportTicket(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to submit support ticket: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} From d27703164495c939ae42cfd7b0d7ef2b79b87457 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 11:03:15 +0700 Subject: [PATCH 054/117] Refactor sign up to RQ mutation and update SignUpForm to use it --- .../interfaces/SignIn/SignUpForm.tsx | 65 +++++++++---------- studio/data/misc/signup-mutation.ts | 44 +++++++++++++ 2 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 studio/data/misc/signup-mutation.ts diff --git a/studio/components/interfaces/SignIn/SignUpForm.tsx b/studio/components/interfaces/SignIn/SignUpForm.tsx index 37c1afaaf57..d08a64ed988 100644 --- a/studio/components/interfaces/SignIn/SignUpForm.tsx +++ b/studio/components/interfaces/SignIn/SignUpForm.tsx @@ -1,13 +1,13 @@ -import * as yup from 'yup' -import { useRef, useState } from 'react' import HCaptcha from '@hcaptcha/react-hcaptcha' +import { useRef, useState } from 'react' import { Alert, Button, Form, IconEye, IconEyeOff, Input } from 'ui' +import * as yup from 'yup' +import { useSignUpMutation } from 'data/misc/signup-mutation' import { useStore } from 'hooks' -import { isResponseOk, post } from 'lib/common/fetch' -import { API_URL, BASE_PATH } from 'lib/constants' -import { passwordSchema } from 'lib/schemas' +import { BASE_PATH } from 'lib/constants' import { resetSignInClicks } from 'lib/local-storage' +import { passwordSchema } from 'lib/schemas' import PasswordConditionsHelper from './PasswordConditionsHelper' const signUpSchema = passwordSchema.shape({ @@ -22,12 +22,25 @@ const SignUpForm = () => { const [passwordHidden, setPasswordHidden] = useState(true) const [captchaToken, setCaptchaToken] = useState(null) - const onSignUp = async ({ email, password }: { email: string; password: string }) => { - const toastId = ui.setNotification({ - category: 'loading', - message: `Signing up...`, - }) + const { mutate: signup, isLoading: isSigningUp } = useSignUpMutation({ + onSuccess: () => { + ui.setNotification({ + category: 'success', + message: `Signed up successfully!`, + }) + setIsSubmitted(true) + }, + onError: (error) => { + setCaptchaToken(null) + captchaRef.current?.resetCaptcha() + ui.setNotification({ + category: 'error', + message: `Failed to sign up: ${error.message}`, + }) + }, + }) + const onSignUp = async ({ email, password }: { email: string; password: string }) => { let token = captchaToken if (!token) { const captchaResponse = await captchaRef.current?.execute({ async: true }) @@ -35,8 +48,7 @@ const SignUpForm = () => { } resetSignInClicks() - - const response = await post(`${API_URL}/signup`, { + signup({ email, password, hcaptchaToken: token ?? null, @@ -46,25 +58,6 @@ const SignUpForm = () => { : process.env.NEXT_PUBLIC_SITE_URL }${BASE_PATH}/sign-in`, }) - - if (isResponseOk(response)) { - ui.setNotification({ - id: toastId, - category: 'success', - message: `Signed up successfully!`, - }) - - setIsSubmitted(true) - } else { - setCaptchaToken(null) - captchaRef.current?.resetCaptcha() - - ui.setNotification({ - id: toastId, - category: 'error', - message: `Failed to sign up: ${response.error.message}`, - }) - } } return ( @@ -89,7 +82,7 @@ const SignUpForm = () => { validationSchema={signUpSchema} onSubmit={onSignUp} > - {({ isSubmitting, values }: { isSubmitting: boolean; values: any }) => { + {({ values }: { values: any }) => { return (
{ type="email" label="Email" placeholder="you@example.com" - disabled={isSubmitting} + disabled={isSigningUp} autoComplete="email" /> @@ -108,7 +101,7 @@ const SignUpForm = () => { type={passwordHidden ? 'password' : 'text'} label="Password" placeholder="••••••••" - disabled={isSubmitting} + disabled={isSigningUp} autoComplete="new-password" onFocus={() => setShowConditions(true)} actions={ @@ -148,8 +141,8 @@ const SignUpForm = () => { form="signUp-form" htmlType="submit" size="large" - disabled={values.password.length === 0 || isSubmitting} - loading={isSubmitting} + disabled={values.password.length === 0 || isSigningUp} + loading={isSigningUp} > Sign Up diff --git a/studio/data/misc/signup-mutation.ts b/studio/data/misc/signup-mutation.ts new file mode 100644 index 00000000000..d056c3cbc21 --- /dev/null +++ b/studio/data/misc/signup-mutation.ts @@ -0,0 +1,44 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { post } from 'data/fetchers' +import { ResponseError } from 'types' + +export type SignUpVariables = { + email: string + password: string + hcaptchaToken: string | null + redirectTo: string +} + +export async function signup({ email, password, hcaptchaToken, redirectTo }: SignUpVariables) { + const { data, error } = await post('/platform/signup', { + // @ts-ignore + body: { email, password, hcaptchaToken, redirectTo }, + }) + + if (error) throw error + return data +} + +type SignUpData = Awaited> + +export const useSignUpMutation = ({ + onSuccess, + onError, + ...options +}: Omit, 'mutationFn'> = {}) => { + return useMutation((vars) => signup(vars), { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to sign up: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} From 164cf087c273e91e611733a1fb7fba3a4eaf6f85 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 11:30:26 +0700 Subject: [PATCH 055/117] Refactor delete organization to RQ and update DeleteOrganizationButton to use it --- .../DeleteOrganizationButton.tsx | 37 +++++++------- .../organization-delete-mutation.ts | 49 +++++++++++++++++++ 2 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 studio/data/organizations/organization-delete-mutation.ts diff --git a/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx b/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx index cc5c66fec0b..c1804f245f3 100644 --- a/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx +++ b/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx @@ -1,17 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/router' import { useState } from 'react' import { Button, Form, Input, Modal } from 'ui' -import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' +import { useOrganizationDeleteMutation } from 'data/organizations/organization-delete-mutation' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' -import { delete_, isResponseOk } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' const DeleteOrganizationButton = () => { const router = useRouter() - const queryClient = useQueryClient() const { ui } = useStore() const selectedOrganization = useSelectedOrganization() @@ -21,6 +17,7 @@ const DeleteOrganizationButton = () => { const [value, setValue] = useState('') const canDeleteOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { mutateAsync: deleteOrganization, isLoading: isDeleting } = useOrganizationDeleteMutation() const onValidate = (values: any) => { const errors: any = {} @@ -33,30 +30,23 @@ const DeleteOrganizationButton = () => { return errors } - const onConfirmDelete = async (values: any, { setSubmitting }: any) => { + const onConfirmDelete = async (values: any) => { if (!canDeleteOrganization) { return ui.setNotification({ category: 'error', message: 'You do not have the required permissions to delete this organization', }) } + if (!orgSlug) return console.error('Org slug is required') - setSubmitting(true) - const response = await delete_(`${API_URL}/organizations/${orgSlug}`) - if (!isResponseOk(response)) { - ui.setNotification({ - category: 'error', - message: `Failed to delete organization: ${response.error.message}`, - }) - setSubmitting(false) - } else { - invalidateOrganizationsQuery(queryClient) - setSubmitting(false) - router.push('/') + try { + await deleteOrganization({ slug: orgSlug }) + } finally { ui.setNotification({ category: 'success', message: `Successfully deleted ${orgName}`, }) + router.push('/') } } @@ -86,7 +76,7 @@ const DeleteOrganizationButton = () => { onSubmit={onConfirmDelete} validate={onValidate} > - {({ isSubmitting }: { isSubmitting: boolean }) => ( + {() => (

@@ -112,7 +102,14 @@ const DeleteOrganizationButton = () => { - diff --git a/studio/data/organizations/organization-delete-mutation.ts b/studio/data/organizations/organization-delete-mutation.ts new file mode 100644 index 00000000000..3b0dcfa7e44 --- /dev/null +++ b/studio/data/organizations/organization-delete-mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + +import { del } from 'data/fetchers' +import { ResponseError } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationDeleteVariables = { + slug: string +} + +export async function deleteOrganization({ slug }: OrganizationDeleteVariables) { + const { data, error } = await del('/platform/organizations/{slug}', { + params: { path: { slug } }, + }) + if (error) throw error + return data +} + +type OrganizationDeleteData = Awaited> + +export const useOrganizationDeleteMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteOrganization(vars), + { + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries(organizationKeys.list()) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to delete organization: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} From ebdbb1a6aa68905f62a89cd5b6f79fb56c489e6e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 11:34:22 +0700 Subject: [PATCH 056/117] Update TeamSettings to use org member delete mutation --- .../Organization/TeamSettings/TeamSettings.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index e1a1ddf0d8a..fb50d911e68 100644 --- a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -21,6 +21,7 @@ import { ScaffoldActionsContainer, ScaffoldSectionContent, } from 'components/layouts/Scaffold' +import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation' const TeamSettings = () => { const { ui } = useStore() @@ -49,6 +50,8 @@ const TeamSettings = () => { const canAddMembers = rolesAddable.length > 0 const canLeave = !isOwner || (isOwner && hasMultipleOwners(members, roles)) + const { mutateAsync: deleteMember } = useOrganizationMemberDeleteMutation() + const leaveTeam = async () => { setIsLeaving(true) try { @@ -56,13 +59,11 @@ const TeamSettings = () => { title: 'Are you sure?', message: 'Are you sure you want to leave this organization? This is permanent.', onAsyncConfirm: async () => { - const response = await delete_( - `${API_URL}/organizations/${slug}/members/${profile!.gotrue_id}` - ) - if (!isResponseOk(response)) { - throw response.error - } else { + try { + if (!slug) return console.error('Org slug is required') + await deleteMember({ slug, gotrueId: profile!.gotrue_id }) window?.location.replace('/') // Force reload to clear Store + } finally { } }, }) From bf583031a22957367f5b6deec475e884b72ee3f9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 12 Jul 2023 20:54:45 -0500 Subject: [PATCH 057/117] link testing policies on db testing page --- apps/docs/pages/guides/database/testing.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/database/testing.mdx b/apps/docs/pages/guides/database/testing.mdx index 1ff2d912d66..7db3ed93dd6 100644 --- a/apps/docs/pages/guides/database/testing.mdx +++ b/apps/docs/pages/guides/database/testing.mdx @@ -63,8 +63,9 @@ Files=1, Tests=1, 1 wallclock secs ( 0.01 usr 0.00 sys + 0.04 cusr 0.02 csys Result: PASS ``` -## More resource +## More resources +- [Testing RLS policies](/docs/guides/auth/row-level-security#testing-policies) - [pgTAP extension](/docs/guides/database/extensions/pgtap) - Official [pgTAP documentation](https://pgtap.org/) From 6b4010b9646c84ae11ba538c32ef4ed3611243ff Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 14:41:11 +0700 Subject: [PATCH 058/117] Add stale times to permissions and organizations queries --- studio/components/interfaces/Home/ProjectList/ProjectList.tsx | 1 - .../to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js | 1 + studio/data/organizations/organizations-query.ts | 2 +- studio/data/permissions/permissions-query.ts | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index d24aa81b1d0..1868b9c2689 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -18,7 +18,6 @@ import { makeRandomString } from 'lib/helpers' import { Organization, Project, ResponseError } from 'types' import ProjectCard from './ProjectCard' import ShimmeringCard from './ShimmeringCard' -import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only' export interface ProjectListProps { rewriteHref?: (projectRef: string) => string diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js index e00d6238ec8..77156adfc84 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js @@ -231,4 +231,5 @@ const StorageExplorer = observer(({ bucket }) => { ) }) +StorageExplorer.displayName = 'StorageExplorer' export default StorageExplorer diff --git a/studio/data/organizations/organizations-query.ts b/studio/data/organizations/organizations-query.ts index 4bf70566dd8..c2c96520364 100644 --- a/studio/data/organizations/organizations-query.ts +++ b/studio/data/organizations/organizations-query.ts @@ -27,7 +27,7 @@ export const useOrganizationsQuery = ({ useQuery( organizationKeys.list(), ({ signal }) => getOrganizations(signal), - { enabled: enabled, ...options } + { enabled: enabled, ...options, staleTime: 30 * 60 * 1000 } ) export function prefetchOrganizations(client: QueryClient) { diff --git a/studio/data/permissions/permissions-query.ts b/studio/data/permissions/permissions-query.ts index 0fea45d1876..b96a57c6698 100644 --- a/studio/data/permissions/permissions-query.ts +++ b/studio/data/permissions/permissions-query.ts @@ -28,6 +28,7 @@ export const usePermissionsQuery = ( { ...options, enabled: IS_PLATFORM && options.enabled, + staleTime: 30 * 60 * 1000, } ) From e1b1f8d0b1b9606595020bdce882b6d3445649cb Mon Sep 17 00:00:00 2001 From: AbhijeetBiswal Date: Thu, 17 Aug 2023 13:47:41 +0530 Subject: [PATCH 059/117] Fix: Documentation link for Google auth provider fix #16437 --- .../schema/AuthProviders/AuthProvidersFormValidation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index 371096dda52..1b15ca18122 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -884,7 +884,7 @@ const EXTERNAL_PROVIDER_GOOGLE = { iconKey: 'google-icon', requiresRedirect: true, helper: `Register this callback URL when using Sign-in with Google on the web using OAuth. - [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`, + [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-google#configure-your-services-id)`, }, } From fd47ff120a287b9bcaeb887bf9c9b731a3a21473 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 16:37:48 +0700 Subject: [PATCH 060/117] Implement read only UI for indexes --- .../interfaces/Database/Indexes/Indexes.tsx | 210 ++++++++++++++++++ .../Database/Migrations/Migrations.tsx | 4 +- .../DatabaseLayout/DatabaseMenu.utils.ts | 6 + studio/data/database/indexes-query.ts | 52 +++++ studio/data/database/schemas-query.ts | 3 +- studio/data/sql/execute-sql-query.ts | 3 +- .../pages/project/[ref]/database/indexes.tsx | 22 ++ .../project/[ref]/database/migrations.tsx | 9 +- 8 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 studio/components/interfaces/Database/Indexes/Indexes.tsx create mode 100644 studio/data/database/indexes-query.ts create mode 100644 studio/pages/project/[ref]/database/indexes.tsx diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx new file mode 100644 index 00000000000..77c03feea64 --- /dev/null +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -0,0 +1,210 @@ +import { observer } from 'mobx-react-lite' +import Link from 'next/link' +import { useState } from 'react' +import { + Button, + IconAlertCircle, + IconExternalLink, + IconSearch, + Input, + Listbox, + SidePanel, +} from 'ui' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import AlertError from 'components/ui/AlertError' +import { DatabaseIndex, useIndexesQuery } from 'data/database/indexes-query' +import { useSchemasQuery } from 'data/database/schemas-query' +import Table from 'components/to-be-cleaned/Table' +import CodeEditor from 'components/ui/CodeEditor' + +const Indexes = () => { + const [search, setSearch] = useState('') + const [selectedSchema, setSelectedSchema] = useState('public') + const [selectedIndex, setSelectedIndex] = useState() + + const { project } = useProjectContext() + const { + data: allIndexes, + error: indexesError, + isLoading: isLoadingIndexes, + isSuccess: isSuccessIndexes, + isError: isErrorIndexes, + } = useIndexesQuery({ + schema: selectedSchema, + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { + data: schemas, + isLoading: isLoadingSchemas, + isSuccess: isSuccessSchemas, + isError: isErrorSchemas, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const sortedIndexes = (allIndexes?.result ?? []).sort( + (a, b) => a.table.localeCompare(b.table) || a.name.localeCompare(b.name) + ) + const indexes = + search.length > 0 + ? sortedIndexes.filter((index) => index.name.includes(search) || index.table.includes(search)) + : sortedIndexes + + return ( + <> +

+ + +
+ {isLoadingSchemas && } + {isErrorSchemas && ( +
+ +

Failed to load schemas

+
+ )} + {isSuccessSchemas && ( + + {(schemas?.result ?? []).map((schema) => ( + schema} + > + {schema.name} + + ))} + + )} + setSearch(e.target.value)} + placeholder="Search for an index" + icon={} + /> +
+ + {isLoadingIndexes && } + + {isErrorIndexes && ( + + )} + + {isSuccessIndexes && ( + Schema, + Table, + Name, + , + ]} + body={ + <> + {sortedIndexes.length === 0 && search.length === 0 && ( + + +

No indexes created yet

+

+ There are no indexes found in the schema "{selectedSchema}" +

+
+
+ )} + {sortedIndexes.length === 0 && search.length > 0 && ( + + +

No results found

+

+ Your search for "{search}" did not return any results +

+
+
+ )} + {indexes.length > 0 && + indexes.map((index) => ( + + +

{index.schema}

+
+ +

{index.table}

+
+ +

{index.name}

+
+ +
+ +
+
+
+ ))} + + } + /> + )} + + + + Index: + {selectedIndex?.name} + + } + onCancel={() => setSelectedIndex(undefined)} + > +
+
+ +
+
+
+ + ) +} + +export default observer(Indexes) diff --git a/studio/components/interfaces/Database/Migrations/Migrations.tsx b/studio/components/interfaces/Database/Migrations/Migrations.tsx index bedf0b8a4cf..5ee1fc8b5ad 100644 --- a/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -11,7 +11,7 @@ import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations- import MigrationsEmptyState from './MigrationsEmptyState' import dayjs from 'dayjs' -const Wrappers = () => { +const Migrations = () => { const [search, setSearch] = useState('') const [selectedMigration, setSelectedMigration] = useState() @@ -183,4 +183,4 @@ const Wrappers = () => { ) } -export default observer(Wrappers) +export default observer(Migrations) diff --git a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts index 44950c66181..641f7804e15 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts +++ b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts @@ -90,6 +90,12 @@ export const generateDatabaseMenu = ( url: `/project/${ref}/database/migrations`, items: [], }, + { + name: 'Indexes', + key: 'indexes', + url: `/project/${ref}/database/indexes`, + items: [], + }, ], }, ] diff --git a/studio/data/database/indexes-query.ts b/studio/data/database/indexes-query.ts new file mode 100644 index 00000000000..852ceeff634 --- /dev/null +++ b/studio/data/database/indexes-query.ts @@ -0,0 +1,52 @@ +import { UseQueryOptions } from '@tanstack/react-query' +import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { ResponseError } from 'types' + +// [Joshen] Future refactor to move to pg-meta once available + +export type DatabaseIndex = { + name: string + schema: string + table: string + definition: string +} + +export const getIndexesQuery = ({ schema }: { schema: string }) => { + const sql = /* SQL */ ` +SELECT schemaname as "schema", + tablename as "table", + indexname as "name", + indexdef as "definition" +FROM pg_indexes +WHERE schemaname = '${schema}'; +`.trim() + + return sql +} + +type getIndexesDefinition = { + schema: string +} + +export type IndexesVariables = getIndexesDefinition & { + projectRef?: string + connectionString?: string +} + +export type IndexesData = { result: DatabaseIndex[] } +export type IndexesError = ResponseError + +export const useIndexesQuery = ( + { schema, projectRef, connectionString }: IndexesVariables, + options: UseQueryOptions = {} +) => { + return useExecuteSqlQuery( + { + projectRef, + connectionString, + sql: getIndexesQuery({ schema }), + queryKey: ['indexes', schema], + }, + options + ) +} diff --git a/studio/data/database/schemas-query.ts b/studio/data/database/schemas-query.ts index 964b1228f54..8be93927229 100644 --- a/studio/data/database/schemas-query.ts +++ b/studio/data/database/schemas-query.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { UseQueryOptions } from '@tanstack/react-query' import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { ResponseError } from 'types' export type Schema = { name: string @@ -27,7 +28,7 @@ export type SchemasVariables = { } export type SchemasData = { result: Schema[] } -export type SchemasError = unknown +export type SchemasError = ResponseError export const useSchemasQuery = ( { projectRef, connectionString }: SchemasVariables, diff --git a/studio/data/sql/execute-sql-query.ts b/studio/data/sql/execute-sql-query.ts index 76ef0f665aa..bff0df93183 100644 --- a/studio/data/sql/execute-sql-query.ts +++ b/studio/data/sql/execute-sql-query.ts @@ -11,6 +11,7 @@ import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { sqlKeys } from './keys' import { noop } from 'lodash' +import { ResponseError } from 'types' export type Error = { code: number; message: string; requestId: string } @@ -65,7 +66,7 @@ export async function executeSql( } export type ExecuteSqlData = Awaited> -export type ExecuteSqlError = unknown +export type ExecuteSqlError = ResponseError export const useExecuteSqlQuery = ( { projectRef, connectionString, sql, queryKey, handleError }: ExecuteSqlVariables, diff --git a/studio/pages/project/[ref]/database/indexes.tsx b/studio/pages/project/[ref]/database/indexes.tsx new file mode 100644 index 00000000000..27514ae048d --- /dev/null +++ b/studio/pages/project/[ref]/database/indexes.tsx @@ -0,0 +1,22 @@ +import clsx from 'clsx' + +import Indexes from 'components/interfaces/Database/Indexes/Indexes' +import { DatabaseLayout } from 'components/layouts' +import { NextPageWithLayout } from 'types' + +const IndexesPage: NextPageWithLayout = () => { + return ( +
+ +
+ ) +} + +IndexesPage.getLayout = (page) => {page} + +export default IndexesPage diff --git a/studio/pages/project/[ref]/database/migrations.tsx b/studio/pages/project/[ref]/database/migrations.tsx index 16a3cd2500e..a7a19f82a4c 100644 --- a/studio/pages/project/[ref]/database/migrations.tsx +++ b/studio/pages/project/[ref]/database/migrations.tsx @@ -1,17 +1,10 @@ import clsx from 'clsx' + import Migrations from 'components/interfaces/Database/Migrations/Migrations' import { DatabaseLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { useMigrationsQuery } from 'data/database/migrations-query' import { NextPageWithLayout } from 'types' const MigrationsPage: NextPageWithLayout = () => { - const { project } = useProjectContext() - const { data, isLoading, isSuccess, isError } = useMigrationsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - return (
Date: Thu, 17 Aug 2023 16:42:15 +0700 Subject: [PATCH 061/117] Fix type --- studio/components/ui/AlertError.tsx | 2 +- studio/data/sql/execute-sql-query.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/studio/components/ui/AlertError.tsx b/studio/components/ui/AlertError.tsx index dfd6b976a9a..c45283dd90e 100644 --- a/studio/components/ui/AlertError.tsx +++ b/studio/components/ui/AlertError.tsx @@ -12,7 +12,7 @@ import { export interface AlertErrorProps { ref?: string subject?: string - error?: ResponseError | null + error?: ResponseError | null | unknown className?: string } diff --git a/studio/data/sql/execute-sql-query.ts b/studio/data/sql/execute-sql-query.ts index bff0df93183..7d6790ba814 100644 --- a/studio/data/sql/execute-sql-query.ts +++ b/studio/data/sql/execute-sql-query.ts @@ -10,8 +10,6 @@ import { post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { sqlKeys } from './keys' -import { noop } from 'lodash' -import { ResponseError } from 'types' export type Error = { code: number; message: string; requestId: string } @@ -66,7 +64,7 @@ export async function executeSql( } export type ExecuteSqlData = Awaited> -export type ExecuteSqlError = ResponseError +export type ExecuteSqlError = unknown export const useExecuteSqlQuery = ( { projectRef, connectionString, sql, queryKey, handleError }: ExecuteSqlVariables, From df07b07670ded60c00e7833d0eed870c63dabdcf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 16:46:38 +0700 Subject: [PATCH 062/117] Fix type --- studio/components/interfaces/Database/Indexes/Indexes.tsx | 2 +- studio/components/ui/AlertError.tsx | 2 +- studio/data/database/indexes-query.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx index 77c03feea64..304f74b21f2 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -122,7 +122,7 @@ const Indexes = () => { {isLoadingIndexes && } {isErrorIndexes && ( - + )} {isSuccessIndexes && ( diff --git a/studio/components/ui/AlertError.tsx b/studio/components/ui/AlertError.tsx index c45283dd90e..dfd6b976a9a 100644 --- a/studio/components/ui/AlertError.tsx +++ b/studio/components/ui/AlertError.tsx @@ -12,7 +12,7 @@ import { export interface AlertErrorProps { ref?: string subject?: string - error?: ResponseError | null | unknown + error?: ResponseError | null className?: string } diff --git a/studio/data/database/indexes-query.ts b/studio/data/database/indexes-query.ts index 852ceeff634..a1ad90d860a 100644 --- a/studio/data/database/indexes-query.ts +++ b/studio/data/database/indexes-query.ts @@ -34,7 +34,7 @@ export type IndexesVariables = getIndexesDefinition & { } export type IndexesData = { result: DatabaseIndex[] } -export type IndexesError = ResponseError +export type IndexesError = unknown export const useIndexesQuery = ( { schema, projectRef, connectionString }: IndexesVariables, From 4927630fb88a7bb1902542f78ef8182a20997e24 Mon Sep 17 00:00:00 2001 From: Ant Wilson Date: Thu, 17 Aug 2023 12:08:49 +0200 Subject: [PATCH 063/117] Update apps/www/_customers/berriai.mdx --- apps/www/_customers/berriai.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index 57e2805dd5e..fd02b657b81 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -2,7 +2,7 @@ name: Berri AI title: Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector # Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': -# meta_title: Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector +meta_title: Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector description: Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase. # Use meta_description to add a custom meta description. Otherwise it defaults to {description}: meta_description: Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase. From 18e3c3fa7c5d0b3c68338a1d627881c6781aebbf Mon Sep 17 00:00:00 2001 From: Ant Wilson Date: Thu, 17 Aug 2023 12:09:39 +0200 Subject: [PATCH 064/117] Update apps/www/_customers/happyteams.mdx --- apps/www/_customers/happyteams.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_customers/happyteams.mdx b/apps/www/_customers/happyteams.mdx index a1846ccf47f..4ccd74a9f62 100644 --- a/apps/www/_customers/happyteams.mdx +++ b/apps/www/_customers/happyteams.mdx @@ -2,7 +2,7 @@ name: HappyTeams title: HappyTeams unlocks better performance and reduces cost with Supabase. # Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': -# meta_title: HappyTeams unlocks better performance and reduces cost with Supabase. +meta_title: HappyTeams unlocks better performance and reduces cost with Supabase. description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. # Use meta_description to add a custom meta description. Otherwise it defaults to {description}: meta_description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. From a8b0c4c3d5c6ae9f2ca36466d691c3b467b01a4e Mon Sep 17 00:00:00 2001 From: Ant Wilson Date: Thu, 17 Aug 2023 13:26:37 +0200 Subject: [PATCH 065/117] fix: fixes naming on lw keys --- apps/www/pages/launch-week/tickets/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/www/pages/launch-week/tickets/index.tsx b/apps/www/pages/launch-week/tickets/index.tsx index 69c57824d72..579634be9da 100644 --- a/apps/www/pages/launch-week/tickets/index.tsx +++ b/apps/www/pages/launch-week/tickets/index.tsx @@ -23,9 +23,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ) const generateOgs = async (users: UserData[]) => { From 477652dfc4e974dca19df505fe1c2596220d9127 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 17 Aug 2023 18:31:30 +0700 Subject: [PATCH 066/117] Fix type --- studio/data/database/schemas-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/data/database/schemas-query.ts b/studio/data/database/schemas-query.ts index 8be93927229..896dd2df016 100644 --- a/studio/data/database/schemas-query.ts +++ b/studio/data/database/schemas-query.ts @@ -28,7 +28,7 @@ export type SchemasVariables = { } export type SchemasData = { result: Schema[] } -export type SchemasError = ResponseError +export type SchemasError = unknown export const useSchemasQuery = ( { projectRef, connectionString }: SchemasVariables, From 93b947e83f42c989ebb69727b0859c932512b8ad Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Thu, 17 Aug 2023 19:37:12 +0800 Subject: [PATCH 067/117] fix: more naming of lw keys --- apps/www/pages/launch-week/7/index.tsx | 4 +--- apps/www/pages/launch-week/7/tickets/[username].tsx | 4 +--- apps/www/pages/launch-week/7/tickets/index.tsx | 4 +--- apps/www/pages/launch-week/index.tsx | 4 +--- apps/www/pages/launch-week/tickets/[username].tsx | 4 +--- apps/www/pages/launch-week/tickets/index.tsx | 2 +- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/apps/www/pages/launch-week/7/index.tsx b/apps/www/pages/launch-week/7/index.tsx index 5043d62d84d..9900b746371 100644 --- a/apps/www/pages/launch-week/7/index.tsx +++ b/apps/www/pages/launch-week/7/index.tsx @@ -29,9 +29,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', // ANON KEY - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) export default function TicketHome({ users }: Props) { const { isDarkMode, toggleTheme } = useTheme() diff --git a/apps/www/pages/launch-week/7/tickets/[username].tsx b/apps/www/pages/launch-week/7/tickets/[username].tsx index 7f74869f8b9..be288f21893 100644 --- a/apps/www/pages/launch-week/7/tickets/[username].tsx +++ b/apps/www/pages/launch-week/7/tickets/[username].tsx @@ -24,9 +24,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - '' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) export default function UsernamePage({ user, users, ogImageUrl }: Props) { diff --git a/apps/www/pages/launch-week/7/tickets/index.tsx b/apps/www/pages/launch-week/7/tickets/index.tsx index 0027a45b7c6..37cfc0c287b 100644 --- a/apps/www/pages/launch-week/7/tickets/index.tsx +++ b/apps/www/pages/launch-week/7/tickets/index.tsx @@ -22,9 +22,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - '' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const generateOgs = async (users: UserData[]) => { diff --git a/apps/www/pages/launch-week/index.tsx b/apps/www/pages/launch-week/index.tsx index 2db9d1e58ea..f679a83999f 100644 --- a/apps/www/pages/launch-week/index.tsx +++ b/apps/www/pages/launch-week/index.tsx @@ -39,9 +39,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', // ANON KEY - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) export default function TicketHome({ users, meetups }: Props) { diff --git a/apps/www/pages/launch-week/tickets/[username].tsx b/apps/www/pages/launch-week/tickets/[username].tsx index 0aa62979f24..28b957b7659 100644 --- a/apps/www/pages/launch-week/tickets/[username].tsx +++ b/apps/www/pages/launch-week/tickets/[username].tsx @@ -28,9 +28,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', // ANON KEY - process.env.SUPABASE_SERVICE_ROLE_SECRET ?? - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) export default function UsernamePage({ user, users, ogImageUrl }: Props) { diff --git a/apps/www/pages/launch-week/tickets/index.tsx b/apps/www/pages/launch-week/tickets/index.tsx index 579634be9da..1dbec06cf36 100644 --- a/apps/www/pages/launch-week/tickets/index.tsx +++ b/apps/www/pages/launch-week/tickets/index.tsx @@ -23,7 +23,7 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const generateOgs = async (users: UserData[]) => { From da4a004dd66598c67ab9d6eed7ff5ca0af32c5d4 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 17 Aug 2023 10:17:10 -0230 Subject: [PATCH 068/117] Add newly rolled anon key, fix up env var references --- apps/www/.env | 2 +- apps/www/components/Partners/TileGrid.tsx | 14 +++++++------- apps/www/lib/supabase.ts | 9 ++++----- apps/www/pages/partners/integrations/index.tsx | 4 ++-- .../supabase/functions/lw7-ticket-og/handler.tsx | 2 +- apps/www/supabase/functions/lw8-ticket/handler.tsx | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/www/.env b/apps/www/.env index 44e4b28a40c..6db5b58d782 100644 --- a/apps/www/.env +++ b/apps/www/.env @@ -12,4 +12,4 @@ NEXT_PUBLIC_REFERENCE_DOCS_URL="https://localhost:3010" NEXT_PUBLIC_LW_STARTED=true NEXT_PUBLIC_SITE_ORIGIN=http://localhost:3000 NEXT_PUBLIC_SUPABASE_URL=https://obuldanrptloktxcffvn.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I \ No newline at end of file +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTIyNjkzMjYsImV4cCI6MjAwNzg0NTMyNn0.1S6qpBbHtEmGuMsIx5UOhRiFd4YbVv-yLTrLk6tVGmM \ No newline at end of file diff --git a/apps/www/components/Partners/TileGrid.tsx b/apps/www/components/Partners/TileGrid.tsx index cca3d822f3c..b7c7d1e99c0 100644 --- a/apps/www/components/Partners/TileGrid.tsx +++ b/apps/www/components/Partners/TileGrid.tsx @@ -26,16 +26,16 @@ export default function TileGrid({ >

Featured

- {featuredPartners.map((p) => ( + {featuredPartners?.map((p) => (
@@ -79,9 +79,9 @@ export default function TileGrid({ className=" bg-scale-100 dark:bg-scale-300 hover:bg-scale-200 hover:dark:bg-scale-400 - group flex h-full w-full flex-col rounded-xl border px-6 - py-6 shadow - transition-all + group flex h-full w-full flex-col rounded-xl border px-6 + py-6 shadow + transition-all hover:shadow-lg" >
diff --git a/apps/www/lib/supabase.ts b/apps/www/lib/supabase.ts index 29d235578b7..9eb2cf3fb03 100644 --- a/apps/www/lib/supabase.ts +++ b/apps/www/lib/supabase.ts @@ -1,11 +1,10 @@ import { createClient } from '@supabase/supabase-js' import { Database } from './database.types' -const SUPABASE_URL = 'https://obuldanrptloktxcffvn.supabase.co' -const SUPABASE_ANON_KEY = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' - -const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY) +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! +) export type SupabaseClient = typeof supabase diff --git a/apps/www/pages/partners/integrations/index.tsx b/apps/www/pages/partners/integrations/index.tsx index 5e57b6bd0e0..d8e667354ff 100644 --- a/apps/www/pages/partners/integrations/index.tsx +++ b/apps/www/pages/partners/integrations/index.tsx @@ -37,7 +37,7 @@ function IntegrationPartnersPage(props: Props) { const { partners: initialPartners } = props const [partners, setPartners] = useState(initialPartners) - const allCategories = Array.from(new Set(initialPartners.map((p) => p.category))) + const allCategories = Array.from(new Set(initialPartners?.map((p) => p.category))) const router = useRouter() @@ -213,7 +213,7 @@ function IntegrationPartnersPage(props: Props) {
{/* Partner Tiles */}
- {partners.length ? ( + {partners?.length ? ( ) : (

No Partners Found

diff --git a/apps/www/supabase/functions/lw7-ticket-og/handler.tsx b/apps/www/supabase/functions/lw7-ticket-og/handler.tsx index 89ebc54d31c..036ef6e10f4 100644 --- a/apps/www/supabase/functions/lw7-ticket-og/handler.tsx +++ b/apps/www/supabase/functions/lw7-ticket-og/handler.tsx @@ -261,7 +261,7 @@ export async function handler(req: Request) { headers: { 'Content-Type': 'application/json', Authorization: - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I', + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTIyNjkzMjYsImV4cCI6MjAwNzg0NTMyNn0.1S6qpBbHtEmGuMsIx5UOhRiFd4YbVv-yLTrLk6tVGmM', }, body: JSON.stringify({ username, diff --git a/apps/www/supabase/functions/lw8-ticket/handler.tsx b/apps/www/supabase/functions/lw8-ticket/handler.tsx index cda66443332..937982b919d 100644 --- a/apps/www/supabase/functions/lw8-ticket/handler.tsx +++ b/apps/www/supabase/functions/lw8-ticket/handler.tsx @@ -257,7 +257,7 @@ export async function handler(req: Request) { headers: { 'Content-Type': 'application/json', Authorization: - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I', + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTIyNjkzMjYsImV4cCI6MjAwNzg0NTMyNn0.1S6qpBbHtEmGuMsIx5UOhRiFd4YbVv-yLTrLk6tVGmM', }, body: JSON.stringify({ username, From b86849da563862c32a12ea965521223252edb6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 16 Aug 2023 23:28:40 +0200 Subject: [PATCH 069/117] chore: slight improvements for org-billing --- .../Home/ProjectList/ProjectList.tsx | 5 +- .../MigrateOrganizationBillingButton.tsx | 76 ++++++++++++++++++- .../OrganizationBillingMigrationPanel.tsx | 2 +- .../Organization/NewOrg/NewOrgForm.tsx | 2 +- .../interfaces/Organization/Usage/Usage.tsx | 2 +- .../Addons/ComputeInstanceSidePanel.tsx | 2 +- .../TransferProjectButton.tsx | 2 +- .../ProjectLayout/ProjectPausedState.tsx | 14 +++- ...anization-migrate-billing-preview-query.ts | 4 +- studio/pages/new/[slug].tsx | 18 +++-- 10 files changed, 106 insertions(+), 21 deletions(-) diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index d24aa81b1d0..bca0ab47d99 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -99,10 +99,7 @@ const OrganizationProjects = ({ return (
-

- {name} - {subscription_id ? V2 : <>} -

+

{name}

{!!overdueInvoices.length && (
diff --git a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx index e3d2f7a0184..6fc84c8c603 100644 --- a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx +++ b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx @@ -23,6 +23,7 @@ import { useOrganizationBillingMigrationPreview } from 'data/organizations/organ import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { PRICING_TIER_LABELS_ORG } from 'lib/constants' import PaymentMethodSelection from '../BillingSettingsV2/Subscription/PaymentMethodSelection' +import InformationBox from 'components/ui/InformationBox' const MigrateOrganizationBillingButton = observer(() => { const { ui } = useStore() @@ -114,7 +115,7 @@ const MigrateOrganizationBillingButton = observer(() => { {

For a detailed breakdown of changes, see{' '} - + Billing Migration Docs @@ -229,7 +230,7 @@ const MigrateOrganizationBillingButton = observer(() => {

Paid plans come with one compute instance included. Additional projects will at least cost the compute instance hours used (min $7/month). See{' '} - + Compute Instance Usage Billing @@ -318,6 +319,75 @@ const MigrateOrganizationBillingButton = observer(() => { )} + + {!migrationPreviewIsLoading && migrationPreviewData && dbTier !== 'tier_free' && ( + + + Estimated monthly price is $ + {migrationPreviewData.monthly_invoice_breakdown.reduce( + (prev, cur) => prev + cur.total_price, + 0 + )}{' '} + + usage + + } + hideCollapse={false} + description={ +

+
+ + + + + + + + + + {migrationPreviewData.monthly_invoice_breakdown.map((item) => ( + + + + + + + ))} + + + + + + + + +
+ Item + + Count + + Unit price + + Price +
{item.description ?? 'Unknown'}{item.quantity} + {item.unit_price === 0 ? 'FREE' : `$${item.unit_price}`} + ${item.total_price}
Total + + + $ + {migrationPreviewData.monthly_invoice_breakdown.reduce( + (prev, cur) => prev + cur.total_price, + 0 + ) ?? 0} +
+
+ } + /> + + )} +
- +
-
+

The project "{project?.name ?? ''}" is currently paused.

@@ -105,6 +105,18 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { You can also prevent project pausing in the future by upgrading to Pro.

)} + {!isFreePlan && ( +

+ Unpaused projects count towards compute usage. For every hour your instance is + active, we'll bill you based on the instance size of your project. See{' '} + + + Compute Instance Usage Billing + + {' '} + for more details. +

+ )}
diff --git a/studio/data/organizations/organization-migrate-billing-preview-query.ts b/studio/data/organizations/organization-migrate-billing-preview-query.ts index 8f9a117d5e6..ef5e6551319 100644 --- a/studio/data/organizations/organization-migrate-billing-preview-query.ts +++ b/studio/data/organizations/organization-migrate-billing-preview-query.ts @@ -10,12 +10,12 @@ export type OrganizationBillingMigrationPreviewVariables = { export type OrganizationBillingMigrationPreviewResponse = { added_credits: number - addons_to_be_removed?: { + addons_to_be_removed: { projectRef: string projectName: string addons: { variant: string; name: string; type: string }[] }[] - monthly_invoice_breakdown?: { + monthly_invoice_breakdown: { description: string unit_price: number quantity: number diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 70f25698dde..4842dfbac91 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -467,20 +467,26 @@ const Wizard: NextPageWithLayout = () => { description={

- This organization uses the new organization-level billing and is on the{' '} + This organization uses the new organization-level-billing and is on the{' '} {orgSubscription?.plan?.name} plan.

{/* Show info when launching a new project in a paid org that already has at least one project */} {orgSubscription?.plan?.id !== 'free' && orgProjectCount > 0 && ( -

- Launching another project incurs additional compute costs (starting at $10 - per month). -

+
+

+ Launching another project incurs additional compute costs, starting at + $0.01344 per hour (~$10/month). +

+

+ You can also create a new organization under the free plan (unless you + have exceeded your 2 free project limit). +

+
)}
- +
- +
-
+

The project "{project?.name ?? ''}" is currently paused.

@@ -105,6 +105,18 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { You can also prevent project pausing in the future by upgrading to Pro.

)} + {!isFreePlan && ( +

+ Unpaused projects count towards compute usage. For every hour your instance is + active, we'll bill you based on the instance size of your project. See{' '} + + + Compute Instance Usage Billing + + {' '} + for more details. +

+ )}
diff --git a/studio/data/organizations/organization-migrate-billing-preview-query.ts b/studio/data/organizations/organization-migrate-billing-preview-query.ts index 8f9a117d5e6..ef5e6551319 100644 --- a/studio/data/organizations/organization-migrate-billing-preview-query.ts +++ b/studio/data/organizations/organization-migrate-billing-preview-query.ts @@ -10,12 +10,12 @@ export type OrganizationBillingMigrationPreviewVariables = { export type OrganizationBillingMigrationPreviewResponse = { added_credits: number - addons_to_be_removed?: { + addons_to_be_removed: { projectRef: string projectName: string addons: { variant: string; name: string; type: string }[] }[] - monthly_invoice_breakdown?: { + monthly_invoice_breakdown: { description: string unit_price: number quantity: number diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 70f25698dde..4842dfbac91 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -467,20 +467,26 @@ const Wizard: NextPageWithLayout = () => { description={

- This organization uses the new organization-level billing and is on the{' '} + This organization uses the new organization-level-billing and is on the{' '} {orgSubscription?.plan?.name} plan.

{/* Show info when launching a new project in a paid org that already has at least one project */} {orgSubscription?.plan?.id !== 'free' && orgProjectCount > 0 && ( -

- Launching another project incurs additional compute costs (starting at $10 - per month). -

+
+

+ Launching another project incurs additional compute costs, starting at + $0.01344 per hour (~$10/month). +

+

+ You can also create a new organization under the free plan (unless you + have exceeded your 2 free project limit). +

+
)}
- + @@ -54,10 +54,17 @@ function TwitterSocialProof() {
+<<<<<<< HEAD
+ >>>>>> 288c2624e (fix carosel on lnading page) spaceBetween={0} slidesPerView={4} speed={300} From 45799b3d91b287c60212bb37a81f04287a871bc7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 18 Aug 2023 17:27:02 +0700 Subject: [PATCH 079/117] Implement create index functionality --- .../Database/Indexes/CreateIndexSidePanel.tsx | 239 ++++++++++++++++++ .../Database/Indexes/Indexes.constants.ts | 32 +++ .../interfaces/Database/Indexes/Indexes.tsx | 81 +++--- .../interfaces/SQLEditor/MonacoEditor.tsx | 6 +- .../components/ui/CodeEditor/CodeEditor.tsx | 1 + .../ui/CodeEditor/CodeEditor.utils.ts | 2 +- studio/components/ui/MultiSelect/Badges.tsx | 1 + studio/components/ui/MultiSelect/index.tsx | 139 +++++----- studio/data/database/table-columns-query.ts | 8 +- studio/pages/project/[ref]/sql/[id].tsx | 14 +- 10 files changed, 406 insertions(+), 117 deletions(-) create mode 100644 studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx create mode 100644 studio/components/interfaces/Database/Indexes/Indexes.constants.ts diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx new file mode 100644 index 00000000000..85176b28042 --- /dev/null +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -0,0 +1,239 @@ +import Link from 'next/link' +import { useEffect, useMemo, useState } from 'react' +import { Button, Input, Listbox, SidePanel } from 'ui' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import CodeEditor from 'components/ui/CodeEditor' +import MultiSelect, { MultiSelectOption } from 'components/ui/MultiSelect' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useTableColumnsQuery } from 'data/database/table-columns-query' +import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' +import { INDEX_TYPES } from './Indexes.constants' +import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { useIndexesQuery } from 'data/database/indexes-query' +import { useStore } from 'hooks' + +interface CreateIndexSidePanelProps { + visible: boolean + onClose: () => void +} + +const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => { + const { ui } = useStore() + const { project } = useProjectContext() + const [selectedSchema, setSelectedSchema] = useState('public') + const [selectedEntity, setSelectedEntity] = useState('---') + const [selectedColumns, setSelectedColumns] = useState([]) + const [selectedIndexType, setSelectedIndexType] = useState(INDEX_TYPES[0].value) + + const { refetch: refetchSchemaIndexes } = useIndexesQuery({ + schema: selectedSchema, + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { data: entities } = useEntityTypesQuery( + { + schema: selectedSchema, + sort: 'alphabetical', + search: undefined, + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { keepPreviousData: true } + ) + const { + data: tableColumns, + isLoading: isLoadingTableColumns, + isSuccess: isSuccessTableColumns, + } = useTableColumnsQuery({ + table: 'countries', + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ + onSuccess(data) { + refetchSchemaIndexes() + onClose() + ui.setNotification({ category: 'success', message: `Successfully created index` }) + }, + onError(error) { + ui.setNotification({ + error, + category: 'error', + message: `Failed to create index: ${error.message}`, + }) + }, + }) + + const entityTypes = useMemo( + () => entities?.pages.flatMap((page) => page.data.entities) || [], + [entities?.pages] + ) + const columns = tableColumns?.result[0].columns ?? [] + const columnOptions: MultiSelectOption[] = columns.map((column) => { + return { id: column.attname, value: column.attname, name: column.attname, disabled: false } + }) + + const generatedSQL = ` +CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexType} (${selectedColumns.join( + ', ' + )}); +`.trim() + + const onSaveIndex = () => { + if (!project) return console.error('Project is required') + + execute({ + projectRef: project.ref, + connectionString: project.connectionString, + sql: generatedSQL, + }) + } + + useEffect(() => { + if (visible) { + setSelectedSchema('public') + setSelectedEntity('---') + setSelectedColumns([]) + setSelectedIndexType(INDEX_TYPES[0].value) + } + }, [visible]) + + useEffect(() => { + setSelectedEntity('---') + setSelectedColumns([]) + setSelectedIndexType(INDEX_TYPES[0].value) + }, [selectedSchema]) + + useEffect(() => { + setSelectedColumns([]) + setSelectedIndexType(INDEX_TYPES[0].value) + }, [selectedEntity]) + + return ( + onSaveIndex()} + loading={isExecuting} + confirmText="Create index" + > +
+ + + {(schemas?.result ?? []).map((schema) => ( + + {schema.name} + + ))} + + + {entityTypes.length === 0 ? ( +
+

Select a table

+ +
+ ) : ( + + + --- + + {(entityTypes ?? []).map((entity) => ( + + {entity.name} + + ))} + + )} + + {selectedEntity !== '---' && ( +
+

Select up to 32 columns

+ {isLoadingTableColumns && } + {isSuccessTableColumns && ( + + )} +
+ )} +
+ + {selectedColumns.length > 0 && ( + <> + + + + {INDEX_TYPES.map((index) => ( + +

{index.name}

+

{index.description}

+
+ ))} +
+
+ + +
+

Preview of SQL statement

+ + + + + +
+
+
+
+ +
+
+ + )} +
+
+ ) +} + +export default CreateIndexSidePanel diff --git a/studio/components/interfaces/Database/Indexes/Indexes.constants.ts b/studio/components/interfaces/Database/Indexes/Indexes.constants.ts new file mode 100644 index 00000000000..2793de20221 --- /dev/null +++ b/studio/components/interfaces/Database/Indexes/Indexes.constants.ts @@ -0,0 +1,32 @@ +export const INDEX_TYPES = [ + { + name: 'B-Tree', + value: 'btree', + description: 'For querying data with equality or range conditions on columns.', + }, + { + name: 'Hash', + value: 'hash', + description: 'For querying exact matches on columns', + }, + { + name: 'GiST', + value: 'gist', + description: 'For querying complex data types or custom-defined operators', + }, + { + name: 'SP-GiST', + value: 'spgist', + description: 'Similar to GiST, but is more specialized and customizable', + }, + { + name: 'GIN', + value: 'gin', + description: 'For querying multi-valued data such as arrays or full-text search scenarios', + }, + { + name: 'BRIN', + value: 'brin', + description: 'For querying large tables with sorted data', + }, +] diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx index 304f74b21f2..a4893bafa54 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -18,11 +18,13 @@ import { DatabaseIndex, useIndexesQuery } from 'data/database/indexes-query' import { useSchemasQuery } from 'data/database/schemas-query' import Table from 'components/to-be-cleaned/Table' import CodeEditor from 'components/ui/CodeEditor' +import CreateIndexSidePanel from './CreateIndexSidePanel' const Indexes = () => { const [search, setSearch] = useState('') const [selectedSchema, setSelectedSchema] = useState('public') const [selectedIndex, setSelectedIndex] = useState() + const [showCreateIndex, setShowCreateIndex] = useState(false) const { project } = useProjectContext() const { @@ -82,41 +84,50 @@ const Indexes = () => {
-
- {isLoadingSchemas && } - {isErrorSchemas && ( -
- -

Failed to load schemas

-
- )} - {isSuccessSchemas && ( - +
+ {isLoadingSchemas && } + {isErrorSchemas && ( +
+ +

Failed to load schemas

+
+ )} + {isSuccessSchemas && ( + + {(schemas?.result ?? []).map((schema) => ( + schema} + > + {schema.name} + + ))} + + )} + - {(schemas?.result ?? []).map((schema) => ( - schema} - > - {schema.name} - - ))} - - )} - setSearch(e.target.value)} - placeholder="Search for an index" - icon={} - /> + value={search} + className="w-[250px]" + onChange={(e) => setSearch(e.target.value)} + placeholder="Search for an index" + icon={} + /> +
+
{isLoadingIndexes && } @@ -203,6 +214,8 @@ const Indexes = () => {
+ + setShowCreateIndex(false)} /> ) } diff --git a/studio/components/interfaces/SQLEditor/MonacoEditor.tsx b/studio/components/interfaces/SQLEditor/MonacoEditor.tsx index 85a40e1a0e3..1ba9617554c 100644 --- a/studio/components/interfaces/SQLEditor/MonacoEditor.tsx +++ b/studio/components/interfaces/SQLEditor/MonacoEditor.tsx @@ -1,7 +1,7 @@ import Editor, { Monaco, OnMount } from '@monaco-editor/react' import { useParams } from 'common' import { useRouter } from 'next/router' -import { MutableRefObject, useRef, useState } from 'react' +import { MutableRefObject, useRef } from 'react' import { cn } from 'ui' import { SqlSnippet } from 'data/content/sql-snippets-query' @@ -27,7 +27,7 @@ const MonacoEditor = ({ className, executeQuery, }: MonacoEditorProps) => { - const { ref } = useParams() + const { ref, content } = useParams() const router = useRouter() const { profile } = useProfile() const project = useSelectedProject() @@ -102,7 +102,7 @@ const MonacoEditor = ({ onMount={handleEditorOnMount} onChange={handleEditorChange} defaultLanguage="pgsql" - defaultValue={snippet?.snippet.content.sql} + defaultValue={snippet?.snippet.content.sql ?? content} path={id} options={{ tabSize: 2, diff --git a/studio/components/ui/CodeEditor/CodeEditor.tsx b/studio/components/ui/CodeEditor/CodeEditor.tsx index 06cdf43c0c7..7939c7881e7 100644 --- a/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -37,6 +37,7 @@ const CodeEditor = ({ const onMount = async (editor: any, monaco: any) => { alignEditor(editor) + editor.addAction({ id: 'supabase', label: 'Run Query', diff --git a/studio/components/ui/CodeEditor/CodeEditor.utils.ts b/studio/components/ui/CodeEditor/CodeEditor.utils.ts index 9b7808ccc71..8f34d2d344f 100644 --- a/studio/components/ui/CodeEditor/CodeEditor.utils.ts +++ b/studio/components/ui/CodeEditor/CodeEditor.utils.ts @@ -3,7 +3,7 @@ export const alignEditor = (editor: any) => { editor.changeViewZones((accessor: any) => { accessor.addZone({ afterLineNumber: 0, - heightInPx: 10, + heightInPx: 4, domNode: document.createElement('div'), }) }) diff --git a/studio/components/ui/MultiSelect/Badges.tsx b/studio/components/ui/MultiSelect/Badges.tsx index 1f891906563..becb862e832 100644 --- a/studio/components/ui/MultiSelect/Badges.tsx +++ b/studio/components/ui/MultiSelect/Badges.tsx @@ -33,6 +33,7 @@ export const BadgeSelected = ({ className="cursor-pointer opacity-50 transition hover:opacity-100" onClick={(e: any) => { e.preventDefault() + e.stopPropagation() handleRemove() }} /> diff --git a/studio/components/ui/MultiSelect/index.tsx b/studio/components/ui/MultiSelect/index.tsx index 6c78b84a762..275e1e704c3 100644 --- a/studio/components/ui/MultiSelect/index.tsx +++ b/studio/components/ui/MultiSelect/index.tsx @@ -1,13 +1,17 @@ -import { useRef, useEffect, useState, FormEvent, KeyboardEvent, ReactNode } from 'react' -import { orderBy, filter, without } from 'lodash' +import clsx from 'clsx' +import { filter, orderBy, without } from 'lodash' +import { KeyboardEvent, ReactNode, useEffect, useRef, useState } from 'react' import { - Popover, - IconCheck, IconAlertCircle, - IconSearch, - IconPlus, + IconCheck, IconChevronDown, + IconPlus, + IconSearch, Input, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, + ScrollArea, } from 'ui' import { BadgeDisabled, BadgeSelected } from './Badges' @@ -54,6 +58,7 @@ export default function MultiSelect({ }: Props) { const ref = useRef(null) + const [open, setOpen] = useState(false) const [selected, setSelected] = useState(value || []) const [searchString, setSearchString] = useState('') const [inputWidth, setInputWidth] = useState(128) @@ -66,6 +71,10 @@ export default function MultiSelect({ setInputWidth(ref.current ? (ref.current as any).offsetWidth : inputWidth) }, [ref.current]) + useEffect(() => { + if (!open) setSearchString('') + }, [open]) + const width = `${inputWidth}px` // Order the options so disabled items are at the beginning @@ -131,28 +140,59 @@ export default function MultiSelect({ ].join(' ')} ref={ref} > - - - ) => - setSearchString(e.currentTarget.value) + + +
setOpen(true)} + > + {selectedOptions.length === 0 && placeholder && ( +
+ {placeholder} +
+ )} + {selectedOptions.map((value, idx) => { + const id = `${value}-${idx}` + const option = formattedOptions.find((x) => x.value === value) + const isDisabled = option?.disabled ?? false + if (!option) { + return <> + } else if (isDisabled) { + return + } else { + return ( + + allowDuplicateSelection ? handleRemove(idx) : handleChange(option) + } + /> + ) } - /> + })} +
+ +
- } - overlay={ -
+ + + } + placeholder={searchPlaceholder} + value={searchString} + onChange={(e) => setSearchString(e.target.value)} + /> + 5 ? 'h-[225px]' : '')}> {filteredOptions.length >= 1 ? ( filteredOptions.map((option) => { const active = @@ -185,9 +225,7 @@ export default function MultiSelect({ )} {allowDuplicateSelection && ( @@ -231,46 +269,9 @@ export default function MultiSelect({ )}
)} -
- } - onOpenChange={() => setSearchString('')} - > -
- {selectedOptions.length === 0 && placeholder && ( -
- {placeholder} -
- )} - {selectedOptions.map((value, idx) => { - const id = `${value}-${idx}` - const option = formattedOptions.find((x) => x.value === value) - const isDisabled = option?.disabled ?? false - if (!option) { - return <> - } else if (isDisabled) { - return - } else { - return ( - - allowDuplicateSelection ? handleRemove(idx) : handleChange(option) - } - /> - ) - } - })} -
- -
-
- + + +
{descriptionText && ( diff --git a/studio/data/database/table-columns-query.ts b/studio/data/database/table-columns-query.ts index 7fa27a50b4c..b0d898eb965 100644 --- a/studio/data/database/table-columns-query.ts +++ b/studio/data/database/table-columns-query.ts @@ -10,7 +10,7 @@ export type TableColumn = { columns: any[] } -export const getTableColumnsQuery = () => { +export const getTableColumnsQuery = (table?: string) => { const sql = /* SQL */ ` SELECT tbl.schemaname, @@ -67,6 +67,7 @@ export const getTableColumnsQuery = () => { AND NOT a.attisdropped AND has_column_privilege(tbl.quoted_name, a.attname, 'SELECT, INSERT, UPDATE, REFERENCES') ) + ${table !== undefined ? `WHERE tablename = '${table}'` : ''} GROUP BY schemaname, tablename, quoted_name, is_table; `.trim() @@ -76,20 +77,21 @@ export const getTableColumnsQuery = () => { export type TableColumnsVariables = { projectRef?: string connectionString?: string + table?: string } export type TableColumnsData = { result: TableColumn[] } export type TableColumnsError = unknown export const useTableColumnsQuery = ( - { projectRef, connectionString }: TableColumnsVariables, + { projectRef, connectionString, table }: TableColumnsVariables, options: UseQueryOptions = {} ) => { return useExecuteSqlQuery( { projectRef, connectionString, - sql: getTableColumnsQuery(), + sql: getTableColumnsQuery(table), queryKey: ['table-columns'], }, options diff --git a/studio/pages/project/[ref]/sql/[id].tsx b/studio/pages/project/[ref]/sql/[id].tsx index 538f48a21ca..1afc3c4ee53 100644 --- a/studio/pages/project/[ref]/sql/[id].tsx +++ b/studio/pages/project/[ref]/sql/[id].tsx @@ -1,17 +1,17 @@ -import { useTheme } from 'common' -import { useEffect, useRef } from 'react' -import { observer } from 'mobx-react-lite' import { useMonaco } from '@monaco-editor/react' +import { useTheme } from 'common' +import { observer } from 'mobx-react-lite' +import { useEffect, useRef } from 'react' -import { NextPageWithLayout } from 'types' -import { useFormatQueryMutation } from 'data/sql/format-sql-query' -import { useKeywordsQuery } from 'data/database/keywords-query' import { useFunctionsQuery } from 'data/database/functions-query' +import { useKeywordsQuery } from 'data/database/keywords-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useTableColumnsQuery } from 'data/database/table-columns-query' +import { useFormatQueryMutation } from 'data/sql/format-sql-query' +import { NextPageWithLayout } from 'types' -import { SQLEditorLayout } from 'components/layouts' import SQLEditor from 'components/interfaces/SQLEditor/SQLEditor' +import { SQLEditorLayout } from 'components/layouts' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import getPgsqlCompletionProvider from 'components/ui/CodeEditor/Providers/PgSQLCompletionProvider' import getPgsqlSignatureHelpProvider from 'components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider' From 630d1404d01250c733cd74ebb557326513c9bffd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 18 Aug 2023 17:56:50 +0700 Subject: [PATCH 080/117] implement delete index functionality --- .../Indexes/ConfirmDeleteIndexModal.tsx | 0 .../Database/Indexes/CreateIndexSidePanel.tsx | 6 +- .../interfaces/Database/Indexes/Indexes.tsx | 88 ++++++++++++++++++- studio/data/database/indexes-query.ts | 7 +- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx diff --git a/studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx b/studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 85176b28042..01c6a54e55e 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -27,7 +27,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = const [selectedColumns, setSelectedColumns] = useState([]) const [selectedIndexType, setSelectedIndexType] = useState(INDEX_TYPES[0].value) - const { refetch: refetchSchemaIndexes } = useIndexesQuery({ + const { refetch: refetchIndexes } = useIndexesQuery({ schema: selectedSchema, projectRef: project?.ref, connectionString: project?.connectionString, @@ -57,8 +57,8 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = }) const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ - onSuccess(data) { - refetchSchemaIndexes() + onSuccess() { + refetchIndexes() onClose() ui.setNotification({ category: 'success', message: `Successfully created index` }) }, diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx index a4893bafa54..ca35fc12275 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -2,12 +2,18 @@ import { observer } from 'mobx-react-lite' import Link from 'next/link' import { useState } from 'react' import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, Button, IconAlertCircle, + IconAlertTriangle, IconExternalLink, IconSearch, + IconTrash, Input, Listbox, + Modal, SidePanel, } from 'ui' @@ -19,16 +25,22 @@ import { useSchemasQuery } from 'data/database/schemas-query' import Table from 'components/to-be-cleaned/Table' import CodeEditor from 'components/ui/CodeEditor' import CreateIndexSidePanel from './CreateIndexSidePanel' +import ConfirmationModal from 'components/ui/ConfirmationModal' +import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { useStore } from 'hooks' const Indexes = () => { + const { ui } = useStore() const [search, setSearch] = useState('') const [selectedSchema, setSelectedSchema] = useState('public') - const [selectedIndex, setSelectedIndex] = useState() const [showCreateIndex, setShowCreateIndex] = useState(false) + const [selectedIndex, setSelectedIndex] = useState() + const [selectedIndexToDelete, setSelectedIndexToDelete] = useState() const { project } = useProjectContext() const { data: allIndexes, + refetch: refetchIndexes, error: indexesError, isLoading: isLoadingIndexes, isSuccess: isSuccessIndexes, @@ -48,6 +60,21 @@ const Indexes = () => { connectionString: project?.connectionString, }) + const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ + onSuccess() { + refetchIndexes() + setSelectedIndexToDelete(undefined) + ui.setNotification({ category: 'success', message: `Successfully deleted index` }) + }, + onError(error) { + ui.setNotification({ + error, + category: 'error', + message: `Failed to delete index: ${error.message}`, + }) + }, + }) + const sortedIndexes = (allIndexes?.result ?? []).sort( (a, b) => a.table.localeCompare(b.table) || a.name.localeCompare(b.name) ) @@ -56,6 +83,15 @@ const Indexes = () => { ? sortedIndexes.filter((index) => index.name.includes(search) || index.table.includes(search)) : sortedIndexes + const onConfirmDeleteIndex = (index: DatabaseIndex) => { + if (!project) return console.error('Project is required') + execute({ + projectRef: project.ref, + connectionString: project.connectionString, + sql: `drop index if exists "${index.name}"`, + }) + } + return ( <>
@@ -179,10 +215,16 @@ const Indexes = () => {

{index.name}

-
+
+
@@ -216,6 +258,48 @@ const Indexes = () => { setShowCreateIndex(false)} /> + + + Confirm to delete index {selectedIndexToDelete?.name} + + } + buttonLabel="Confirm delete" + buttonLoadingLabel="Deleting..." + onSelectConfirm={() => + selectedIndexToDelete !== undefined ? onConfirmDeleteIndex(selectedIndexToDelete) : {} + } + onSelectCancel={() => setSelectedIndexToDelete(undefined)} + > + +
+ + + This action cannot be undone + + Deleting an index that is still in use will cause queries to slow down, in some + cases causing major issues. + + +
    +
  • +
    + Before deleting this index, consider: +
      +
    • This index is no longer in use
    • +
    • Disabling the index instead of deleting it
    • +
    +
    +
  • +
+
+
+
) } diff --git a/studio/data/database/indexes-query.ts b/studio/data/database/indexes-query.ts index a1ad90d860a..ca24b67c786 100644 --- a/studio/data/database/indexes-query.ts +++ b/studio/data/database/indexes-query.ts @@ -9,6 +9,7 @@ export type DatabaseIndex = { schema: string table: string definition: string + enabled: boolean } export const getIndexesQuery = ({ schema }: { schema: string }) => { @@ -16,8 +17,10 @@ export const getIndexesQuery = ({ schema }: { schema: string }) => { SELECT schemaname as "schema", tablename as "table", indexname as "name", - indexdef as "definition" -FROM pg_indexes + indexdef as "definition", + indisvalid as "enabled" +FROM pg_indexes a + LEFT JOIN pg_index b ON b.indexrelid = a.indexname::regclass WHERE schemaname = '${schema}'; `.trim() From d0064bf61a41fc759d466e708de20d3599b2ab56 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 18 Aug 2023 23:42:33 +1000 Subject: [PATCH 081/117] fix: only save truthy values on wrapper table options --- studio/data/fdw/fdw-create-mutation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/studio/data/fdw/fdw-create-mutation.ts b/studio/data/fdw/fdw-create-mutation.ts index 590ec98f744..3243178d1f3 100644 --- a/studio/data/fdw/fdw-create-mutation.ts +++ b/studio/data/fdw/fdw-create-mutation.ts @@ -101,11 +101,13 @@ export function getCreateFDWSql({ options ( ${Object.entries(newTable) .filter( - ([key]) => + ([key, value]) => key !== 'table_name' && key !== 'schema_name' && key !== 'columns' && - key !== 'index' + key !== 'index' && + key !== 'is_new_schema' && + Boolean(value) ) .map(([key, value]) => `${key} '${value}'`) .join(',\n ')} From 282daad2cd5213303839ccbe876a8d90a3d1f9e0 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 18 Aug 2023 23:46:58 +1000 Subject: [PATCH 082/117] chore: enable airtable wrapper --- .../Database/Wrappers/Wrappers.constants.ts | 85 +++++++++---------- .../Database/Wrappers/WrappersDropdown.tsx | 17 ---- 2 files changed, 42 insertions(+), 60 deletions(-) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index 7ea87aeb22d..618774cd858 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -750,49 +750,48 @@ export const WRAPPERS: WrapperMeta[] = [ }, ], }, - // Commented out until the airtable is available in the postgres image - // { - // name: 'airtable_wrapper', - // handlerName: 'airtable_fdw_handler', - // validatorName: 'airtable_fdw_validator', - // icon: `${BASE_PATH}/img/icons/airtable-icon.svg`, - // extensionName: 'airtableFdw', - // label: 'Airtable', - // docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/airtable', - // server: { - // options: [ - // { - // name: 'api_key_id', - // label: 'API Key ID', - // required: true, - // encrypted: true, - // hidden: true, - // }, - // ], - // }, - // tables: [ - // { - // label: 'Airtable Table', - // description: 'Map to an Airtable Table', - // options: [ - // { - // name: 'base_id', - // label: 'Base ID', - // editable: true, - // required: true, - // type: 'text', - // }, - // { - // name: 'table_id', - // label: 'Table ID', - // editable: true, - // required: true, - // type: 'text', - // }, - // ], - // }, - // ], - // }, + { + name: 'airtable_wrapper', + handlerName: 'airtable_fdw_handler', + validatorName: 'airtable_fdw_validator', + icon: `${BASE_PATH}/img/icons/airtable-icon.svg`, + extensionName: 'airtableFdw', + label: 'Airtable', + docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/airtable', + server: { + options: [ + { + name: 'api_key_id', + label: 'API Key ID', + required: true, + encrypted: true, + hidden: true, + }, + ], + }, + tables: [ + { + label: 'Airtable Table', + description: 'Map to an Airtable Table', + options: [ + { + name: 'base_id', + label: 'Base ID', + editable: true, + required: true, + type: 'text', + }, + { + name: 'table_id', + label: 'Table ID', + editable: true, + required: true, + type: 'text', + }, + ], + }, + ], + }, { name: 'logflare_wrapper', handlerName: 'logflare_fdw_handler', diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx index fc3fdebd80e..a44288c5a94 100644 --- a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -8,7 +8,6 @@ import { Button, Dropdown, IconPlus } from 'ui' import { useParams } from 'common/hooks' import { useCheckPermissions } from 'hooks' -import { BASE_PATH } from 'lib/constants' import { WRAPPERS } from './Wrappers.constants' interface WrapperDropdownProps { @@ -78,22 +77,6 @@ const WrapperDropdown = ({ buttonText = 'Add wrapper', align = 'end' }: WrapperD {idx !== WRAPPERS.length - 1 && } ))} - - - - - } - > - Airtable (soon) - } > From 1a24d823282d6fb6d0dc8cadd07eef613b29b8e9 Mon Sep 17 00:00:00 2001 From: Saber Heydari <55661572+Silverethical@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:08:45 +0330 Subject: [PATCH 083/117] Install `@supabase/auth-ui-shared` before using it Based on the example provided, @supabase/auth-ui-shared package is also needed to be installed. However, it is not mentioned in the previous section. --- apps/docs/pages/guides/auth/quickstarts/react.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/quickstarts/react.mdx b/apps/docs/pages/guides/auth/quickstarts/react.mdx index 3dfdc081c80..8a852e454fd 100644 --- a/apps/docs/pages/guides/auth/quickstarts/react.mdx +++ b/apps/docs/pages/guides/auth/quickstarts/react.mdx @@ -58,7 +58,7 @@ export const meta = { ```bash Terminal - cd my-app && npm install @supabase/supabase-js @supabase/auth-ui-react + cd my-app && npm install @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shared ``` From a4e25bc3d2a1a9b7e6c670a670b224a0eec92f35 Mon Sep 17 00:00:00 2001 From: PRIYANSHU PRASAD GUPTA <100031428+priyanshu2k3@users.noreply.github.com> Date: Fri, 18 Aug 2023 23:04:04 +0530 Subject: [PATCH 084/117] fied some problems TwitterSocialProof.tsx i made some wrong commits rectified in this commit --- apps/www/components/Sections/TwitterSocialProof.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx index c9370474b4a..45062e98ebc 100644 --- a/apps/www/components/Sections/TwitterSocialProof.tsx +++ b/apps/www/components/Sections/TwitterSocialProof.tsx @@ -39,7 +39,7 @@ function TwitterSocialProof() { @@ -54,17 +54,10 @@ function TwitterSocialProof() {
-<<<<<<< HEAD
- >>>>>> 288c2624e (fix carosel on lnading page) spaceBetween={0} slidesPerView={4} speed={300} From 76b3e9adeb53d274685fea8a48749f1c4e892bc7 Mon Sep 17 00:00:00 2001 From: Adrian Fraguela Date: Fri, 18 Aug 2023 20:49:11 +0100 Subject: [PATCH 085/117] Update with-nextjs.mdx Grammar --- .../docs/pages/guides/getting-started/tutorials/with-nextjs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx index 4bf3351ce31..a771e0f5571 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx @@ -333,7 +333,7 @@ export async function GET(req: NextRequest) { ### Sign out -Let's create an route handler to handle the signout from the server side. +Let's create a route handler to handle the signout from the server side. Date: Sat, 19 Aug 2023 14:21:18 +0700 Subject: [PATCH 086/117] Update delete index confirmation modal content --- .../Database/Indexes/ConfirmDeleteIndexModal.tsx | 0 .../interfaces/Database/Indexes/Indexes.tsx | 11 +++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx diff --git a/studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx b/studio/components/interfaces/Database/Indexes/ConfirmDeleteIndexModal.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx index ca35fc12275..8450e8538f5 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -220,7 +220,7 @@ const Indexes = () => { View definition
From 5d8b308a50712a3a9b1da0ef2669cce9136ab360 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Sat, 19 Aug 2023 14:22:52 +0700 Subject: [PATCH 087/117] Prevent autofocusing into code editor in create index side panel --- .../interfaces/Database/Indexes/CreateIndexSidePanel.tsx | 1 + studio/components/ui/CodeEditor/CodeEditor.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 01c6a54e55e..e0a9474bb88 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -221,6 +221,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
void @@ -24,6 +25,7 @@ const CodeEditor = ({ id, language, defaultValue, + autofocus = true, isReadOnly = false, hideLineNumbers = false, onInputChange = noop, @@ -53,7 +55,7 @@ const CodeEditor = ({ }) await timeout(500) - editor?.focus() + if (autofocus) editor?.focus() editorRef.current = editor } From d2e7dddfd1fed2b8eac45e71652dbff8f275ec94 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Sat, 19 Aug 2023 14:28:09 +0700 Subject: [PATCH 088/117] Small style fixes --- studio/components/grid/components/menu/ColumnMenu.tsx | 2 +- .../interfaces/Database/Indexes/CreateIndexSidePanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/studio/components/grid/components/menu/ColumnMenu.tsx b/studio/components/grid/components/menu/ColumnMenu.tsx index 7d2c3553b9f..0d4af517ce5 100644 --- a/studio/components/grid/components/menu/ColumnMenu.tsx +++ b/studio/components/grid/components/menu/ColumnMenu.tsx @@ -46,7 +46,7 @@ const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => { <> {state.editable && onEditColumn !== undefined && ( - +
-
+
Date: Sat, 19 Aug 2023 14:53:27 +0700 Subject: [PATCH 089/117] Address comments --- .../layouts/ProjectLayout/BuildingState.tsx | 4 ++++ .../layouts/ProjectLayout/PausingState.tsx | 2 ++ studio/data/projects/project-detail-query.ts | 14 +++----------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/studio/components/layouts/ProjectLayout/BuildingState.tsx b/studio/components/layouts/ProjectLayout/BuildingState.tsx index 59e6f8f0b68..42418ba43ab 100644 --- a/studio/components/layouts/ProjectLayout/BuildingState.tsx +++ b/studio/components/layouts/ProjectLayout/BuildingState.tsx @@ -11,12 +11,15 @@ import { invalidateProjectsQuery } from 'data/projects/projects-query' import { getWithTimeout } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' import { Project } from 'types' +import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' +import { useParams } from 'common' export interface BuildingStateProps { project: Project } const BuildingState = ({ project }: BuildingStateProps) => { + const { ref } = useParams() const queryClient = useQueryClient() const checkServerInterval = useRef() @@ -31,6 +34,7 @@ const BuildingState = ({ project }: BuildingStateProps) => { const { status } = projectStatus if (status === PROJECT_STATUS.ACTIVE_HEALTHY) { clearInterval(checkServerInterval.current) + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) await invalidateProjectsQuery(queryClient) } } diff --git a/studio/components/layouts/ProjectLayout/PausingState.tsx b/studio/components/layouts/ProjectLayout/PausingState.tsx index d802248c3de..e2cce40abce 100644 --- a/studio/components/layouts/ProjectLayout/PausingState.tsx +++ b/studio/components/layouts/ProjectLayout/PausingState.tsx @@ -7,6 +7,7 @@ import { invalidateProjectsQuery } from 'data/projects/projects-query' import { PROJECT_STATUS } from 'lib/constants' import { Project } from 'types' import { useEffect, useState } from 'react' +import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' export interface PausingStateProps { project: Project @@ -26,6 +27,7 @@ const PausingState = ({ project }: PausingStateProps) => { }, onSuccess: async (res) => { if (res.status === PROJECT_STATUS.INACTIVE) { + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) await invalidateProjectsQuery(queryClient) } }, diff --git a/studio/data/projects/project-detail-query.ts b/studio/data/projects/project-detail-query.ts index d6d6e195045..4d7408cf4e5 100644 --- a/studio/data/projects/project-detail-query.ts +++ b/studio/data/projects/project-detail-query.ts @@ -1,4 +1,4 @@ -import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import { useCallback } from 'react' import { get, isResponseOk } from 'lib/common/fetch' @@ -33,14 +33,6 @@ export const useProjectDetailQuery = ( } ) -export const useProjectDetailPrefetch = ({ ref }: ProjectDetailVariables) => { - const client = useQueryClient() - - return useCallback(() => { - if (ref) { - client.prefetchQuery(projectKeys.detail(ref), ({ signal }) => - getProjectDetail({ ref }, signal) - ) - } - }, [ref]) +export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) { + return client.invalidateQueries(projectKeys.detail(ref)) } From 14e5a2f71202a456ebd5ca9596cffbce76b6bda5 Mon Sep 17 00:00:00 2001 From: amintai Date: Sat, 19 Aug 2023 17:54:36 +0530 Subject: [PATCH 090/117] Fixed navbar active link color issue --- apps/www/components/Nav/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 4149d2eb1e6..758fca78a08 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -166,7 +166,6 @@ const Nav = () => {
{ text-sm font-medium transition-colors`, showLaunchWeekNavMode && '!text-white', + props.active ? 'text-brand' : 'hover:text-brand', props.active, ].join(' ')} onClick={props.onClick} From 81dd8a7831c8683f02977a1bcee937a5e605f5eb Mon Sep 17 00:00:00 2001 From: samueldenzil Date: Sun, 20 Aug 2023 15:10:36 +0530 Subject: [PATCH 091/117] Fix markdown URL rendering issue --- apps/docs/pages/guides/self-hosting/storage/config.mdx | 2 ++ spec/storage_v0_config.yaml | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/self-hosting/storage/config.mdx b/apps/docs/pages/guides/self-hosting/storage/config.mdx index a2e12665551..295e4193d44 100644 --- a/apps/docs/pages/guides/self-hosting/storage/config.mdx +++ b/apps/docs/pages/guides/self-hosting/storage/config.mdx @@ -11,6 +11,8 @@ export const meta = {
{spec.info.description}
+A sample `.env` file is located in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). +
{spec.info.tags.map(tag => { return ( diff --git a/spec/storage_v0_config.yaml b/spec/storage_v0_config.yaml index fd45b2d862f..c39b7073fe1 100644 --- a/spec/storage_v0_config.yaml +++ b/spec/storage_v0_config.yaml @@ -9,8 +9,6 @@ info: bugs: 'https://github.com/supabase/storage-api/issues' # {string} Where developers can file bugs. spec: 'https://github.com/supabase/supabase/blob/master/spec/storage_v1_config.yml' # {string} Where developers can find this spec (to link directly in the docs). description: | - A sample `.env` file is located in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). - Use this file to configure your environment variables for your Storage server. tags: - id: general From fb908cf1fe1367401785ecd331acb28b2b952db6 Mon Sep 17 00:00:00 2001 From: samueldenzil Date: Sun, 20 Aug 2023 16:02:32 +0530 Subject: [PATCH 092/117] Fix markdown URL rendering issue --- apps/docs/pages/guides/self-hosting/storage/config.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/guides/self-hosting/storage/config.mdx b/apps/docs/pages/guides/self-hosting/storage/config.mdx index 295e4193d44..c6d637300b2 100644 --- a/apps/docs/pages/guides/self-hosting/storage/config.mdx +++ b/apps/docs/pages/guides/self-hosting/storage/config.mdx @@ -9,10 +9,10 @@ export const meta = { description: 'How to configure and deploy Supabase.', } -
{spec.info.description}
- A sample `.env` file is located in the [storage repository](https://github.com/supabase/storage-api/blob/master/.env.sample). +
{spec.info.description}
+
{spec.info.tags.map(tag => { return ( From 2bcff99b5dbb5ed45dee14576e3bc34e9ce93873 Mon Sep 17 00:00:00 2001 From: Mohamed Daw Date: Sun, 20 Aug 2023 16:14:36 +0300 Subject: [PATCH 093/117] Update README.md --- studio/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/README.md b/studio/README.md index b94a0ae0041..bd232f5b0f4 100644 --- a/studio/README.md +++ b/studio/README.md @@ -34,7 +34,7 @@ Project settings are managed outside of the Dashboard. If you use docker compose ### Developer Quickstart ```bash -# You'll need to be on Node v14 +# You'll need to be on Node v18 # in /studio npm i # install dependencies From 5dcdf93695932f1a566c78ec1bd114e3cdf83069 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 14:00:52 +0800 Subject: [PATCH 094/117] Ensure that return to project button invalidates RQ --- .../interfaces/Settings/General/General.tsx | 4 ++-- .../General/Infrastructure/PauseProjectButton.tsx | 8 ++++---- .../General/Infrastructure/RestartServerButton.tsx | 4 ++-- .../layouts/ProjectLayout/ProjectLayout.tsx | 2 +- .../layouts/ProjectLayout/RestoringState.tsx | 11 +++++++---- .../ProjectLayout/UpgradingState/UpgradingState.tsx | 9 +++++---- studio/data/projects/project-detail-query.ts | 3 +-- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/studio/components/interfaces/Settings/General/General.tsx b/studio/components/interfaces/Settings/General/General.tsx index af429ec80fc..2088ab521b8 100644 --- a/studio/components/interfaces/Settings/General/General.tsx +++ b/studio/components/interfaces/Settings/General/General.tsx @@ -125,7 +125,7 @@ const General = () => {

- {project && } +
@@ -136,7 +136,7 @@ const General = () => {

- {project && } +
diff --git a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx index 364336086f2..c9671048d82 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx @@ -16,10 +16,10 @@ import { API_URL, PROJECT_STATUS } from 'lib/constants' import { Button, IconPause } from 'ui' const PauseProjectButton = () => { - const queryClient = useQueryClient() const { ui } = useStore() - const { project } = useProjectContext() const router = useRouter() + const queryClient = useQueryClient() + const { project } = useProjectContext() const isProjectActive = useIsProjectActive() const [loading, setLoading] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) @@ -70,12 +70,12 @@ const PauseProjectButton = () => { icon={} onClick={openModal} loading={loading} - disabled={isPaused || !canPauseProject || !isProjectActive} + disabled={project === undefined || isPaused || !canPauseProject || !isProjectActive} > Pause Project - {isPaused || !canPauseProject || !isProjectActive ? ( + {project !== undefined && (isPaused || !canPauseProject || !isProjectActive) ? ( diff --git a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index 0c3bd1bd5c8..f05bb989f35 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -87,7 +87,7 @@ const RestartServerButton = () => {
- {(!canRestartProject || !isProjectActive) && ( + {project !== undefined && (!canRestartProject || !isProjectActive) && ( diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 90d627cc098..72d71362d2c 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -159,8 +159,8 @@ interface ContentWrapperProps { * [TODO] Next iteration should scrape long polling and just listen to the project's status */ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => { - const selectedProject = useSelectedProject() const router = useRouter() + const selectedProject = useSelectedProject() const requiresDbConnection: boolean = (!router.pathname.includes('/project/[ref]/settings') && diff --git a/studio/components/layouts/ProjectLayout/RestoringState.tsx b/studio/components/layouts/ProjectLayout/RestoringState.tsx index 5902f68d075..700f2a23bdf 100644 --- a/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -1,15 +1,19 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'common' import Link from 'next/link' import { useEffect, useRef, useState } from 'react' import { Button, IconAlertCircle, IconCheckCircle, IconLoader } from 'ui' -import { getProjectDetail } from 'data/projects/project-detail-query' +import { getProjectDetail, invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useStore } from 'hooks' import { getWithTimeout } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' import { useProjectContext } from './ProjectContext' const RestoringState = () => { + const { ref } = useParams() const { meta } = useStore() + const queryClient = useQueryClient() const { project } = useProjectContext() const checkServerInterval = useRef() @@ -45,9 +49,8 @@ const RestoringState = () => { setLoading(true) const projectDetail = await getProjectDetail({ ref: project?.ref }) - if (projectDetail) { - meta.setProjectDetails(projectDetail) - } + if (projectDetail) meta.setProjectDetails(projectDetail) + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) } return ( diff --git a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx index dc7e4cd26cc..d978e253b6f 100644 --- a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx +++ b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx @@ -17,15 +17,17 @@ import { import { useParams } from 'common/hooks' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' -import { getProjectDetail } from 'data/projects/project-detail-query' +import { getProjectDetail, invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useStore } from 'hooks' import { IS_PLATFORM } from 'lib/constants' import { useProjectContext } from '../ProjectContext' import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' +import { useQueryClient } from '@tanstack/react-query' const UpgradingState = () => { const { ref } = useParams() const { meta } = useStore() + const queryClient = useQueryClient() const { project } = useProjectContext() const [loading, setLoading] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -55,9 +57,8 @@ const UpgradingState = () => { setLoading(true) const projectDetail = await getProjectDetail({ ref }) - if (projectDetail) { - meta.setProjectDetails(projectDetail) - } + if (projectDetail) meta.setProjectDetails(projectDetail) + if (ref) await invalidateProjectDetailsQuery(queryClient, ref) } const subject = 'Upgrade%20failed%20for%20project' diff --git a/studio/data/projects/project-detail-query.ts b/studio/data/projects/project-detail-query.ts index 4d7408cf4e5..cab2475d06c 100644 --- a/studio/data/projects/project-detail-query.ts +++ b/studio/data/projects/project-detail-query.ts @@ -1,5 +1,4 @@ -import { QueryClient, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -import { useCallback } from 'react' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, isResponseOk } from 'lib/common/fetch' import { API_URL } from 'lib/constants' From b34146d7c451f4ff241285cbf6ce6763e461192f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 14:56:43 +0800 Subject: [PATCH 095/117] Revert changes in passwordStrengthCheck --- studio/lib/helpers.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index a03468b00de..815e16a0bfc 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -1,4 +1,3 @@ -import { checkPasswordStrength } from 'data/profile/password-check-mutation' import { post } from 'lib/common/fetch' import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH, PASSWORD_STRENGTH } from 'lib/constants' import { toast } from 'react-hot-toast' @@ -190,8 +189,10 @@ export async function passwordStrength(value: string) { message = `${PASSWORD_STRENGTH[0]} Maximum length of password exceeded` warning = `Password should be less than 100 characters` } else { - try { - const { result } = await checkPasswordStrength({ password: value }) + // [Joshen] Unable to use RQ atm due to our Jest tests being in JS + const response = await post(`${API_URL}/profile/password-check`, { password: value }) + if (!response.error) { + const { result } = response const resultScore = result?.score ?? 0 const score = (PASSWORD_STRENGTH as any)[resultScore] @@ -208,8 +209,8 @@ export async function passwordStrength(value: string) { result?.feedback?.warning ? result?.feedback?.warning + '.' : '' } You need a stronger password.` } - } catch (error: any) { - toast.error(`Failed to check password strength: ${error.message}`) + } else { + toast.error(`Failed to check password strength: ${response.error.message}`) } } } From b936fa12b58b40007d2c193d8eed982cacd465a9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 15:12:59 +0800 Subject: [PATCH 096/117] Add loading state when enabling hooks --- studio/components/to-be-cleaned/ProductEmptyState.tsx | 9 ++++++++- studio/pages/project/[ref]/database/hooks/index.tsx | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/studio/components/to-be-cleaned/ProductEmptyState.tsx b/studio/components/to-be-cleaned/ProductEmptyState.tsx index 09eaff00b57..ab85f4820cb 100644 --- a/studio/components/to-be-cleaned/ProductEmptyState.tsx +++ b/studio/components/to-be-cleaned/ProductEmptyState.tsx @@ -9,6 +9,7 @@ interface ProductEmptyStateProps { infoButtonLabel?: string infoButtonUrl?: string onClickCta?: () => void + loading?: boolean disabled?: boolean disabledMessage?: string } @@ -21,6 +22,7 @@ const ProductEmptyState = ({ infoButtonLabel = '', infoButtonUrl = '', onClickCta = () => {}, + loading = false, disabled = false, disabledMessage = '', }: PropsWithChildren) => { @@ -39,7 +41,12 @@ const ProductEmptyState = ({ {ctaButtonLabel && onClickCta && ( - diff --git a/studio/pages/project/[ref]/database/hooks/index.tsx b/studio/pages/project/[ref]/database/hooks/index.tsx index 71f7f6bdf0d..86ccc236c4b 100644 --- a/studio/pages/project/[ref]/database/hooks/index.tsx +++ b/studio/pages/project/[ref]/database/hooks/index.tsx @@ -30,7 +30,7 @@ const HooksPage: NextPageWithLayout = () => { const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') - const { mutate: enableHooks } = useHooksEnableMutation({ + const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({ onSuccess: () => { meta.schemas.load() ui.setNotification({ @@ -85,8 +85,11 @@ const HooksPage: NextPageWithLayout = () => { title="Database Webhooks" ctaButtonLabel="Enable webhooks" onClickCta={() => enableHooksForProject()} - disabled={!canCreateWebhooks} - disabledMessage="You need additional permissions to enable webhooks" + loading={isEnablingHooks} + disabled={isEnablingHooks || !canCreateWebhooks} + disabledMessage={ + !canCreateWebhooks ? 'You need additional permissions to enable webhooks' : undefined + } >

Database Webhooks can be used to trigger serverless functions or send requests to an From 20ca3f33b52752ac59e184ab20e3cf2b3eb4c53e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 16:16:14 +0800 Subject: [PATCH 097/117] Address comments --- .../interfaces/App/AppBannerWrapper.tsx | 15 ++++++++++++++- .../Database/Indexes/CreateIndexSidePanel.tsx | 14 +++++++++----- .../interfaces/Database/Indexes/Indexes.tsx | 6 ++++-- .../NavigationBar/NavigationBar.tsx | 2 +- studio/components/ui/CodeEditor/CodeEditor.tsx | 7 ++++--- .../ui/CodeEditor/CodeEditor.utils.ts | 17 +++++++++-------- studio/hooks/misc/useStore.tsx | 12 ------------ studio/pages/project/[ref]/sql/[id].tsx | 17 ----------------- 8 files changed, 41 insertions(+), 49 deletions(-) diff --git a/studio/components/interfaces/App/AppBannerWrapper.tsx b/studio/components/interfaces/App/AppBannerWrapper.tsx index 581ca0c6669..65f203d560e 100644 --- a/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -1,9 +1,22 @@ +import { useMonaco } from '@monaco-editor/react' +import { useTheme } from 'common' import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner' +import { getTheme } from 'components/ui/CodeEditor' import { useFlag } from 'hooks' -import { PropsWithChildren } from 'react' +import { PropsWithChildren, useEffect } from 'react' const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { + const monaco = useMonaco() + const { isDarkMode } = useTheme() const ongoingIncident = useFlag('ongoingIncident') + + useEffect(() => { + if (monaco) { + const theme: any = getTheme(isDarkMode) + monaco.editor.defineTheme('supabase', theme) + } + }, [isDarkMode, monaco]) + return (

{ongoingIncident && } diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 7fff0a15a28..1662ee4ab0d 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -57,12 +57,12 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = }) const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ - onSuccess() { - refetchIndexes() + onSuccess: async () => { + await refetchIndexes() onClose() ui.setNotification({ category: 'success', message: `Successfully created index` }) }, - onError(error) { + onError: (error) => { ui.setNotification({ error, category: 'error', @@ -75,7 +75,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) = () => entities?.pages.flatMap((page) => page.data.entities) || [], [entities?.pages] ) - const columns = tableColumns?.result[0].columns ?? [] + const columns = tableColumns?.result[0]?.columns ?? [] const columnOptions: MultiSelectOption[] = columns.map((column) => { return { id: column.attname, value: column.attname, name: column.attname, disabled: false } }) @@ -144,7 +144,11 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp {entityTypes.length === 0 ? (

Select a table

- +
) : ( { const { ui } = useStore() @@ -184,7 +185,7 @@ const Indexes = () => { <> {sortedIndexes.length === 0 && search.length === 0 && ( - +

No indexes created yet

There are no indexes found in the schema "{selectedSchema}" @@ -194,7 +195,7 @@ const Indexes = () => { )} {sortedIndexes.length === 0 && search.length > 0 && ( - +

No results found

Your search for "{search}" did not return any results @@ -236,6 +237,7 @@ const Indexes = () => {

diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index a20b2bd4c40..ad43e9c951d 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -26,7 +26,7 @@ import { } from './NavigationBar.utils' import NavigationIconButton from './NavigationIconButton' -const NavigationBar = ({}) => { +const NavigationBar = () => { const router = useRouter() const { isDarkMode, toggleTheme } = useTheme() const { ref: projectRef } = useParams() diff --git a/studio/components/ui/CodeEditor/CodeEditor.tsx b/studio/components/ui/CodeEditor/CodeEditor.tsx index 080742477c0..6dd28561652 100644 --- a/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -5,6 +5,7 @@ import { useRef } from 'react' import { timeout } from 'lib/helpers' import Connecting from '../Loading' import { alignEditor } from './CodeEditor.utils' +import { cn } from 'ui' interface CodeEditorProps { id: string @@ -38,6 +39,7 @@ const CodeEditor = ({ const editorRef = useRef() const onMount = async (editor: any, monaco: any) => { + editorRef.current = editor alignEditor(editor) editor.addAction({ @@ -56,7 +58,6 @@ const CodeEditor = ({ await timeout(500) if (autofocus) editor?.focus() - editorRef.current = editor } const optionsMerged = merge( @@ -80,10 +81,10 @@ const CodeEditor = ({ return ( } diff --git a/studio/components/ui/CodeEditor/CodeEditor.utils.ts b/studio/components/ui/CodeEditor/CodeEditor.utils.ts index 8f34d2d344f..3cf3c882200 100644 --- a/studio/components/ui/CodeEditor/CodeEditor.utils.ts +++ b/studio/components/ui/CodeEditor/CodeEditor.utils.ts @@ -12,18 +12,19 @@ export const alignEditor = (editor: any) => { export const getTheme = (isDarkTheme: boolean) => { // [TODO] Probably need better theming for light mode return { - base: 'vs-dark', // can also be vs-dark or hc-black + base: isDarkTheme ? 'vs-dark' : 'vs', // can also be vs-dark or hc-black inherit: true, // can also be false to completely replace the builtin rules - colors: { - 'editor.background': isDarkTheme ? '#1F1F1F' : '#FFFFFF', - }, rules: [ - { background: isDarkTheme ? '1F1F1F' : 'FFFFFF' }, - { token: '', foreground: isDarkTheme ? 'D4D4D4' : '444444' }, - { token: 'string.sql', foreground: '24B47E' }, + { background: isDarkTheme ? '1f1f1f' : 'f0f0f0' }, + { + token: '', + background: isDarkTheme ? '1f1f1f' : 'f0f0f0', + foreground: isDarkTheme ? 'd4d4d4' : '444444', + }, + { token: 'string.sql', foreground: '24b47e' }, { token: 'comment', foreground: '666666' }, { token: 'predefined.sql', foreground: isDarkTheme ? 'D4D4D4' : '444444' }, - // { token: '', foreground: 'ffcc00' }, // Trying to figure out how to change the border color of the row selected ], + colors: { 'editor.background': isDarkTheme ? '#1f1f1f' : '#f0f0f0' }, } } diff --git a/studio/hooks/misc/useStore.tsx b/studio/hooks/misc/useStore.tsx index 840ebc3ffb5..7abc9c6043a 100644 --- a/studio/hooks/misc/useStore.tsx +++ b/studio/hooks/misc/useStore.tsx @@ -1,10 +1,7 @@ -import { useMonaco } from '@monaco-editor/react' -import { useTheme } from 'common' import { autorun } from 'mobx' import { createContext, PropsWithChildren, useContext, useEffect } from 'react' import toast from 'react-hot-toast' -import { getTheme } from 'components/ui/CodeEditor' import SparkBar from 'components/ui/SparkBar' import { IRootStore } from 'stores' @@ -23,16 +20,7 @@ interface StoreProvider { rootStore: IRootStore } export const StoreProvider = ({ children, rootStore }: PropsWithChildren) => { - const monaco = useMonaco() const { ui } = rootStore - const { isDarkMode } = useTheme() - - useEffect(() => { - if (monaco) { - const theme: any = getTheme(isDarkMode) - monaco.editor.defineTheme('supabase', theme) - } - }, [isDarkMode, monaco]) useEffect(() => { autorun(() => { diff --git a/studio/pages/project/[ref]/sql/[id].tsx b/studio/pages/project/[ref]/sql/[id].tsx index 1afc3c4ee53..9cf3c7b1225 100644 --- a/studio/pages/project/[ref]/sql/[id].tsx +++ b/studio/pages/project/[ref]/sql/[id].tsx @@ -1,5 +1,4 @@ import { useMonaco } from '@monaco-editor/react' -import { useTheme } from 'common' import { observer } from 'mobx-react-lite' import { useEffect, useRef } from 'react' @@ -18,7 +17,6 @@ import getPgsqlSignatureHelpProvider from 'components/ui/CodeEditor/Providers/Pg const SqlEditor: NextPageWithLayout = () => { const monaco = useMonaco() - const { isDarkMode } = useTheme() const { project } = useProjectContext() const { mutateAsync: formatQuery } = useFormatQueryMutation() @@ -70,21 +68,6 @@ const SqlEditor: NextPageWithLayout = () => { pgInfoRef.current.functions = functions?.result } - useEffect(() => { - if (monaco) { - monaco.editor.defineTheme('supabase', { - base: isDarkMode ? 'vs-dark' : 'vs', - inherit: true, - rules: [ - { token: '', background: isDarkMode ? '1f1f1f' : 'f0f0f0' }, - { token: 'string.sql', foreground: '24b47e' }, - { token: 'comment', foreground: '666666' }, - ], - colors: { 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0' }, - }) - } - }, [monaco, isDarkMode]) - // Enable pgsql format useEffect(() => { if (monaco) { From 6124425e17f45a1cd2da403a029a5ffc38e2b49c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 16:19:31 +0800 Subject: [PATCH 098/117] Remove and unify monaco editor define theming --- studio/components/grid/SupabaseGrid.tsx | 49 ++++++------------- .../ColumnEditor/ColumnEditor.tsx | 26 +++++----- .../RowEditor/JsonEditor/JsonCodeEditor.tsx | 33 ++----------- studio/components/ui/SqlEditor.tsx | 15 +----- 4 files changed, 35 insertions(+), 88 deletions(-) diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 94e65bfa996..373ee642756 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -1,52 +1,31 @@ -import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' -import { createPortal } from 'react-dom' +import { DataGridHandle } from '@supabase/react-data-grid' +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react' import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' -import { useMonaco } from '@monaco-editor/react' -import { DataGridHandle } from '@supabase/react-data-grid' +import { createPortal } from 'react-dom' -import { useUrlState } from 'hooks' -import { Dictionary, SupabaseGridProps, SupabaseGridRef } from './types' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' -import { StoreProvider, useDispatch, useTrackedState } from './store' -import { Shortcuts } from './components/common' -import { Grid } from './components/grid' -import Header from './components/header' -import Footer from './components/footer' -import { RowContextMenu } from './components/menu' +import { useUrlState } from 'hooks' import { cleanupProps, - initTable, - saveStorageDebounced, formatFilterURLParams, formatSortURLParams, + initTable, + saveStorageDebounced, } from './SupabaseGrid.utils' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { Shortcuts } from './components/common' +import Footer from './components/footer' +import { Grid } from './components/grid' +import Header from './components/header' +import { RowContextMenu } from './components/menu' +import { StoreProvider, useDispatch, useTrackedState } from './store' +import { Dictionary, SupabaseGridProps, SupabaseGridRef } from './types' /** Supabase Grid: React component to render database table */ export const SupabaseGrid = forwardRef((props, ref) => { - const monaco = useMonaco() const _props = cleanupProps(props) - const { theme } = _props - - useEffect(() => { - if (monaco) { - const darkTheme = theme && theme === 'dark' ? true : false - monaco.editor.defineTheme('supabase', { - base: darkTheme ? 'vs-dark' : 'vs', - inherit: true, // can also be false to completely replace the builtin rules - rules: [ - { token: 'string.sql', foreground: '24b47e' }, - { token: 'comment', foreground: '666666' }, - { token: 'predefined.sql', foreground: '999999' }, - ], - colors: { - 'editor.background': darkTheme ? '#222222' : '#fbfcfd', - }, - }) - } - }, [monaco, theme]) return ( diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index c98ba5e41fa..df5362b0cb7 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -243,18 +243,20 @@ const ColumnEditor = ({ - - +
+ + + +
} > Data Type diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx index 2c76081f737..0dfdda0a335 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx @@ -1,7 +1,8 @@ -import Editor, { useMonaco } from '@monaco-editor/react' -import { useTheme } from 'common' +import Editor from '@monaco-editor/react' import { noop } from 'lodash' -import { useEffect, useRef } from 'react' +import { useRef } from 'react' + +// [Joshen] Should just use CodeEditor instead of declaring Editor here so that all the mount logic is consistent interface JsonEditorProps { queryId?: string @@ -16,31 +17,7 @@ const JsonEditor = ({ readOnly = false, onInputChange = noop, }: JsonEditorProps) => { - const monaco = useMonaco() const editorRef = useRef() - const { isDarkMode } = useTheme() - - useEffect(() => { - if (monaco) { - // Supabase theming (Can't seem to get it to work for now) - monaco.editor.defineTheme('supabase', { - base: 'vs-dark', // can also be vs-dark or hc-black - inherit: true, // can also be false to completely replace the builtin rules - rules: [ - // @ts-ignore - { background: isDarkMode ? '1f1f1f' : '30313f' }, - { token: 'string.sql', foreground: '24b47e' }, - { token: 'comment', foreground: '666666' }, - { token: 'predefined.sql', foreground: 'D4D4D4' }, - ], - colors: { - 'editor.background': isDarkMode ? '#1f1f1f' : '#30313f', - // 'editorGutter.background': '#30313f', - // 'editorLineNumber.foreground': '#555671', - }, - }) - } - }, [isDarkMode, monaco]) const onMount = (editor: any, monaco: any) => { editorRef.current = editor @@ -60,7 +37,7 @@ const JsonEditor = ({ return ( { if (monaco) { - // Supabase theming (Can't seem to get it to work for now) - monaco.editor.defineTheme('supabase', { - base: 'vs-dark', - inherit: true, - colors: {}, - rules: [ - { token: '', background: '4a5568' }, - { token: 'string.sql', foreground: '24b47e' }, - { token: 'comment', foreground: '666666' }, - { token: 'predefined.sql', foreground: 'D4D4D4' }, - ], - }) - // Enable pgsql format const formatprovider = monaco.languages.registerDocumentFormattingEditProvider('pgsql', { async provideDocumentFormattingEdits(model: any) { From 7cc2588e340e821285ab90ae0ea3e8485f4d9b39 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 16:24:45 +0800 Subject: [PATCH 099/117] Improve description on b-tree index --- .../interfaces/Database/Indexes/CreateIndexSidePanel.tsx | 4 +++- .../interfaces/Database/Indexes/Indexes.constants.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 1662ee4ab0d..ed43d312130 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -198,7 +198,9 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp {INDEX_TYPES.map((index) => (

{index.name}

-

{index.description}

+ {index.description.split('\n').map((x, idx) => ( +

{x}

+ ))}
))} diff --git a/studio/components/interfaces/Database/Indexes/Indexes.constants.ts b/studio/components/interfaces/Database/Indexes/Indexes.constants.ts index 2793de20221..a8b922775b8 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.constants.ts +++ b/studio/components/interfaces/Database/Indexes/Indexes.constants.ts @@ -2,7 +2,8 @@ export const INDEX_TYPES = [ { name: 'B-Tree', value: 'btree', - description: 'For querying data with equality or range conditions on columns.', + description: + 'For querying data with equality or range conditions on columns.\nThis is the default index type that Postgres uses.', }, { name: 'Hash', From d107b26d97fd161947ea0f52e745d99cdd0fd2e4 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Aug 2023 17:04:12 +0800 Subject: [PATCH 100/117] Update query Key for table columns query --- studio/data/database/table-columns-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/data/database/table-columns-query.ts b/studio/data/database/table-columns-query.ts index b0d898eb965..e4d7f9a5c46 100644 --- a/studio/data/database/table-columns-query.ts +++ b/studio/data/database/table-columns-query.ts @@ -92,7 +92,7 @@ export const useTableColumnsQuery = Date: Mon, 21 Aug 2023 13:06:38 +0300 Subject: [PATCH 101/117] prettier on customer stories --- apps/www/_customers/berriai.mdx | 6 +++--- apps/www/_customers/mendableai.mdx | 6 +++--- apps/www/public/customers-rss.xml | 28 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/www/_customers/berriai.mdx b/apps/www/_customers/berriai.mdx index fd02b657b81..34031d9a18f 100644 --- a/apps/www/_customers/berriai.mdx +++ b/apps/www/_customers/berriai.mdx @@ -53,9 +53,9 @@ Berri AI migrated their vector database from AWS RDS to Supabase for several com The migration to [Supabase Vector](https://supabase.com/vector), which facilitated efficient storage and retrieval of vectors, proved to be a significant advantage for Berri AI's operations. - Switching to Supabase Vector was a game-changer for us. The faster view times allowed me to quickly - access and debug data, enhancing my ability to analyze individual customer issues. The no-code - option to add new columns on the fly was fantastic, enabling us to adapt swiftly. + Switching to Supabase Vector was a game-changer for us. The faster view times allowed me to + quickly access and debug data, enhancing my ability to analyze individual customer issues. The + no-code option to add new columns on the fly was fantastic, enabling us to adapt swiftly. ## What They Built diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index bc4ebf979d8..73d2523387b 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -56,15 +56,15 @@ Thanks to Supabase Vector, Mendable was able to significantly improve the effici We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for - all the ways we need to use it, Supabase has been just as performant - if not more performant - than - the other vector databases. + all the ways we need to use it, Supabase has been just as performant - if not more performant - + than the other vector databases. To learn more about how Supabase Vector can help you store vector embeddings at scale and build AI apps with ease, [reach out to us](https://forms.supabase.com/enterprise). ## Tech stack -- React +- React - Vercel - Next.js - Express diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index bbb538fb5db..5d6c9be00a3 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,63 +5,63 @@ https://supabase.com Latest news from Supabase en - Mon, 05 Jun 2023 22:00:00 GMT + Mon, 05 Jun 2023 21:00:00 GMT https://supabase.com/customers/berriai - Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector + Berri AI boosts productivity by migrating from AWS RDS to Supabase Vector https://supabase.com/customers/berriai Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase. - Mon, 05 Jun 2023 22:00:00 GMT + Mon, 05 Jun 2023 21:00:00 GMT https://supabase.com/customers/markprompt - Markprompt and Supabase - GDPR-Compliant AI Chatbots for Docs and Websites. + Markprompt and Supabase - GDPR-compliant AI chatbots for docs and websites. https://supabase.com/customers/markprompt - AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions. - Tue, 16 May 2023 22:00:00 GMT + AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused vector database and authentication solutions. + Tue, 16 May 2023 21:00:00 GMT https://supabase.com/customers/mendableai - Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings. + Mendable switches from Pinecone to Supabase Vector for PostgreSQL vector embeddings. https://supabase.com/customers/mendableai - How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector. - Thu, 04 May 2023 22:00:00 GMT + How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase Vector. + Thu, 04 May 2023 21:00:00 GMT https://supabase.com/customers/happyteams HappyTeams unlocks better performance and reduces cost with Supabase. https://supabase.com/customers/happyteams How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. - Wed, 15 Feb 2023 23:00:00 GMT + Wed, 15 Feb 2023 22:00:00 GMT https://supabase.com/customers/xendit Xendit use Supabase and create a full solution shipped to production in less than one week. https://supabase.com/customers/xendit As a payment processor, Xendit are responsible for verifying that all transactions are legal. - Mon, 13 Feb 2023 23:00:00 GMT + Mon, 13 Feb 2023 22:00:00 GMT https://supabase.com/customers/replenysh Replenysh uses Supabase to implement OTP in less than 24 hours. https://supabase.com/customers/replenysh With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres. - Mon, 13 Feb 2023 23:00:00 GMT + Mon, 13 Feb 2023 22:00:00 GMT https://supabase.com/customers/mobbin How Mobbin migrated 200,000 users from Firebase for a better authentication experience. https://supabase.com/customers/mobbin Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps. - Mon, 13 Feb 2023 23:00:00 GMT + Mon, 13 Feb 2023 22:00:00 GMT https://supabase.com/customers/epsilon3 Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations. https://supabase.com/customers/epsilon3 Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions. - Mon, 13 Feb 2023 23:00:00 GMT + Mon, 13 Feb 2023 22:00:00 GMT From f73d2839a27c13ee4d4d6ed9a66353e2f898a319 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 8 Aug 2023 13:48:38 +0200 Subject: [PATCH 102/117] The app included two versions of the "supabase/gotrue-js" library. This may cause issues so one of them (the older one) is removed. --- package-lock.json | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 089f7ba903e..cb5ae610fc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14121,8 +14121,9 @@ } }, "node_modules/@supabase/gotrue-js": { - "version": "2.27.2", - "license": "MIT", + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.47.0.tgz", + "integrity": "sha512-3e34/vsKH/DoSZCpB85UZpFWSJ2p4GRUUlqgAgeTPagPlx4xS+Nc5v7g7ic7vp3gK0J5PsYVCn9Qu2JQUp4vXg==", "dependencies": { "cross-fetch": "^3.1.5" } @@ -40764,14 +40765,6 @@ "typescript": "^5.0.4" } }, - "packages/common/node_modules/@supabase/gotrue-js": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.47.0.tgz", - "integrity": "sha512-3e34/vsKH/DoSZCpB85UZpFWSJ2p4GRUUlqgAgeTPagPlx4xS+Nc5v7g7ic7vp3gK0J5PsYVCn9Qu2JQUp4vXg==", - "dependencies": { - "cross-fetch": "^3.1.5" - } - }, "packages/common/node_modules/@types/react": { "version": "17.0.50", "license": "MIT", From 661a4b597f43c0afd5a7ad78fdfdef832dca8838 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:35:55 +0000 Subject: [PATCH 103/117] Bump certifi from 2023.5.7 to 2023.7.22 in /examples/ai/image_search Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] --- examples/ai/image_search/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ai/image_search/poetry.lock b/examples/ai/image_search/poetry.lock index 5041be77551..ca16827c247 100644 --- a/examples/ai/image_search/poetry.lock +++ b/examples/ai/image_search/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] From 9686c3462a547bc54634eb6b1834e03840bac873 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 21 Aug 2023 16:44:02 +0300 Subject: [PATCH 104/117] scroll page when navigating to archive launch weeks --- .../7/LaunchSection/TicketBrickWall.tsx | 2 +- .../components/LaunchWeek/8/LW8Meetups.tsx | 7 +- .../www/components/LaunchWeek/8/LWArchive.tsx | 72 +++++++++++-------- .../LaunchWeek/8/Releases/index.tsx | 4 +- apps/www/components/Nav/index.tsx | 2 +- .../ui/src/layout/banners/LWAnnouncement.tsx | 2 +- .../src/layout/banners/data/Announcement.json | 2 +- 7 files changed, 50 insertions(+), 41 deletions(-) diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/TicketBrickWall.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/TicketBrickWall.tsx index ea7922d61f4..aadbdc5ca35 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/TicketBrickWall.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/TicketBrickWall.tsx @@ -35,7 +35,7 @@ export default function TicketBrickWall({ users }: Props) { {isMobile && }
- + +
) diff --git a/apps/www/components/LaunchWeek/8/Releases/index.tsx b/apps/www/components/LaunchWeek/8/Releases/index.tsx index 6a5405b2e3e..db94c66a6f5 100644 --- a/apps/www/components/LaunchWeek/8/Releases/index.tsx +++ b/apps/www/components/LaunchWeek/8/Releases/index.tsx @@ -369,7 +369,7 @@ export default function LW8Releases() {
)}