diff --git a/apps/docs/content/guides/platform/privatelink.mdx b/apps/docs/content/guides/platform/privatelink.mdx index 8d079f5d3bc..cdaa3aaeb14 100644 --- a/apps/docs/content/guides/platform/privatelink.mdx +++ b/apps/docs/content/guides/platform/privatelink.mdx @@ -44,9 +44,9 @@ Navigate to your project's Integrations section to set up PrivateLink: 2. Navigate to [**Settings** > **Integrations**](/dashboard/project/_/settings/integrations) 3. Find the **AWS PrivateLink** section 4. Click **Add connection** -5. Provide a description (recommended) -6. Select the database target: the primary database or a specific read replica -7. Enter your AWS account ID +5. Enter the destination AWS account ID +6. Select the database: the primary database or a specific read replica +7. Optionally add a description 8. Click **Add connection** to submit Each database, whether the primary or a read replica, needs its own connection. Create a separate connection for every database you want to reach over PrivateLink. diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx index 52b0ba6ac35..53c293ecddc 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx @@ -1,7 +1,7 @@ +import { zodResolver } from '@hookform/resolvers/zod' import { useFlag } from 'common' import { ExternalLink } from 'lucide-react' import Link from 'next/link' -import { useEffect } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { @@ -27,7 +27,9 @@ import { import { Admonition } from 'ui-patterns/Admonition' import { Input as CopyableInput } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' +import { getConnectionStatusUi } from './AWSPrivateLink.utils' import { InlineLink } from '@/components/ui/InlineLink' import { useAWSAccountCreateMutation } from '@/data/aws-accounts/aws-account-create-mutation' import type { AWSAccount } from '@/data/aws-accounts/aws-accounts-query' @@ -36,18 +38,25 @@ import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/rep import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' +const FORM_ID = 'privatelink-connection-form' + +const FormSchema = z.object({ + awsAccountId: z + .string() + .trim() + .regex(/^\d{12}$/, 'Enter a 12-digit AWS account ID'), + databaseIdentifier: z.string().min(1, 'Select a database'), + accountName: z.string(), +}) + +type FormValues = z.infer + interface AWSPrivateLinkFormProps { account?: AWSAccount open: boolean onOpenChange: (open: boolean) => void } -interface FormValues { - awsAccountId: string - accountName: string - databaseIdentifier: string -} - export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLinkFormProps) => { const isNew = !account const { data: project } = useSelectedProjectQuery() @@ -62,40 +71,24 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi const { mutate: createAccount, isPending } = useAWSAccountCreateMutation() const readReplicas = databases.filter((database) => database.identifier !== project?.ref) - - const defaultValues = { + const showDatabaseTarget = showPrivateLinkReadReplica || !isNew + const statusUi = getConnectionStatusUi(account?.status) + const formValues: FormValues = { awsAccountId: account?.aws_account_id ?? '', - accountName: account?.account_name ?? '', databaseIdentifier: account?.database_identifier ?? project?.ref ?? '', + accountName: account?.account_name ?? '', } - const form = useForm({ defaultValues }) - - const title = - account?.status === 'ASSOCIATION_ACCEPTED' - ? 'This connection is active' - : account?.status === 'READY' - ? 'Connection is ready to accept' - : account?.status === 'CREATING' - ? 'This connection is being created' - : account?.status === 'DELETING' - ? 'This connection is being deleted' - : account?.status === 'ASSOCIATION_REQUEST_EXPIRED' - ? 'This request has expired' - : account?.status === 'CREATION_FAILED' - ? "Couldn't create this connection" - : 'This connection needs to be accepted by the AWS account owner.' - - const description = - account?.status === 'ASSOCIATION_ACCEPTED' - ? 'The AWS account owner has accepted the resource share.' - : account?.status === 'READY' - ? 'It may be waiting acceptance from the AWS account owner. Requests expire after 12 hours.' - : account?.status === 'ASSOCIATION_REQUEST_EXPIRED' - ? 'Add a new connection to try again.' - : account?.status === 'CREATION_FAILED' - ? 'Add a new connection to try again.' - : '' + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + awsAccountId: '', + databaseIdentifier: '', + accountName: '', + }, + values: formValues, + resetOptions: { keepDirtyValues: true }, + }) const onSubmit = (values: FormValues) => { if (!project) return @@ -104,7 +97,7 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi { projectRef: project.ref, awsAccountId: values.awsAccountId, - accountName: values.accountName, + accountName: values.accountName.trim() || undefined, databaseIdentifier: values.databaseIdentifier && values.databaseIdentifier !== project.ref ? values.databaseIdentifier @@ -112,7 +105,7 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi }, { onSuccess: () => { - form.reset(defaultValues) + form.reset(formValues) toast.success('Connection added') onOpenChange(false) }, @@ -121,17 +114,13 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi } } - // Reset form when account changes - useEffect(() => { - form.reset({ - awsAccountId: account?.aws_account_id ?? '', - accountName: account?.account_name ?? '', - databaseIdentifier: account?.database_identifier ?? project?.ref ?? '', - }) - }, [account, form, project?.ref]) + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) form.reset(formValues) + onOpenChange(nextOpen) + } return ( - + {isNew ? 'Add connection' : 'Connection details'} @@ -143,96 +132,72 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi
- + {!isNew && account && ( - <> - - {title} - + {statusUi.title} + + {statusUi.badge} + + + } + description={statusUi.description} + actions={ + account.status === 'READY' && ( + - ) - } - /> - + How to accept + + + ) + } + /> )} ( { - if (!isNew) { - e.target.blur() - } + if (!isNew) e.target.blur() }} /> )} /> - {(showPrivateLinkReadReplica || !isNew) && ( + {showDatabaseTarget && ( ( { - if (!isNew) { - e.target.blur() - } + if (!isNew) e.target.blur() }} /> @@ -289,10 +248,7 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi )} /> {!isNew && account?.resource_access_manager_resource_config_id && ( - + )} {!isNew && account?.resource_access_manager_resource_config_arn && ( - + )} {!isNew && account?.resource_access_manager_share_arn && ( - + )} - {isNew && ( - )}