diff --git a/apps/docs/content/guides/platform/privatelink.mdx b/apps/docs/content/guides/platform/privatelink.mdx index a934dc535bd23..cdaa3aaeb148c 100644 --- a/apps/docs/content/guides/platform/privatelink.mdx +++ b/apps/docs/content/guides/platform/privatelink.mdx @@ -7,7 +7,6 @@ description: 'Secure private network connectivity to your Supabase database usin PrivateLink is available only to Team and Enterprise customers. -Contact support if you would like to create a PrivateLink connection for a read-only replica. @@ -37,19 +36,24 @@ To use PrivateLink with your Supabase project: ## Getting started -### Step 1: Add AWS account +### Step 1: Add connection Navigate to your project's Integrations section to set up PrivateLink: 1. Go to your Supabase project dashboard 2. Navigate to [**Settings** > **Integrations**](/dashboard/project/_/settings/integrations) 3. Find the **AWS PrivateLink** section -4. Click **Add Account** -5. Enter your AWS Account ID -6. Provide a description for the account (recommended) -7. Click **Add Account** to submit +4. Click **Add connection** +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 -After submission, Supabase creates a VPC Lattice Resource Configuration for your project and sends an AWS Resource Share to the specified AWS Account ID. This process may take a few moments. Once complete, the account will show a "Ready" status, indicating that the resource share has been sent to your AWS account and is ready to be accepted. +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. + +After submission, Supabase creates a VPC Lattice Resource Configuration for your project and sends an AWS Resource Share to the specified AWS account ID. This process may take a few moments. Once complete, the connection will show a "Ready" status, indicating that the resource share has been sent to your AWS account and is ready to be accepted. You must accept the resource share within 12 hours, or the request expires and can no longer be accepted in AWS. You'll need to create a new connection to try again. + +Once ready, select **View connection** to see the VPC Lattice resource configuration ID and ARNs for the connection. This is useful for confirming which resource configuration corresponds to which database when a project has multiple connections, for example one for the primary database and one for each read replica. ### Step 2: Accept resource share @@ -61,6 +65,7 @@ Supabase will send you an AWS Resource Share containing the VPC Lattice Resource 3. Go to [Shared with me > Resource shares](https://console.aws.amazon.com/ram/home#SharedResourceShares) 4. Locate the resource share from Supabase. - The resource share has the format `sspl-[project_ref]-[random alphanumeric string]` + - If your project has multiple connections, for example one for the primary database and one for each read replica, match the share's ARN to the **Resource share ARN** shown for that connection in **View connection** to confirm you're accepting the correct one 5. Click on the resource share name to view details. Review the list of resource shares - it should only include resources of type vpc-lattice:ResourceConfiguration. 6. Click **Accept resource share** 7. Confirm the acceptance in the dialog box @@ -95,6 +100,7 @@ In your AWS account, you have two options to establish connectivity: 5. Under Type, select **Resources** 6. In the **Resource configurations** section select the appropriate resource configuration - The resource configuration name will be in the format `[organisation]-[project-ref]-rc` + - If you have multiple connections, match the **Resource configuration ID** shown for that connection in **View connection** to confirm you select the configuration for the correct database 7. Select your VPC from the dropdown. This should match the VPC you selected for your security group in Step 3 8. Enable the **Enable DNS name** option if you want to use a DNS record instead of the endpoints IP address(es) 9. Choose the appropriate subnets for your network @@ -115,6 +121,7 @@ In your AWS account, you have two options to establish connectivity: 4. In the service network details, go to the **Resource configuration associations** tab 5. Click **Create associations** 6. Select the appropriate **Resource configuration** from the dropdown + - If you have multiple connections, match the **Resource configuration ID** shown for that connection in **View connection** to confirm you select the configuration for the correct database 7. Click **Save changes** 8. After creation, you will see the resource configuration in the Resource configurations section of your service network with the status "Active" 9. For connectivity, click on the association details and the domain name will be listed in the **DNS entries** section @@ -174,7 +181,7 @@ For maximum security, you can restrict public database access in your project se ## Limitations -- **Read Replicas**: To establish PrivateLink with a Read Replica, reach out to your account rep. +- **Service Scope**: PrivateLink only supports database connections (Postgres and PgBouncer). Other Supabase services (API, Storage, Auth, Realtime) will continue to operate over public internet connections. - **Feature Evolution**: The setup process and capabilities may evolve as we refine the offering ## Compatibility diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx index 61c79ac6bec4e..d8952f9625b31 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx @@ -1,6 +1,17 @@ import dayjs from 'dayjs' +import { Plus } from 'lucide-react' import { useState } from 'react' -import { Button } from 'ui' +import { Button, Card, CardContent } from 'ui' +import { Admonition } from 'ui-patterns/Admonition' +import { + PageSection, + PageSectionAside, + PageSectionContent, + PageSectionDescription, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AddNewFactorModal } from './AddNewFactorModal' @@ -14,54 +25,86 @@ export const TOTPFactors = () => { const [factorToBeDeleted, setFactorToBeDeleted] = useState(null) const { data, isPending: isLoading, isError, isSuccess, error } = useMfaListFactorsQuery() + const totpFactors = data?.totp ?? [] + const canAddApp = isSuccess && totpFactors.length < 2 + const shouldShowLockoutWarning = isSuccess && totpFactors.length === 1 + + const handleAddNewApp = () => setIsAddNewFactorOpen(true) + return ( <> -
-

