From 9fe966f8150adbcbf75698f740e1df4988e27d72 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Tue, 21 Oct 2025 14:55:09 -0400 Subject: [PATCH 1/4] refactor DatabaseManageNetworkingDrawer to use react-hook-form --- .../DatabaseCreate/DatabaseVPCSelector.tsx | 5 +- .../DatabaseManageNetworkingDrawer.tsx | 173 ++++++------ .../DatabaseVPCSelector.tsx | 254 +++++++----------- 3 files changed, 178 insertions(+), 254 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx index 843d55aefdf..9b27812ea09 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx @@ -12,11 +12,10 @@ import * as React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Link } from 'src/components/Link'; +import { MANAGE_NETWORKING_LEARN_MORE_LINK } from 'src/features/Databases/constants'; import { useFlags } from 'src/hooks/useFlags'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { MANAGE_NETWORKING_LEARN_MORE_LINK } from '../constants'; - import type { DatabaseCreateValues } from './DatabaseCreate'; import type { VPC } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; @@ -108,7 +107,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { }} options={vpcs ?? []} placeholder="Select a VPC" - sx={{ width: '354px' }} + sx={{ width: '390px' }} textFieldProps={{ tooltipText: 'A cluster may be assigned only to a VPC in the same region', diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx index bd5c235a3fa..a6ef28297ab 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx @@ -1,19 +1,15 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import { useDatabaseMutation } from '@linode/queries'; import { Box, Button, Drawer, Notice } from '@linode/ui'; import { updatePrivateNetworkSchema } from '@linode/validation'; import { useNavigate } from '@tanstack/react-router'; -import { useFormik } from 'formik'; import { enqueueSnackbar } from 'notistack'; import * as React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; -import { DatabaseVPCSelector } from './DatabaseVPCSelector'; +import { DatabaseVPCSelector } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector'; -import type { - Database, - PrivateNetwork, - UpdateDatabasePayload, - VPC, -} from '@linode/api-v4'; +import type { Database, UpdateDatabasePayload, VPC } from '@linode/api-v4'; import type { Theme } from '@linode/ui'; interface Props { @@ -24,9 +20,10 @@ interface Props { vpc: undefined | VPC; } -export type ManageNetworkingFormValues = { - private_network: PrivateNetwork; -}; +export type ManageNetworkingFormValues = Pick< + UpdateDatabasePayload, + 'private_network' +>; const DatabaseManageNetworkingDrawer = (props: Props) => { const { database, vpc, onClose, onUnassign, open } = props; @@ -41,10 +38,22 @@ const DatabaseManageNetworkingDrawer = (props: Props) => { }, }; - const submitForm = () => { - const payload: UpdateDatabasePayload = { ...values }; + const form = useForm({ + defaultValues: initialValues, + mode: 'onBlur', + // @ts-expect-error handle null validation with trigger + resolver: yupResolver(updatePrivateNetworkSchema), + }); + + const { + formState: { isDirty, isValid }, + handleSubmit, + reset, + watch, + } = form; - updateDatabase(payload).then(() => { + const onSubmit = (values: ManageNetworkingFormValues) => { + updateDatabase(values).then(() => { enqueueSnackbar('Changes are being applied.', { variant: 'info', }); @@ -59,34 +68,20 @@ const DatabaseManageNetworkingDrawer = (props: Props) => { }); }; - const { - errors, - handleSubmit, - resetForm, - isValid, - dirty, - setFieldValue, - values, - } = useFormik({ - initialValues, - onSubmit: submitForm, - validationSchema: updatePrivateNetworkSchema, - validateOnChange: true, - validateOnBlur: true, - }); // TODO (UIE-8903): Replace deprecated Formik with React Hook Form + const [publicAccess, subnetId, vpcId] = watch([ + 'private_network.public_access', + 'private_network.subnet_id', + 'private_network.vpc_id', + ]); const hasVPCConfigured = !!database?.private_network?.vpc_id; const hasConfigChanged = - values.private_network.vpc_id !== database?.private_network?.vpc_id || - values.private_network.subnet_id !== database?.private_network?.subnet_id || - values.private_network.public_access !== - database?.private_network?.public_access; - const hasValidSelection = - !!values.private_network.vpc_id && - !!values.private_network.subnet_id && - hasConfigChanged; + vpcId !== database?.private_network?.vpc_id || + subnetId !== database?.private_network?.subnet_id || + publicAccess !== database?.private_network?.public_access; + const hasValidSelection = !!vpcId && !!subnetId && hasConfigChanged; - const isSaveDisabled = !dirty || !isValid || !hasValidSelection; + const isSaveDisabled = !isDirty || !isValid || !hasValidSelection; const { error: manageNetworkingError, @@ -97,14 +92,14 @@ const DatabaseManageNetworkingDrawer = (props: Props) => { const handleOnClose = () => { onClose(); - resetForm(); + reset(); resetMutation?.(); }; /** Resets the form after opening the unassign VPC dialog */ const handleOnUnassign = () => { onUnassign(); - resetForm(); + reset(); resetMutation?.(); }; @@ -113,59 +108,53 @@ const DatabaseManageNetworkingDrawer = (props: Props) => { {manageNetworkingError && ( )} -
- - setFieldValue(field, value) - } - privateNetworkValues={values.private_network} - selectedRegionId={database?.region} - /> - ({ - marginTop: theme.spacingFunction(50), - paddingTop: theme.spacingFunction(8), - paddingBottom: theme.spacingFunction(8), - display: 'flex', - justifyContent: hasVPCConfigured ? 'space-between' : 'flex-end', - })} - > - {hasVPCConfigured && ( - - )} - - - + + + + ({ + marginTop: theme.spacingFunction(50), + paddingTop: theme.spacingFunction(8), + paddingBottom: theme.spacingFunction(8), + display: 'flex', + justifyContent: hasVPCConfigured ? 'space-between' : 'flex-end', + })} + > + {hasVPCConfigured && ( + + )} + + + + - - + + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx index 6f5c7b7b220..f3ec51281dd 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx @@ -5,60 +5,34 @@ import { Box, Checkbox, FormHelperText, - Notice, - TooltipIcon, Typography, } from '@linode/ui'; import * as React from 'react'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { MANAGE_NETWORKING_LEARN_MORE_LINK } from 'src/features/Databases/constants'; import { useFlags } from 'src/hooks/useFlags'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import type { ClusterSize, Engine, PrivateNetwork, VPC } from '@linode/api-v4'; +import type { ManageNetworkingFormValues } from './DatabaseManageNetworkingDrawer'; import type { Theme } from '@mui/material/styles'; -import type { FormikErrors } from 'formik'; - -interface DatabaseCreateValuesFormik { - allow_list: { - address: string; - error: string; - }[]; - cluster_size: ClusterSize; - engine: Engine; - label: string; - private_network: PrivateNetwork; - region: string; - type: string; -} interface DatabaseVPCSelectorProps { - errors: FormikErrors; // TODO (UIE-8903): Replace deprecated Formik with React Hook Form - mode: 'create' | 'networking'; - onChange: (field: string, value: boolean | null | number) => void; - onConfigurationChange?: (vpc: null | VPC) => void; - privateNetworkValues: PrivateNetwork; - resetFormFields?: ( - partialValues?: Partial - ) => void; - selectedRegionId: string; + region: string; } export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { - const { - errors, - mode, - onConfigurationChange, - onChange, - selectedRegionId, - resetFormFields, - privateNetworkValues, - } = props; - + const { region } = props; const flags = useFlags(); - const isCreate = mode === 'create'; - const { data: selectedRegion } = useRegionQuery(selectedRegionId); + const { control, setValue, trigger } = + useFormContext(); + const [vpcId, subnetId] = useWatch({ + control, + name: ['private_network.vpc_id', 'private_network.subnet_id'], + }); + + const { data: selectedRegion } = useRegionQuery(region); const regionSupportsVPCs = selectedRegion?.capabilities.includes('VPCs'); const { @@ -67,77 +41,27 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { isLoading, } = useAllVPCsQuery({ enabled: regionSupportsVPCs, - filter: { region: selectedRegionId }, + filter: { region }, }); const vpcErrorMessage = vpcsError && getAPIErrorOrDefault(vpcsError, 'Unable to load VPCs')[0].reason; - const selectedVPC = vpcs?.find( - (vpc) => vpc.id === privateNetworkValues.vpc_id - ); + const selectedVPC = vpcs?.find((vpc) => vpc.id === vpcId); const selectedSubnet = selectedVPC?.subnets.find( - (subnet) => subnet.id === privateNetworkValues.subnet_id + (subnet) => subnet.id === subnetId ); - const prevRegionId = React.useRef(undefined); const regionHasVPCs = Boolean(vpcs && vpcs.length > 0); const disableVPCSelectors = !!vpcsError || !regionSupportsVPCs || !regionHasVPCs; - const resetVPCConfiguration = () => { - resetFormFields?.({ - private_network: { - vpc_id: null, - subnet_id: null, - public_access: false, - }, - }); - }; - - React.useEffect(() => { - // When the selected region has changed, reset VPC configuration. - // Then switch back to default validation behavior - if (prevRegionId.current && prevRegionId.current !== selectedRegionId) { - resetVPCConfiguration(); - onConfigurationChange?.(null); - } - prevRegionId.current = selectedRegionId; - }, [selectedRegionId]); - - const vpcHelperTextCopy = !selectedRegionId + const vpcHelperTextCopy = !region ? 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.' : 'No VPC is available in the selected region.'; - /** Returns dynamic marginTop value used to center TooltipIcon in different scenarios */ - const getVPCTooltipIconMargin = () => { - const margins = { - longHelperText: '.75rem', - shortHelperText: '1.75rem', - noHelperText: '2.75rem', - errorText: '1.5rem', - errorTextWithLongHelperText: '-.5rem', - }; - if (disableVPCSelectors && vpcsError) - return margins.errorTextWithLongHelperText; - if (errors?.private_network?.vpc_id) return margins.errorText; - if (disableVPCSelectors && !selectedRegionId) return margins.longHelperText; - if (disableVPCSelectors && selectedRegionId) return margins.shortHelperText; - return margins.noHelperText; - }; - - const accessNotice = isCreate && ( - ({ - marginTop: theme.spacingFunction(20), - })} - text="The cluster will have public access by default if a VPC is not assigned." - variant="info" - /> - ); - return ( <> { Assign a VPC {flags.databaseVpcBeta && } - Assign this cluster to an existing VPC.{' '} { Learn more. - - { - onChange('private_network.subnet_id', null); // Always reset subnet selection when VPC changes - if (!value) { - onChange('private_network.public_access', false); - } - onConfigurationChange?.(value ?? null); - onChange('private_network.vpc_id', value?.id ?? null); - }} - options={vpcs ?? []} - placeholder="Select a VPC" - sx={{ width: '354px' }} - value={selectedVPC ?? null} - /> - + ( + { + setValue('private_network.subnet_id', null); // Always reset subnet selection when VPC changes + trigger('private_network.subnet_id'); + if (!value) { + setValue('private_network.public_access', false); + } + field.onChange(value?.id ?? null); + }} + options={vpcs ?? []} + placeholder="Select a VPC" + sx={{ width: '390px' }} + textFieldProps={{ + tooltipText: + 'A cluster may be assigned only to a VPC in the same region', + }} + value={selectedVPC ?? null} + /> + )} /> - - {selectedVPC ? ( + {selectedVPC && ( <> - `${subnet.label} (${subnet.ipv4})`} - label="Subnet" - onChange={(e, value) => { - onChange('private_network.subnet_id', value?.id ?? null); - }} - options={selectedVPC?.subnets ?? []} - placeholder="Select a subnet" - value={selectedSubnet ?? null} + ( + `${subnet.label} (${subnet.ipv4})`} + label="Subnet" + onChange={(e, value) => { + field.onChange(value?.id ?? null); + trigger('private_network.subnet_id'); + }} + options={selectedVPC?.subnets ?? []} + placeholder="Select a subnet" + value={selectedSubnet ?? null} + /> + )} /> ({ marginTop: theme.spacingFunction(20), })} > - { - onChange('private_network.public_access', value ?? null); - }} - text={'Enable public access'} - toolTipText={ - 'Adds a public endpoint to the database in addition to the private VPC endpoint.' - } + ( + <> + { + field.onChange(value ?? null); + }} + text={'Enable public access'} + toolTipText={ + 'Adds a public endpoint to the database in addition to the private VPC endpoint.' + } + /> + {fieldState.error?.message && ( + + {fieldState.error?.message} + + )} + + )} /> - {errors?.private_network?.public_access && ( - - {errors?.private_network?.public_access} - - )} - ) : ( - accessNotice )} ); From 638df4e439a775d0356bf54c80a6fb1132a56f46 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Tue, 21 Oct 2025 22:23:00 -0400 Subject: [PATCH 2/4] clean up --- .../DatabaseCreate/DatabaseCreate.tsx | 2 +- .../DatabaseCreateNetworkingConfiguration.tsx | 4 +- .../DatabaseCreate/DatabaseCreateVPC.tsx | 47 +++++ .../DatabaseVPC.tsx} | 55 +++-- .../DatabaseVPCSelector.test.tsx | 38 ++-- .../DatabaseCreate/DatabaseVPCSelector.tsx | 188 ------------------ .../DatabaseNetworking/DatabaseDetailVPC.tsx | 34 ++++ .../DatabaseManageNetworkingDrawer.tsx | 4 +- 8 files changed, 146 insertions(+), 226 deletions(-) create mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx rename packages/manager/src/features/Databases/{DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx => DatabaseCreate/DatabaseVPC.tsx} (81%) delete mode 100644 packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index d9b3b0d8aa2..7c670ebf370 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -53,7 +53,7 @@ export interface DatabaseCreateValues { cluster_size: ClusterSize; engine: Engine; label: string; - private_network?: PrivateNetwork; + private_network?: null | PrivateNetwork; region: string; type: string; } diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx index 30d03f0a799..46353595165 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateNetworkingConfiguration.tsx @@ -2,7 +2,7 @@ import { Typography } from '@linode/ui'; import * as React from 'react'; import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; -import { DatabaseVPCSelector } from './DatabaseVPCSelector'; +import { DatabaseCreateVPC } from './DatabaseCreateVPC'; import type { AccessProps } from './DatabaseCreateAccessControls'; import type { VPC } from '@linode/api-v4'; @@ -30,7 +30,7 @@ export const DatabaseCreateNetworkingConfiguration = ( - + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx new file mode 100644 index 00000000000..12eb5a25fbe --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; + +import { DatabaseVPC } from './DatabaseVPC'; + +import type { DatabaseCreateValues } from './DatabaseCreate'; +import type { VPC } from '@linode/api-v4'; + +interface DatabaseCreateVPCProps { + onChange: (selectedVPC: null | VPC) => void; +} + +export const DatabaseCreateVPC = (props: DatabaseCreateVPCProps) => { + const { onChange } = props; + + const { control } = + useFormContext>(); + + const { + control: networkControl, + setValue, + trigger, + } = useFormContext>(); + + const region = useWatch({ + control, + name: 'region', + }); + + const [vpcId, subnetId] = useWatch({ + control: networkControl, + name: ['private_network.vpc_id', 'private_network.subnet_id'], + }); + + return ( + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx similarity index 81% rename from packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx rename to packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx index f3ec51281dd..bef0491c4e6 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx @@ -5,32 +5,48 @@ import { Box, Checkbox, FormHelperText, + Notice, Typography, } from '@linode/ui'; import * as React from 'react'; -import { Controller, useFormContext, useWatch } from 'react-hook-form'; +import type { Control, UseFormSetValue, UseFormTrigger } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { MANAGE_NETWORKING_LEARN_MORE_LINK } from 'src/features/Databases/constants'; import { useFlags } from 'src/hooks/useFlags'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import type { ManageNetworkingFormValues } from './DatabaseManageNetworkingDrawer'; +import type { PrivateNetwork, VPC } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; -interface DatabaseVPCSelectorProps { +interface NetworkValues { + private_network?: null | PrivateNetwork; +} + +interface DatabaseVPCProps { + control: Control; + mode: 'create' | 'networking'; + onChange?: (selectedVPC: null | VPC) => void; region: string; + setValue: UseFormSetValue; + subnetId: null | number; + trigger: UseFormTrigger; + vpcId: null | number; } -export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { - const { region } = props; - const flags = useFlags(); - const { control, setValue, trigger } = - useFormContext(); - const [vpcId, subnetId] = useWatch({ +export const DatabaseVPC = (props: DatabaseVPCProps) => { + const { + onChange, + setValue, + trigger, control, - name: ['private_network.vpc_id', 'private_network.subnet_id'], - }); + region, + vpcId, + subnetId, + mode, + } = props; + const flags = useFlags(); const { data: selectedRegion } = useRegionQuery(region); const regionSupportsVPCs = selectedRegion?.capabilities.includes('VPCs'); @@ -38,7 +54,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { const { data: vpcs, error: vpcsError, - isLoading, + isLoading: vpcsLoading, } = useAllVPCsQuery({ enabled: regionSupportsVPCs, filter: { region }, @@ -93,7 +109,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { errorText={vpcErrorMessage || fieldState.error?.message} helperText={disableVPCSelectors ? vpcHelperTextCopy : undefined} label="VPC" - loading={isLoading} + loading={vpcsLoading} noOptionsText="There are no VPCs in the selected region." onChange={(e, value) => { setValue('private_network.subnet_id', null); // Always reset subnet selection when VPC changes @@ -101,6 +117,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { if (!value) { setValue('private_network.public_access', false); } + onChange?.(value ?? null); // Update VPC in DatabaseCreate.tsx field.onChange(value?.id ?? null); }} options={vpcs ?? []} @@ -115,7 +132,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { )} /> - {selectedVPC && ( + {selectedVPC ? ( <> { /> + ) : ( + mode === 'create' && ( + ({ + marginTop: theme.spacingFunction(20), + })} + text="The cluster will have public access by default if a VPC is not assigned." + variant="info" + /> + ) )} ); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx index 1ca6bade784..80182828b67 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { subnetFactory, vpcFactory } from 'src/factories'; -import { DatabaseVPCSelector } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector'; +import { DatabaseDetailVPC } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC'; import { renderWithTheme } from 'src/utilities/testHelpers'; import type { PrivateNetwork } from '@linode/api-v4'; @@ -64,7 +64,7 @@ const vpcPlaceholder = 'Select a VPC'; const subnetPlaceholder = 'Select a subnet'; const mockMode: 'create' | 'networking' = 'create'; -describe('DatabaseVPCSelector', () => { +describe('DatabaseDetailVPC', () => { const mockProps = { errors: {}, onChange: vi.fn(), @@ -92,13 +92,13 @@ describe('DatabaseVPCSelector', () => { }); it('Should render the VPC selector heading', () => { - renderWithTheme(); + renderWithTheme(); const vpcField = screen.getByText('Assign a VPC', { exact: true }); expect(vpcField).toBeInTheDocument(); }); it('Should render VPC autocomplete in initial disabled state', () => { - renderWithTheme(); + renderWithTheme(); const vpcSelector = screen.getByTestId(vpcSelectorTestId); expect(vpcSelector).toBeInTheDocument(); const vpcSelectorInput = screen.getByPlaceholderText(vpcPlaceholder); @@ -127,7 +127,7 @@ describe('DatabaseVPCSelector', () => { }); const mockEnabledProps = { ...mockProps, selectedRegionId: 'us-east' }; - renderWithTheme(); + renderWithTheme(); const vpcSelector = screen.getByTestId(vpcSelectorTestId); expect(vpcSelector).toBeInTheDocument(); @@ -156,7 +156,7 @@ describe('DatabaseVPCSelector', () => { privateNetworkValues: mockPrivateNetwork, selectedRegionId: 'us-east', }; - renderWithTheme(); + renderWithTheme(); const vpcInput = screen.getByPlaceholderText( vpcPlaceholder @@ -189,7 +189,7 @@ describe('DatabaseVPCSelector', () => { privateNetworkValues: mockPrivateNetwork, selectedRegionId: 'us-east', }; - renderWithTheme(); + renderWithTheme(); const vpcInput = screen.getByPlaceholderText( vpcPlaceholder @@ -244,7 +244,7 @@ describe('DatabaseVPCSelector', () => { const onConfigurationChange = vi.fn(); const { rerender } = renderWithTheme( - { queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); rerender( - { const onConfigurationChange = vi.fn(); const { rerender } = renderWithTheme( - { queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); rerender( - { }); it('Should show long helper text when no region is selected', () => { - renderWithTheme(); + renderWithTheme(); const expectedHelperText = screen.getByText(initialHelperText, { exact: true, }); @@ -322,7 +322,7 @@ describe('DatabaseVPCSelector', () => { queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); renderWithTheme( - + ); const expectedHelperText = screen.getByText(altHelperText, { exact: true }); @@ -341,7 +341,7 @@ describe('DatabaseVPCSelector', () => { }); renderWithTheme( - + ); const expectedAltHelperText = screen.queryByText(altHelperText); const expectedInitialHelperText = screen.queryByText(initialHelperText); @@ -364,7 +364,7 @@ describe('DatabaseVPCSelector', () => { }; renderWithTheme( - { }; renderWithTheme( - { }; renderWithTheme( - { }; renderWithTheme( - { }; renderWithTheme( - void; -} - -export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { - const { onChange } = props; - const flags = useFlags(); - const { control, setValue } = useFormContext(); - - const [region, vpcId, subnetId] = useWatch({ - control, - name: ['region', 'private_network.vpc_id', 'private_network.subnet_id'], - }); - - const { data: selectedRegion } = useRegionQuery(region); - const regionSupportsVPCs = selectedRegion?.capabilities.includes('VPCs'); - - const { - data: vpcs, - error: vpcsError, - isLoading, - } = useAllVPCsQuery({ - enabled: regionSupportsVPCs, - filter: { region }, - }); - - const vpcErrorMessage = - vpcsError && - getAPIErrorOrDefault(vpcsError, 'Unable to load VPCs')[0].reason; - - const selectedVPC = vpcs?.find((vpc) => vpc.id === vpcId); - - const selectedSubnet = selectedVPC?.subnets.find( - (subnet) => subnet.id === subnetId - ); - - const regionHasVPCs = Boolean(vpcs && vpcs.length > 0); - const disableVPCSelectors = - !!vpcsError || !regionSupportsVPCs || !regionHasVPCs; - - const vpcHelperTextCopy = !region - ? 'In the Select Engine and Region section, select a region with an existing VPC to see available VPCs.' - : 'No VPC is available in the selected region.'; - - return ( - <> - ({ - display: 'flex', - marginTop: theme.spacingFunction(20), - marginBottom: theme.spacingFunction(4), - })} - > - Assign a VPC - {flags.databaseVpcBeta && } - - - Assign this cluster to an existing VPC.{' '} - - Learn more. - - - - ( - { - setValue('private_network.subnet_id', null); // Always reset subnet selection when VPC changes - if (!value) { - setValue('private_network.public_access', false); - } - onChange(value ?? null); // Update VPC in DatabaseCreate.tsx - field.onChange(value?.id ?? null); - }} - options={vpcs ?? []} - placeholder="Select a VPC" - sx={{ width: '390px' }} - textFieldProps={{ - tooltipText: - 'A cluster may be assigned only to a VPC in the same region', - }} - value={selectedVPC ?? null} - /> - )} - /> - - {selectedVPC ? ( - <> - ( - `${subnet.label} (${subnet.ipv4})`} - label="Subnet" - onChange={(e, value) => { - field.onChange(value?.id ?? null); - }} - options={selectedVPC?.subnets ?? []} - placeholder="Select a subnet" - value={selectedSubnet ?? null} - /> - )} - /> - ({ - marginTop: theme.spacingFunction(20), - })} - > - ( - <> - { - field.onChange(value ?? null); - }} - text={'Enable public access'} - toolTipText={ - 'Adds a public endpoint to the database in addition to the private VPC endpoint.' - } - /> - {fieldState.error?.message && ( - - {fieldState.error?.message} - - )} - - )} - /> - - - ) : ( - ({ - marginTop: theme.spacingFunction(20), - })} - text="The cluster will have public access by default if a VPC is not assigned." - variant="info" - /> - )} - - ); -}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx new file mode 100644 index 00000000000..c14120cd103 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; + +import { DatabaseVPC } from '../../DatabaseCreate/DatabaseVPC'; + +import type { ManageNetworkingFormValues } from './DatabaseManageNetworkingDrawer'; + +interface DatabaseDetailVPCProps { + region: string; +} + +export const DatabaseDetailVPC = (props: DatabaseDetailVPCProps) => { + const { region } = props; + + const { control, setValue, trigger } = + useFormContext(); + + const [vpcId, subnetId] = useWatch({ + control, + name: ['private_network.vpc_id', 'private_network.subnet_id'], + }); + + return ( + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx index a6ef28297ab..c3d9bca2806 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseManageNetworkingDrawer.tsx @@ -7,7 +7,7 @@ import { enqueueSnackbar } from 'notistack'; import * as React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { DatabaseVPCSelector } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector'; +import { DatabaseDetailVPC } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC'; import type { Database, UpdateDatabasePayload, VPC } from '@linode/api-v4'; import type { Theme } from '@linode/ui'; @@ -110,7 +110,7 @@ const DatabaseManageNetworkingDrawer = (props: Props) => { )}
- + ({ marginTop: theme.spacingFunction(50), From bb71e41ba46a1de05d8c260ec5987eae296d20a2 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 22 Oct 2025 09:00:36 -0400 Subject: [PATCH 3/4] update unit tests --- .../DatabaseCreate/DatabaseCreateVPC.tsx | 8 +- ...Selector.test.tsx => DatabaseVPC.test.tsx} | 355 +++++++----------- .../Databases/DatabaseCreate/DatabaseVPC.tsx | 6 +- 3 files changed, 135 insertions(+), 234 deletions(-) rename packages/manager/src/features/Databases/DatabaseCreate/{DatabaseVPCSelector.test.tsx => DatabaseVPC.test.tsx} (57%) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx index 12eb5a25fbe..6143a0c9037 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx @@ -16,11 +16,8 @@ export const DatabaseCreateVPC = (props: DatabaseCreateVPCProps) => { const { control } = useFormContext>(); - const { - control: networkControl, - setValue, - trigger, - } = useFormContext>(); + const { control: networkControl, setValue } = + useFormContext>(); const region = useWatch({ control, @@ -40,7 +37,6 @@ export const DatabaseCreateVPC = (props: DatabaseCreateVPCProps) => { region={region} setValue={setValue} subnetId={subnetId} - trigger={trigger} vpcId={vpcId} /> ); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.test.tsx similarity index 57% rename from packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx rename to packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.test.tsx index 80182828b67..a0b01817574 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.test.tsx @@ -5,9 +5,10 @@ import * as React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { subnetFactory, vpcFactory } from 'src/factories'; -import { DatabaseDetailVPC } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { DatabaseCreateVPC } from 'src/features/Databases/DatabaseCreate/DatabaseCreateVPC'; +import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; +import type { DatabaseCreateValues } from './DatabaseCreate'; import type { PrivateNetwork } from '@linode/api-v4'; // Hoist query mocks @@ -62,21 +63,12 @@ const vpcSelectorTestId = 'database-vpc-selector'; const subnetSelectorTestId = 'database-subnet-selector'; const vpcPlaceholder = 'Select a VPC'; const subnetPlaceholder = 'Select a subnet'; -const mockMode: 'create' | 'networking' = 'create'; - -describe('DatabaseDetailVPC', () => { - const mockProps = { - errors: {}, - onChange: vi.fn(), - onConfigurationChange: vi.fn(), - privateNetworkValues: { - vpc_id: null, - subnet_id: null, - public_access: false, - }, - resetFormFields: vi.fn(), - selectedRegionId: '', - mode: mockMode, + +describe('DatabaseCreateVPC', () => { + const defaultPrivateNetworkValues = { + vpc_id: null, + subnet_id: null, + public_access: false, }; beforeEach(() => { @@ -92,13 +84,23 @@ describe('DatabaseDetailVPC', () => { }); it('Should render the VPC selector heading', () => { - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { private_network: defaultPrivateNetworkValues }, + }, + }); const vpcField = screen.getByText('Assign a VPC', { exact: true }); expect(vpcField).toBeInTheDocument(); }); it('Should render VPC autocomplete in initial disabled state', () => { - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { private_network: defaultPrivateNetworkValues }, + }, + }); const vpcSelector = screen.getByTestId(vpcSelectorTestId); expect(vpcSelector).toBeInTheDocument(); const vpcSelectorInput = screen.getByPlaceholderText(vpcPlaceholder); @@ -126,8 +128,15 @@ describe('DatabaseDetailVPC', () => { isLoading: false, }); - const mockEnabledProps = { ...mockProps, selectedRegionId: 'us-east' }; - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: 'us-east', + }, + }, + }); const vpcSelector = screen.getByTestId(vpcSelectorTestId); expect(vpcSelector).toBeInTheDocument(); @@ -151,12 +160,15 @@ describe('DatabaseDetailVPC', () => { public_access: false, }; - const mockEnabledProps = { - ...mockProps, - privateNetworkValues: mockPrivateNetwork, - selectedRegionId: 'us-east', - }; - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: mockPrivateNetwork, + region: 'us-east', + }, + }, + }); const vpcInput = screen.getByPlaceholderText( vpcPlaceholder @@ -184,12 +196,15 @@ describe('DatabaseDetailVPC', () => { public_access: true, }; - const mockEnabledProps = { - ...mockProps, - privateNetworkValues: mockPrivateNetwork, - selectedRegionId: 'us-east', - }; - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: mockPrivateNetwork, + region: 'us-east', + }, + }, + }); const vpcInput = screen.getByPlaceholderText( vpcPlaceholder @@ -243,29 +258,29 @@ describe('DatabaseDetailVPC', () => { const resetFormFields = vi.fn(); const onConfigurationChange = vi.fn(); - const { rerender } = renderWithTheme( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: mockPrivateNetwork, + region: 'us-east', + }, + }, + }); // Change region to a new one queryMocks.useRegionQuery.mockReturnValue({ data: region2 }); queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); - rerender( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: mockPrivateNetwork, + region: 'us-west', + }, + }, + }); expect(resetFormFields).toHaveBeenCalled(); expect(onConfigurationChange).toHaveBeenCalledWith(null); @@ -283,34 +298,44 @@ describe('DatabaseDetailVPC', () => { const resetFormFields = vi.fn(); const onConfigurationChange = vi.fn(); - const { rerender } = renderWithTheme( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: '', + }, + }, + }); // Now render with a valid region queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); - rerender( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: 'us-east', + }, + }, + }); expect(resetFormFields).not.toHaveBeenCalled(); expect(onConfigurationChange).not.toHaveBeenCalledWith(null); }); it('Should show long helper text when no region is selected', () => { - renderWithTheme(); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: '', + }, + }, + }); const expectedHelperText = screen.getByText(initialHelperText, { exact: true, }); @@ -321,9 +346,15 @@ describe('DatabaseDetailVPC', () => { queryMocks.useRegionQuery.mockReturnValue({ data: mockRegion }); queryMocks.useAllVPCsQuery.mockReturnValue({ data: [], isLoading: false }); - renderWithTheme( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: 'us-east', + }, + }, + }); const expectedHelperText = screen.getByText(altHelperText, { exact: true }); expect(expectedHelperText).toBeInTheDocument(); @@ -340,77 +371,24 @@ describe('DatabaseDetailVPC', () => { isLoading: false, }); - renderWithTheme( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: defaultPrivateNetworkValues, + region: 'us-east', + }, + }, + }); + const expectedAltHelperText = screen.queryByText(altHelperText); const expectedInitialHelperText = screen.queryByText(initialHelperText); expect(expectedAltHelperText).not.toBeInTheDocument(); expect(expectedInitialHelperText).not.toBeInTheDocument(); }); - it('Should show vpc validation error text when there is a vpc error', () => { - setUpBaseMocks(); - const mockPrivateNetwork: PrivateNetwork = { - vpc_id: 1234, - subnet_id: null, - public_access: false, - }; - - const mockErrors = { - private_network: { - vpc_id: 'VPC is required.', - }, - }; - - renderWithTheme( - - ); - - const subnetSelector = screen.getByTestId(subnetSelectorTestId); - expect(subnetSelector).toBeInTheDocument(); - const expectedValidationError = screen.getByText('VPC is required.'); - expect(expectedValidationError).toBeInTheDocument(); - }); - - it('Should show subnet validation error text when there is a subnet error', () => { - setUpBaseMocks(); - const mockPrivateNetwork: PrivateNetwork = { - vpc_id: 1234, - subnet_id: null, - public_access: false, - }; - - const mockErrors = { - private_network: { - subnet_id: 'Subnet is required.', - }, - }; - - renderWithTheme( - - ); - - const subnetSelector = screen.getByTestId(subnetSelectorTestId); - expect(subnetSelector).toBeInTheDocument(); - const expectedValidationError = screen.getByText('Subnet is required.'); - expect(expectedValidationError).toBeInTheDocument(); - }); - - it('Should clear subnet field when the VPC field is cleared', async () => { + it('Should hide the subnet field when the VPC field is cleared', async () => { setUpBaseMocks(); - const onChange = vi.fn(); - // Start with both VPC and subnet selected const mockPrivateNetwork: PrivateNetwork = { vpc_id: 1234, @@ -418,102 +396,29 @@ describe('DatabaseDetailVPC', () => { public_access: false, }; - renderWithTheme( - - ); + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + private_network: mockPrivateNetwork, + region: 'us-east', + }, + }, + }); - // Simulate clearing the VPC field (user clears the Autocomplete) + // Clear VPC selection const vpcSelector = screen.getByTestId(vpcSelectorTestId); const clearButton = vpcSelector.querySelector( 'button[title="Clear"]' ) as HTMLElement; await userEvent.click(clearButton); - // ...assertions as above... - expect(onChange).toHaveBeenCalledWith('private_network.vpc_id', null); - expect(onChange).toHaveBeenCalledWith('private_network.subnet_id', null); - expect(onChange).toHaveBeenCalledWith( - 'private_network.public_access', - false - ); - }); - - it('Should call onChange for the VPC field when a value is selected', async () => { - setUpBaseMocks(); - const onChange = vi.fn(); - - // Start with no VPC selected - const mockPrivateNetwork: PrivateNetwork = { - vpc_id: null, - subnet_id: null, - public_access: false, - }; - renderWithTheme( - - ); - - // Simulate selecting a VPC from the Autocomplete - const vpcInput = screen.getByPlaceholderText( - vpcPlaceholder - ) as HTMLInputElement; - // Open the autocomplete dropdown - await userEvent.click(vpcInput); - - // Select the option - const newVPC = await screen.findByText('VPC 1'); - await userEvent.click(newVPC); - - expect(onChange).toHaveBeenCalledWith( - 'private_network.vpc_id', - mockVPCWithSubnet.id - ); - }); - - it('Should call onChange for the Subnet field when subnet value is selected', async () => { - setUpBaseMocks(); - const onChange = vi.fn(); - - // Start with VPC selected and no subnet selection - const mockPrivateNetwork: PrivateNetwork = { - vpc_id: 1234, - subnet_id: null, - public_access: false, - }; - - renderWithTheme( - - ); - - // Simulate selecting a Subnet from the Autocomplete - const subnetInput = screen.getByPlaceholderText( - subnetPlaceholder - ) as HTMLInputElement; - - await userEvent.click(subnetInput); - - // Select the option - const expectedSubnetLabel = `${mockSubnets[0].label} (${mockSubnets[0].ipv4})`; - const newSubnet = await screen.findByText(expectedSubnetLabel); - await userEvent.click(newSubnet); - - expect(onChange).toHaveBeenCalledWith( - 'private_network.subnet_id', - mockSubnets[0].id - ); + const subnetSelector = screen.queryByTestId(subnetSelectorTestId); + expect(subnetSelector).not.toBeInTheDocument(); + expect( + screen.getByText( + 'The cluster will have public access by default if a VPC is not assigned.' + ) + ).toBeVisible(); }); }); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx index bef0491c4e6..c7d0a1bfb5f 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx @@ -31,7 +31,7 @@ interface DatabaseVPCProps { region: string; setValue: UseFormSetValue; subnetId: null | number; - trigger: UseFormTrigger; + trigger?: UseFormTrigger; vpcId: null | number; } @@ -113,7 +113,7 @@ export const DatabaseVPC = (props: DatabaseVPCProps) => { noOptionsText="There are no VPCs in the selected region." onChange={(e, value) => { setValue('private_network.subnet_id', null); // Always reset subnet selection when VPC changes - trigger('private_network.subnet_id'); + trigger?.('private_network.subnet_id'); if (!value) { setValue('private_network.public_access', false); } @@ -146,7 +146,7 @@ export const DatabaseVPC = (props: DatabaseVPCProps) => { label="Subnet" onChange={(e, value) => { field.onChange(value?.id ?? null); - trigger('private_network.subnet_id'); + trigger?.('private_network.subnet_id'); }} options={selectedVPC?.subnets ?? []} placeholder="Select a subnet" From 9c2b8c5c4dd115e8fcc4faacc60e852633c3bd1f Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 29 Oct 2025 10:15:45 -0400 Subject: [PATCH 4/4] feedback --- .../DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx index c14120cd103..7ddaa0a9145 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx @@ -23,7 +23,7 @@ export const DatabaseDetailVPC = (props: DatabaseDetailVPCProps) => { return (