From 3718608cf034eb09f93edc80dd406c0feb262f7c Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Tue, 4 Mar 2025 09:58:25 +1000 Subject: [PATCH] Update authentication advanced page (#33955) * init PrePostTab component * init * support auto fixing rounded of inputs/select/textarea * remove form item layout prepost * Update FormLayout.tsx * Update FormLayout.tsx * Update FormLayout.tsx * form layout and advanced * Update advanced.tsx * protection and hooks * fix * parse --------- Co-authored-by: Jonathan Summers-Muir --- .../Auth/AdvancedAuthSettingsForm.tsx | 262 +++++++++------ .../interfaces/Auth/Hooks/HooksListing.tsx | 10 +- .../ProtectionAuthSettingsForm.tsx | 308 ++++++++++-------- .../pages/project/[ref]/auth/advanced.tsx | 66 ++-- .../studio/pages/project/[ref]/auth/hooks.tsx | 61 ++-- .../pages/project/[ref]/auth/protection.tsx | 74 +++-- .../ui-patterns/form/Layout/FormLayout.tsx | 4 +- packages/ui/index.tsx | 2 + .../src/components/PrePostTab/PrePostTab.tsx | 36 ++ .../ui/src/components/PrePostTab/index.tsx | 2 + 10 files changed, 513 insertions(+), 312 deletions(-) create mode 100644 packages/ui/src/components/PrePostTab/PrePostTab.tsx create mode 100644 packages/ui/src/components/PrePostTab/index.tsx diff --git a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx index 20381650218..dbbb01898b8 100644 --- a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx @@ -1,15 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect, useMemo } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' import { useParams } from 'common' -import { StringNumberOrNull } from 'components/ui/Forms/Form.constants' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -22,15 +19,19 @@ import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, + Button, + Card, + CardContent, + CardFooter, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Input_Shadcn_, + PrePostTab, WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -const formId = 'auth-config-advanced-form' +import { StringNumberOrNull } from 'components/ui/Forms/Form.constants' const FormSchema = z.object({ API_MAX_REQUEST_DURATION: z.coerce @@ -46,69 +47,110 @@ export const AdvancedAuthSettingsForm = () => { const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false) + const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false) + const { data: authConfig, error: authConfigError, isLoading, isError, - isSuccess, } = useAuthConfigQuery({ projectRef }) const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug, }) - const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - - const defaultValues = useMemo( - () => ({ - API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION ?? undefined, - DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE ?? '', - }), - [authConfig] - ) - - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: defaultValues as any, - }) - const { isDirty } = form.formState + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const isTeamsEnterprisePlan = - isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro' + isSuccessSubscription && subscription?.plan.id !== 'free' && subscription?.plan.id !== 'pro' const promptTeamsEnterpriseUpgrade = IS_PLATFORM && isSuccessSubscription && !isTeamsEnterprisePlan - const resetForm = () => { - form.reset(defaultValues as any) - } + const requestDurationForm = useForm({ + resolver: zodResolver( + z.object({ + API_MAX_REQUEST_DURATION: FormSchema.shape.API_MAX_REQUEST_DURATION, + }) + ), + defaultValues: { + API_MAX_REQUEST_DURATION: 10, + }, + }) - const onSubmit: SubmitHandler> = (data) => { - if (!projectRef) return console.error('Project ref is required') + const databaseForm = useForm({ + resolver: zodResolver( + z.object({ + DB_MAX_POOL_SIZE: FormSchema.shape.DB_MAX_POOL_SIZE, + }) + ), + defaultValues: { + DB_MAX_POOL_SIZE: '', + }, + }) - const { API_MAX_REQUEST_DURATION, DB_MAX_POOL_SIZE, ...config } = data - const payload = { - ...config, - ...(isTeamsEnterprisePlan ? { API_MAX_REQUEST_DURATION } : {}), - DB_MAX_POOL_SIZE, + useEffect(() => { + if (authConfig) { + if (!isUpdatingRequestDurationForm) { + requestDurationForm.reset({ + API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION ?? 10, + }) + } + + if (!isUpdatingDatabaseForm) { + databaseForm.reset({ + DB_MAX_POOL_SIZE: + authConfig?.DB_MAX_POOL_SIZE !== null ? String(authConfig?.DB_MAX_POOL_SIZE) : '', + }) + } } + }, [authConfig, isUpdatingRequestDurationForm, isUpdatingDatabaseForm]) + + const onSubmitRequestDurationForm = (values: any) => { + if (!projectRef) return console.error('Project ref is required') + if (!isTeamsEnterprisePlan) return + + setIsUpdatingRequestDurationForm(true) updateAuthConfig( - // @ts-expect-error - { projectRef: projectRef, config: payload }, + { projectRef: projectRef, config: values }, { + onError: (error) => { + toast.error(`Failed to update request duration settings: ${error?.message}`) + setIsUpdatingRequestDurationForm(false) + }, onSuccess: () => { - toast.success('Successfully updated settings') - form.reset({ ...defaultValues, ...data }) + toast.success('Successfully updated request duration settings') + setIsUpdatingRequestDurationForm(false) }, } ) } - useEffect(() => { - // reset form with the current values - if (isSuccess) resetForm() - }, [defaultValues, isSuccess]) + const onSubmitDatabaseForm = (values: any) => { + if (!projectRef) return console.error('Project ref is required') + + setIsUpdatingDatabaseForm(true) + + const config = { + DB_MAX_POOL_SIZE: values.DB_MAX_POOL_SIZE, + } + + updateAuthConfig( + { projectRef: projectRef, config }, + { + onError: (error) => { + toast.error(`Failed to update database connection settings: ${error?.message}`) + setIsUpdatingDatabaseForm(false) + }, + onSuccess: () => { + toast.success('Successfully updated database connection settings') + setIsUpdatingDatabaseForm(false) + }, + } + ) + } if (isError) { return ( @@ -127,88 +169,126 @@ export const AdvancedAuthSettingsForm = () => { return ( <> {promptTeamsEnterpriseUpgrade && ( - +
+ +
)} - -
- - resetForm()} - disabled={!canUpdateConfig} - helper={ - !canUpdateConfig - ? 'You need additional permissions to update authentication settings' - : undefined - } - /> - - } + + Request Duration + + + - Max Request Duration}> - + + (
- - - seconds - + + +
)} /> -
-
+ - Auth Database Connections}> - + + {requestDurationForm.formState.isDirty && ( + + )} + + + + +
+
+ + + Auth Database Connections + + +
+ + ( )} /> - - - - -
+ + + + {databaseForm.formState.isDirty && ( + + )} + + + + +
+ ) } diff --git a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx index f005bb73987..6b2937894e0 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx @@ -16,6 +16,7 @@ import { CreateHookSheet } from './CreateHookSheet' import { HookCard } from './HookCard' import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants' import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' export const HooksListing = () => { const { ref: projectRef } = useParams() @@ -68,8 +69,11 @@ export const HooksListing = () => { } return ( -
- } /> + +
+ All hooks + +
{hooks.filter((h) => isValidHook(h)).length === 0 && (
{ )}
-
+ ) } diff --git a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx index ce1aad4abbc..a0e3fe248f7 100644 --- a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx @@ -1,30 +1,40 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Eye, EyeOff } from 'lucide-react' import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { boolean, number, object, string } from 'yup' +import { yupResolver } from '@hookform/resolvers/yup' import { useParams } from 'common' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, - Form, - Input, - Toggle, + Card, + CardContent, + CardFooter, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Input_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Switch, WarningIcon, + PrePostTab, } from 'ui' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' -import FormField from '../AuthProvidersForm/FormField' const schema = object({ DISABLE_SIGNUP: boolean().required(), @@ -51,8 +61,6 @@ const schema = object({ PASSWORD_HIBP_ENABLED: boolean(), }) -const formId = 'auth-config-protection-settings' - const ProtectionAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { @@ -60,32 +68,57 @@ const ProtectionAuthSettingsForm = () => { error: authConfigError, isLoading, isError, - isSuccess, } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - + const [isUpdatingProtection, setIsUpdatingProtection] = useState(false) const [hidden, setHidden] = useState(true) + const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') - const INITIAL_VALUES = { - DISABLE_SIGNUP: !authConfig?.DISABLE_SIGNUP, - EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED, - SECURITY_MANUAL_LINKING_ENABLED: authConfig?.SECURITY_MANUAL_LINKING_ENABLED || false, - SITE_URL: authConfig?.SITE_URL, - SECURITY_CAPTCHA_ENABLED: authConfig?.SECURITY_CAPTCHA_ENABLED || false, - SECURITY_CAPTCHA_SECRET: authConfig?.SECURITY_CAPTCHA_SECRET || '', - SECURITY_CAPTCHA_PROVIDER: authConfig?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', - SESSIONS_TIMEBOX: authConfig?.SESSIONS_TIMEBOX || 0, - SESSIONS_INACTIVITY_TIMEOUT: authConfig?.SESSIONS_INACTIVITY_TIMEOUT || 0, - SESSIONS_SINGLE_PER_USER: authConfig?.SESSIONS_SINGLE_PER_USER || false, - PASSWORD_MIN_LENGTH: authConfig?.PASSWORD_MIN_LENGTH || 6, - PASSWORD_REQUIRED_CHARACTERS: - authConfig?.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS, - PASSWORD_HIBP_ENABLED: authConfig?.PASSWORD_HIBP_ENABLED || false, - } + const protectionForm = useForm({ + resolver: yupResolver(schema), + defaultValues: { + DISABLE_SIGNUP: true, + EXTERNAL_ANONYMOUS_USERS_ENABLED: false, + SECURITY_MANUAL_LINKING_ENABLED: false, + SITE_URL: '', + SECURITY_CAPTCHA_ENABLED: false, + SECURITY_CAPTCHA_SECRET: '', + SECURITY_CAPTCHA_PROVIDER: 'hcaptcha', + SESSIONS_TIMEBOX: 0, + SESSIONS_INACTIVITY_TIMEOUT: 0, + SESSIONS_SINGLE_PER_USER: false, + PASSWORD_MIN_LENGTH: 6, + PASSWORD_REQUIRED_CHARACTERS: NO_REQUIRED_CHARACTERS, + PASSWORD_HIBP_ENABLED: false, + }, + }) + + useEffect(() => { + if (authConfig && !isUpdatingProtection) { + protectionForm.reset({ + DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP, + EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED || false, + SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED || false, + SITE_URL: authConfig.SITE_URL || '', + SECURITY_CAPTCHA_ENABLED: authConfig.SECURITY_CAPTCHA_ENABLED || false, + SECURITY_CAPTCHA_SECRET: authConfig.SECURITY_CAPTCHA_SECRET || '', + SECURITY_CAPTCHA_PROVIDER: authConfig.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha', + SESSIONS_TIMEBOX: authConfig.SESSIONS_TIMEBOX || 0, + SESSIONS_INACTIVITY_TIMEOUT: authConfig.SESSIONS_INACTIVITY_TIMEOUT || 0, + SESSIONS_SINGLE_PER_USER: authConfig.SESSIONS_SINGLE_PER_USER || false, + PASSWORD_MIN_LENGTH: authConfig.PASSWORD_MIN_LENGTH || 6, + PASSWORD_REQUIRED_CHARACTERS: + authConfig.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS, + PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false, + }) + } + }, [authConfig, isUpdatingProtection]) + + const onSubmitProtection = (values: any) => { + setIsUpdatingProtection(true) - const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP // The backend uses empty string to represent no required characters in the password @@ -97,11 +130,12 @@ const ProtectionAuthSettingsForm = () => { { projectRef: projectRef!, config: payload }, { onError: (error) => { - toast.error(`Failed to update settings: ${error?.message}`) + toast.error(`Failed to update settings: ${error?.message}`) + setIsUpdatingProtection(false) }, onSuccess: () => { - toast.success(`Successfully updated settings`) - resetForm({ values: values, initialValues: values }) + toast.success('Successfully updated settings') + setIsUpdatingProtection(false) }, } ) @@ -122,106 +156,122 @@ const ProtectionAuthSettingsForm = () => { } return ( -
- + + Bot and Abuse Protection -
- {({ handleReset, resetForm, values, initialValues, setFieldValue }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - // Form is reset once remote data is loaded in store - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess) resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [isSuccess]) + + + + + ( + + + + + + )} + /> + - return ( - <> - - -
- } - > - Bot and Abuse Protection}> - - - {values.SECURITY_CAPTCHA_ENABLED && ( - <> - - : } - type="default" - onClick={() => setHidden(!hidden)} - /> - } - /> - + {protectionForm.watch('SECURITY_CAPTCHA_ENABLED') && ( + <> + + ( + + + + + + + + hCaptcha + + Turnstile by Cloudflare + + + + + )} - - - - - ) - }} - - + /> + + + + ( + + +
+ setHidden(!hidden)} + icon={hidden ? : } + /> + } + > + + +
+
+
+ )} + /> +
+ + )} + + + {protectionForm.formState.isDirty && ( + + )} + + + + + + ) } diff --git a/apps/studio/pages/project/[ref]/auth/advanced.tsx b/apps/studio/pages/project/[ref]/auth/advanced.tsx index 5086974932a..f6a0ef0340f 100644 --- a/apps/studio/pages/project/[ref]/auth/advanced.tsx +++ b/apps/studio/pages/project/[ref]/auth/advanced.tsx @@ -1,44 +1,56 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ReactNode } from 'react' +import { useParams } from 'common' import { AdvancedAuthSettingsForm } from 'components/interfaces/Auth/AdvancedAuthSettingsForm' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const PageLayout: NextPageWithLayout = () => { - const isPermissionsLoaded = usePermissionsLoaded() - // TODO: check if these permissions cover third party auth as well - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - - return ( - <> - - - Advanced - - - - {!isPermissionsLoaded ? ( - - ) : !canReadAuthSettings ? ( - - ) : ( - - )} - - - ) +interface AdvancedLayoutProps { + children: ReactNode } -PageLayout.getLayout = (page) => { +export const AdvancedLayout = ({ children }: AdvancedLayoutProps) => { + const { ref } = useParams() + return ( - {page} + + + {children} + + ) } -export default PageLayout + +const AdvancedPage: NextPageWithLayout = () => { + const isPermissionsLoaded = usePermissionsLoaded() + const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + + if (isPermissionsLoaded && !canReadAuthSettings) { + return + } + + return ( + + {!isPermissionsLoaded ? ( +
+ +
+ ) : ( + + )} +
+ ) +} + +AdvancedPage.getLayout = (page) => {page} + +export default AdvancedPage diff --git a/apps/studio/pages/project/[ref]/auth/hooks.tsx b/apps/studio/pages/project/[ref]/auth/hooks.tsx index e939b807163..e3725985bf4 100644 --- a/apps/studio/pages/project/[ref]/auth/hooks.tsx +++ b/apps/studio/pages/project/[ref]/auth/hooks.tsx @@ -1,48 +1,49 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ReactNode } from 'react' import { HooksListing } from 'components/interfaces/Auth/Hooks/HooksListing' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' -import { - ScaffoldHeader, - ScaffoldContainer, - ScaffoldTitle, - ScaffoldDescription, -} from 'components/layouts/Scaffold' +import DefaultLayout from 'components/layouts/DefaultLayout' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -import DefaultLayout from 'components/layouts/DefaultLayout' + +interface HooksLayoutProps { + children: ReactNode +} + +export const HooksLayout = ({ children }: HooksLayoutProps) => { + return ( + + + + {children} + + + + ) +} + const Hooks: NextPageWithLayout = () => { const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const isPermissionsLoaded = usePermissionsLoaded() if (isPermissionsLoaded && !canReadAuthSettings) { return - } else { - return ( -
- - - Auth Hooks - - Use Postgres functions or HTTP endpoints to customize the behavior of Supabase Auth to - meet your needs - - - - - - - -
- ) } + + return ( + + + + ) } -Hooks.getLayout = (page) => ( - - {page} - -) +Hooks.getLayout = (page) => {page} export default Hooks diff --git a/apps/studio/pages/project/[ref]/auth/protection.tsx b/apps/studio/pages/project/[ref]/auth/protection.tsx index 6f7bbddef9d..e4dea43e4c8 100644 --- a/apps/studio/pages/project/[ref]/auth/protection.tsx +++ b/apps/studio/pages/project/[ref]/auth/protection.tsx @@ -1,45 +1,59 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ReactNode } from 'react' -import { - AuthProvidersForm, - ProtectionAuthSettingsForm, - ThirdPartyAuthForm, -} from 'components/interfaces/Auth' +import { useParams } from 'common' +import { ProtectionAuthSettingsForm } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import { ScaffoldHeader, ScaffoldContainer, ScaffoldTitle } from 'components/layouts/Scaffold' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const PageLayout: NextPageWithLayout = () => { - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const isPermissionsLoaded = usePermissionsLoaded() - - if (isPermissionsLoaded && !canReadAuthSettings) { - return - } else { - return ( -
- - - Attack Protection - - - - - - -
- ) - } +interface ProtectionLayoutProps { + children: ReactNode } -PageLayout.getLayout = (page) => { +export const ProtectionLayout = ({ children }: ProtectionLayoutProps) => { + const { ref } = useParams() + return ( - {page} + + + {children} + + ) } -export default PageLayout + +const ProtectionPage: NextPageWithLayout = () => { + const isPermissionsLoaded = usePermissionsLoaded() + const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + + if (isPermissionsLoaded && !canReadAuthSettings) { + return + } + + return ( + + {!isPermissionsLoaded ? ( +
+ +
+ ) : ( + + )} +
+ ) +} + +ProtectionPage.getLayout = (page) => {page} + +export default ProtectionPage diff --git a/packages/ui-patterns/form/Layout/FormLayout.tsx b/packages/ui-patterns/form/Layout/FormLayout.tsx index f1940a90e6b..f8333c23bc3 100644 --- a/packages/ui-patterns/form/Layout/FormLayout.tsx +++ b/packages/ui-patterns/form/Layout/FormLayout.tsx @@ -45,7 +45,7 @@ const ContainerVariants = cva('relative grid gap-10', { horizontal: 'flex flex-col gap-2 md:grid md:grid-cols-12', vertical: 'flex flex-col gap-3', flex: 'flex flex-row gap-3', - 'flex-row-reverse': 'flex flex-row gap-3 flex-row-reverse justify-between', + 'flex-row-reverse': 'flex flex-row gap-6 flex-row-reverse justify-between', }, flex: { true: '', @@ -233,7 +233,7 @@ const FlexContainer = cva('', { }, { layout: 'flex-row-reverse', - className: 'flex flex-col justify-center items-end', + className: 'flex flex-col justify-center items-end shrink-0', }, ], }) diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 37bf296a2ae..ef837cb4dc2 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -58,6 +58,8 @@ export * from './src/components/Upload' export * from './src/components/Form' export * from './src/components/ExpandingTextArea' +export * from './src/components/PrePostTab' + // layout export * from './src/components/LoadingLine' diff --git a/packages/ui/src/components/PrePostTab/PrePostTab.tsx b/packages/ui/src/components/PrePostTab/PrePostTab.tsx new file mode 100644 index 00000000000..be2180673f6 --- /dev/null +++ b/packages/ui/src/components/PrePostTab/PrePostTab.tsx @@ -0,0 +1,36 @@ +import { forwardRef, ReactNode, PropsWithChildren, HTMLAttributes } from 'react' +import { cn } from '../../lib/utils' + +export interface PrePostTabProps extends HTMLAttributes { + preTab?: ReactNode | string + postTab?: ReactNode | string +} + +export const PrePostTab = forwardRef>( + ({ preTab, postTab, children, className, ...props }, ref) => { + return ( +
+ {preTab && ( +
+ {preTab} +
+ )} +
+ {children} +
+ {postTab && ( +
+ {postTab} +
+ )} +
+ ) + } +) diff --git a/packages/ui/src/components/PrePostTab/index.tsx b/packages/ui/src/components/PrePostTab/index.tsx new file mode 100644 index 00000000000..89a9935b84a --- /dev/null +++ b/packages/ui/src/components/PrePostTab/index.tsx @@ -0,0 +1,2 @@ +export { PrePostTab } from './PrePostTab' +export type { PrePostTabProps } from './PrePostTab'