- Use an authenticator app (like Google Authenticator or 1Password) to protect your account. -

-
- {isLoading && } + + + + Multi-factor authentication + + Use an authenticator app (like Google Authenticator or 1Password) to protect your + account. + + + {canAddApp && ( + + + + )} + + + {shouldShowLockoutWarning && ( + } onClick={handleAddNewApp}> + Add another app + + } + /> + )} + {isLoading && ( + + + + + + )} {isError && ( )} {isSuccess && ( - <> -
- {data.totp.map((factor) => { - return ( -
-

- Name:{' '} - {factor.friendly_name ?? 'No name provided'} -

-
-

+ + {totpFactors.length === 0 ? ( + +

No authenticator apps yet.

+ + ) : ( +
+ {totpFactors.map((factor) => ( + +
+

{factor.friendly_name ?? 'No name provided'}

+

Added on {dayjs(factor.created_at).format(DATETIME_FORMAT)}

-
-
- ) - })} -
- {data.totp.length < 2 ? ( - <> -
- -
- - ) : null} - + + + ))} +
+ )} + )} -
-
+ + setIsAddNewFactorOpen(false)} @@ -69,7 +112,7 @@ export const TOTPFactors = () => { setFactorToBeDeleted(null)} /> diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx index 99f9cb7658ede..6ef98e5749da5 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx @@ -676,8 +676,8 @@ export const ProjectCreationForm = ({ label="GitHub (optional)" description={ <> - Ideal for agent-first workflows: update your schema in code, push it - to GitHub, and Supabase deploys the changes automatically.{' '} + Ideal for agent-first workflows. Update your schema in code and push + it to GitHub. Supabase deploys the changes.{' '} Remove the replica{' '} - PrivateLink association + PrivateLink connection {' '} before dropping this read replica diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.test.ts b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.test.ts new file mode 100644 index 0000000000000..7cc18dd989c9f --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' + +import { getConnectionStatusUi, type PrivateLinkConnectionStatus } from './AWSPrivateLink.utils' + +describe('getConnectionStatusUi', () => { + it.each([ + [ + 'ASSOCIATION_ACCEPTED', + { + badge: 'Connected', + badgeVariant: 'success', + title: 'This connection is active', + }, + ], + [ + 'READY', + { + badge: 'Ready', + badgeVariant: 'success', + title: 'Waiting for the AWS account owner to accept', + description: 'This request expires after 12 hours.', + }, + ], + [ + 'CREATING', + { + badge: 'Creating', + badgeVariant: 'warning', + title: 'This connection is being created', + }, + ], + [ + 'DELETING', + { + badge: 'Deleting', + badgeVariant: 'destructive', + title: 'This connection is being deleted', + }, + ], + [ + 'ASSOCIATION_REQUEST_EXPIRED', + { + badge: 'Expired', + badgeVariant: 'destructive', + title: 'This request has expired', + }, + ], + [ + 'CREATION_FAILED', + { + badge: 'Failed', + badgeVariant: 'destructive', + title: "Couldn't create this connection", + }, + ], + ] as const satisfies ReadonlyArray< + [PrivateLinkConnectionStatus, Partial>] + >)('maps %s', (status, expected) => { + expect(getConnectionStatusUi(status)).toMatchObject(expected) + }) + + it('returns unknown copy when status is missing', () => { + const ui = getConnectionStatusUi() + + expect(ui.badge).toBe('Unknown') + expect(ui.badgeVariant).toBe('default') + expect(ui.title).toBe("Couldn't determine this connection's status") + }) +}) diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.ts b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.ts new file mode 100644 index 0000000000000..07baa806d76d7 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLink.utils.ts @@ -0,0 +1,63 @@ +import type { AWSAccount } from '@/data/aws-accounts/aws-accounts-query' + +export type PrivateLinkConnectionStatus = AWSAccount['status'] + +type BadgeVariant = 'success' | 'warning' | 'destructive' | 'default' + +export type ConnectionStatusUi = { + title: string + description: string + badge: string + badgeVariant: BadgeVariant +} + +const CONNECTION_STATUS_UI: Record = { + ASSOCIATION_ACCEPTED: { + title: 'This connection is active', + description: 'The AWS account owner has accepted the resource share.', + badge: 'Connected', + badgeVariant: 'success', + }, + READY: { + title: 'Waiting for the AWS account owner to accept', + description: 'This request expires after 12 hours.', + badge: 'Ready', + badgeVariant: 'success', + }, + CREATING: { + title: 'This connection is being created', + description: '', + badge: 'Creating', + badgeVariant: 'warning', + }, + DELETING: { + title: 'This connection is being deleted', + description: '', + badge: 'Deleting', + badgeVariant: 'destructive', + }, + ASSOCIATION_REQUEST_EXPIRED: { + title: 'This request has expired', + description: 'Add a new connection to try again.', + badge: 'Expired', + badgeVariant: 'destructive', + }, + CREATION_FAILED: { + title: "Couldn't create this connection", + description: 'Add a new connection to try again.', + badge: 'Failed', + badgeVariant: 'destructive', + }, +} + +const UNKNOWN_STATUS_UI: ConnectionStatusUi = { + title: "Couldn't determine this connection's status", + description: '', + badge: 'Unknown', + badgeVariant: 'default', +} + +export function getConnectionStatusUi(status?: PrivateLinkConnectionStatus): ConnectionStatusUi { + if (!status) return UNKNOWN_STATUS_UI + return CONNECTION_STATUS_UI[status] ?? UNKNOWN_STATUS_UI +} diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx index 6d7af86331ddd..225ad2ca66627 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx @@ -8,8 +8,12 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' +import { getConnectionStatusUi } from './AWSPrivateLink.utils' import { formatDatabaseID } from '@/data/read-replicas/replicas.utils' interface AWSPrivateLinkAccountItemProps { @@ -17,6 +21,9 @@ interface AWSPrivateLinkAccountItemProps { account_name?: string database_type?: 'PRIMARY' | 'READ_REPLICA' database_identifier?: string + resource_access_manager_resource_config_id?: string + resource_access_manager_resource_config_arn?: string + resource_access_manager_share_arn?: string status: | 'CREATING' | 'READY' @@ -34,6 +41,9 @@ export const AWSPrivateLinkAccountItem = ({ account_name, database_type, database_identifier, + resource_access_manager_resource_config_id, + resource_access_manager_resource_config_arn, + resource_access_manager_share_arn, status, onEdit, onDelete, @@ -42,35 +52,38 @@ export const AWSPrivateLinkAccountItem = ({ database_type === 'READ_REPLICA' ? `Read replica (ID: ${database_identifier ? formatDatabaseID(database_identifier) : 'Unknown identifier'})` : 'Primary database' - - const getStatusBadge = () => { - switch (status) { - case 'ASSOCIATION_ACCEPTED': - return Connected - case 'READY': - return Ready - case 'CREATING': - return Creating - case 'DELETING': - return Deleting - case 'ASSOCIATION_REQUEST_EXPIRED': - return Expired - case 'CREATION_FAILED': - return Failed - default: - return Unknown - } - } + const statusUi = getConnectionStatusUi(status) return (
-
{aws_account_id}
-
{databaseTarget}
-
{account_name || 'No description'}
+ {account_name &&
{account_name}
} +
Database: {databaseTarget}
+
Destination account: {aws_account_id}
+ {resource_access_manager_resource_config_id && ( +
+ Resource configuration: + + + {resource_access_manager_resource_config_id} + + {(resource_access_manager_resource_config_arn || + resource_access_manager_share_arn) && ( + + {resource_access_manager_resource_config_arn && ( +

Resource config ARN: {resource_access_manager_resource_config_arn}

+ )} + {resource_access_manager_share_arn && ( +

Resource share ARN: {resource_access_manager_share_arn}

+ )} +
+ )} +
+
+ )}
- {getStatusBadge()} + {statusUi.badge} @@ -79,12 +92,12 @@ export const AWSPrivateLinkAccountItem = ({ - View account + View connection - Delete account + Delete connection diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx index a41ec80e7b353..53c293ecddc66 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 { @@ -25,8 +25,11 @@ import { SheetTitle, } from 'ui' 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' @@ -35,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() @@ -61,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 account connection is being created' - : account?.status === 'DELETING' - ? 'This account is being deleted' - : account?.status === 'ASSOCIATION_REQUEST_EXPIRED' - ? 'Account acceptance request has expired' - : account?.status === 'CREATION_FAILED' - ? 'Failed to create account' - : 'This account needs to be accepted by the AWS account owner.' - - const description = - account?.status === 'ASSOCIATION_ACCEPTED' - ? 'The resource share has been accepted by the AWS account owner and the connection is established.' - : account?.status === 'READY' - ? 'It may be waiting acceptance from the AWS account owner. Association requests are automatically deleted if not accepted within 12 hours.' - : account?.status === 'ASSOCIATION_REQUEST_EXPIRED' - ? 'Reconnect this account to initiate a new connection request' - : account?.status === 'CREATION_FAILED' - ? 'Reconnect this account to initiate a new connection request' - : '' + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + awsAccountId: '', + databaseIdentifier: '', + accountName: '', + }, + values: formValues, + resetOptions: { keepDirtyValues: true }, + }) const onSubmit = (values: FormValues) => { if (!project) return @@ -103,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 @@ -111,8 +105,8 @@ export const AWSPrivateLinkForm = ({ account, open, onOpenChange }: AWSPrivateLi }, { onSuccess: () => { - form.reset(defaultValues) - toast.success('Successfully added AWS account') + form.reset(formValues) + toast.success('Connection added') onOpenChange(false) }, } @@ -120,94 +114,90 @@ 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 AWS Account' : 'AWS Account Details'} + {isNew ? 'Add connection' : 'Connection details'} - Connect to your Supabase project from your AWS VPC using AWS PrivateLink.{' '} + {isNew + ? 'Enter an AWS account to connect. You’ll need to accept the share in AWS. ' + : 'These values identify the resource share in AWS. '} Learn more
- - + + {!isNew && account && ( - <> - - {title} - + {statusUi.title} + + {statusUi.badge} + + + } + description={statusUi.description} + actions={ + account.status === 'READY' && ( + - ) - } - /> - + How to accept + + + ) + } + /> )} - {(showPrivateLinkReadReplica || !isNew) && ( + ( + + + { + if (!isNew) e.target.blur() + }} + /> + + + )} + /> + {showDatabaseTarget && ( ( { - if (!isNew) { - e.target.blur() - } - }} - /> - - - )} - /> ( - + { - if (!isNew) { - e.target.blur() - } + if (!isNew) e.target.blur() }} /> )} /> + {!isNew && account?.resource_access_manager_resource_config_id && ( + + + + )} + {!isNew && account?.resource_access_manager_resource_config_arn && ( + + + + )} + {!isNew && account?.resource_access_manager_share_arn && ( + + + + )} - {isNew && ( - )} diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx index e8d1595f2397c..ecc6759da4229 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx @@ -34,7 +34,7 @@ export const AWSPrivateLinkSection = () => { const { mutate: deleteAccount, isPending: isDeleting } = useAWSAccountDeleteMutation({ onSuccess: () => { - toast.success('Account will be deleted shortly') + toast.success('Connection will be deleted shortly') setShowDeleteModal(false) setSelectedAccount(undefined) }, @@ -80,9 +80,7 @@ export const AWSPrivateLinkSection = () => { AWS PrivateLink - Connect to your Supabase project from your AWS VPC using AWS PrivateLink. Create a - private connection between your AWS VPC and your Supabase project without traffic - traversing the public internet. + Connect to this project from your AWS VPC without using the public internet. @@ -92,17 +90,17 @@ export const AWSPrivateLinkSection = () => { {promptPlanUpgrade && ( )}
-

AWS Accounts

+

Connections

{(accounts?.length ?? 0) > 0 ? ( @@ -119,7 +117,7 @@ export const AWSPrivateLinkSection = () => { ) : ( -

No accounts connected

+

No connections yet

)} @@ -133,15 +131,15 @@ export const AWSPrivateLinkSection = () => { setShowDeleteModal(false)} onConfirm={onConfirmDelete} >

- Are you sure you want to delete the AWS account connection for{' '} - {selectedAccount?.aws_account_id}? + This removes the PrivateLink connection for {selectedAccount?.aws_account_id}. + Applications using this private path will lose access.

Database:{' '} diff --git a/apps/studio/data/aws-accounts/aws-account-create-mutation.ts b/apps/studio/data/aws-accounts/aws-account-create-mutation.ts index 9bfe6bedf8c4b..060e6f9e56629 100644 --- a/apps/studio/data/aws-accounts/aws-account-create-mutation.ts +++ b/apps/studio/data/aws-accounts/aws-account-create-mutation.ts @@ -56,7 +56,7 @@ export const useAWSAccountCreateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to create AWS account: ${data.message}`) + toast.error(`Failed to create connection: ${data.message}`) } else { onError(data, variables, context) } diff --git a/apps/studio/data/aws-accounts/aws-account-delete-mutation.ts b/apps/studio/data/aws-accounts/aws-account-delete-mutation.ts index 9389d2eca9533..2fc22aa974a0b 100644 --- a/apps/studio/data/aws-accounts/aws-account-delete-mutation.ts +++ b/apps/studio/data/aws-accounts/aws-account-delete-mutation.ts @@ -63,7 +63,7 @@ export const useAWSAccountDeleteMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to delete AWS account: ${data.message}`) + toast.error(`Failed to delete connection: ${data.message}`) } else { onError(data, variables, context) } diff --git a/apps/studio/data/aws-accounts/aws-accounts-query.ts b/apps/studio/data/aws-accounts/aws-accounts-query.ts index 19f2208ef0a34..ca378da61fdf9 100644 --- a/apps/studio/data/aws-accounts/aws-accounts-query.ts +++ b/apps/studio/data/aws-accounts/aws-accounts-query.ts @@ -11,11 +11,14 @@ type AWSAccountsVariables = { projectRef?: string } -// [Joshen] API types should be updated with these 2 parameters, so remove once verified +// [Joshen] API types should be updated with these parameters, so remove once verified export type AWSAccount = components['schemas']['GetPrivateLinkResponse']['private_link_associations'][number] & { database_type?: 'PRIMARY' | 'READ_REPLICA' database_identifier?: string + resource_access_manager_resource_config_id?: string + resource_access_manager_resource_config_arn?: string + resource_access_manager_share_arn?: string } async function getAWSAccounts({ projectRef }: AWSAccountsVariables, signal?: AbortSignal) { diff --git a/apps/studio/pages/account/security.tsx b/apps/studio/pages/account/security.tsx index db7ec4be0b798..95ceb21923920 100644 --- a/apps/studio/pages/account/security.tsx +++ b/apps/studio/pages/account/security.tsx @@ -1,6 +1,3 @@ -import { Lock } from 'lucide-react' -import { Badge, Card, CardContent, CardHeader } from 'ui' -import { Admonition } from 'ui-patterns/Admonition' import { PageContainer } from 'ui-patterns/PageContainer' import { PageHeader, @@ -15,15 +12,12 @@ import AccountLayout from '@/components/layouts/AccountLayout/AccountLayout' import { AppLayout } from '@/components/layouts/AppLayout/AppLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { UnknownInterface } from '@/components/ui/UnknownInterface' -import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from '@/types' const Security: NextPageWithLayout = () => { const showSecuritySettings = useIsFeatureEnabled('account:show_security_settings') - const { data } = useMfaListFactorsQuery({ enabled: showSecuritySettings }) - if (!showSecuritySettings) { return } @@ -41,32 +35,7 @@ const Security: NextPageWithLayout = () => { - {data?.totp.length === 1 && ( - - )} - - -

- - Multi-factor authentication (MFA) -
- - {data ? ( - - {data.totp.length} app{data.totp.length === 1 ? '' : 's'} configured - - ) : null} - - - - - + ) diff --git a/packages/ui-patterns/src/form/Layout/FormLayout.tsx b/packages/ui-patterns/src/form/Layout/FormLayout.tsx index 863bc0dd6e34b..37f67aa61a314 100644 --- a/packages/ui-patterns/src/form/Layout/FormLayout.tsx +++ b/packages/ui-patterns/src/form/Layout/FormLayout.tsx @@ -342,7 +342,7 @@ export const FormLayout = React.forwardRef< // Rendered as a div rather than a p as descriptions can be arbitrary JSX // which may contain block-level elements (invalid HTML inside a p)
{description}