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..6143a0c9037 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateVPC.tsx @@ -0,0 +1,43 @@ +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 } = + 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/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 1ca6bade784..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 { DatabaseVPCSelector } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector'; -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('DatabaseVPCSelector', () => { - 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('DatabaseVPCSelector', () => { }); 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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('DatabaseVPCSelector', () => { 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/DatabaseVPCSelector.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx similarity index 78% rename from packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx rename to packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx index 843d55aefdf..c7d0a1bfb5f 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPCSelector.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseVPC.tsx @@ -9,31 +9,44 @@ import { 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 { MANAGE_NETWORKING_LEARN_MORE_LINK } from '../constants'; - -import type { DatabaseCreateValues } from './DatabaseCreate'; -import type { VPC } from '@linode/api-v4'; +import type { PrivateNetwork, VPC } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; -interface DatabaseVPCSelectorProps { - onChange: (selectedVPC: null | VPC) => void; +interface NetworkValues { + private_network?: null | PrivateNetwork; } -export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { - const { onChange } = props; - const flags = useFlags(); - const { control, setValue } = useFormContext(); +interface DatabaseVPCProps { + control: Control; + mode: 'create' | 'networking'; + onChange?: (selectedVPC: null | VPC) => void; + region: string; + setValue: UseFormSetValue; + subnetId: null | number; + trigger?: UseFormTrigger; + vpcId: null | number; +} - const [region, vpcId, subnetId] = useWatch({ +export const DatabaseVPC = (props: DatabaseVPCProps) => { + const { + onChange, + setValue, + trigger, control, - name: ['region', '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'); @@ -41,7 +54,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { const { data: vpcs, error: vpcsError, - isLoading, + isLoading: vpcsLoading, } = useAllVPCsQuery({ enabled: regionSupportsVPCs, filter: { region }, @@ -96,19 +109,20 @@ 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 + trigger?.('private_network.subnet_id'); if (!value) { setValue('private_network.public_access', false); } - onChange(value ?? null); // Update VPC in DatabaseCreate.tsx + onChange?.(value ?? null); // Update VPC in DatabaseCreate.tsx field.onChange(value?.id ?? null); }} 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', @@ -132,6 +146,7 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { label="Subnet" onChange={(e, value) => { field.onChange(value?.id ?? null); + trigger?.('private_network.subnet_id'); }} options={selectedVPC?.subnets ?? []} placeholder="Select a subnet" @@ -176,13 +191,15 @@ export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { ) : ( - ({ - marginTop: theme.spacingFunction(20), - })} - text="The cluster will have public access by default if a VPC is not assigned." - variant="info" - /> + 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/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC.tsx new file mode 100644 index 00000000000..7ddaa0a9145 --- /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 bd5c235a3fa..c3d9bca2806 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 { DatabaseDetailVPC } from 'src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseDetailVPC'; -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 deleted file mode 100644 index 6f5c7b7b220..00000000000 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseNetworking/DatabaseVPCSelector.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useAllVPCsQuery, useRegionQuery } from '@linode/queries'; -import { - Autocomplete, - BetaChip, - Box, - Checkbox, - FormHelperText, - Notice, - TooltipIcon, - Typography, -} from '@linode/ui'; -import * as React from 'react'; - -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 { 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; -} - -export const DatabaseVPCSelector = (props: DatabaseVPCSelectorProps) => { - const { - errors, - mode, - onConfigurationChange, - onChange, - selectedRegionId, - resetFormFields, - privateNetworkValues, - } = props; - - const flags = useFlags(); - const isCreate = mode === 'create'; - const { data: selectedRegion } = useRegionQuery(selectedRegionId); - const regionSupportsVPCs = selectedRegion?.capabilities.includes('VPCs'); - - const { - data: vpcs, - error: vpcsError, - isLoading, - } = useAllVPCsQuery({ - enabled: regionSupportsVPCs, - filter: { region: selectedRegionId }, - }); - - const vpcErrorMessage = - vpcsError && - getAPIErrorOrDefault(vpcsError, 'Unable to load VPCs')[0].reason; - - const selectedVPC = vpcs?.find( - (vpc) => vpc.id === privateNetworkValues.vpc_id - ); - - const selectedSubnet = selectedVPC?.subnets.find( - (subnet) => subnet.id === privateNetworkValues.subnet_id - ); - - 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 - ? '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 ( - <> - ({ - display: 'flex', - marginTop: theme.spacingFunction(20), - marginBottom: theme.spacingFunction(4), - })} - > - 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} - /> - - - - {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} - /> - ({ - 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.' - } - /> - {errors?.private_network?.public_access && ( - - {errors?.private_network?.public_access} - - )} - - - ) : ( - accessNotice - )} - - ); -}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/VPCIPv4Address.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/VPCIPv4Address.tsx index f6cabb33ee7..e21d95274c3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/VPCIPv4Address.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/VPCIPv4Address.tsx @@ -40,7 +40,8 @@ export const VPCIPv4Address = (props: Props) => { // Auto-assign should be checked if any of the following are true // - field value matches the identifier // - field value is undefined (because the API's default behavior is to auto-assign) - const shouldAutoAssign = fieldValue === autoAssignValue || fieldValue === undefined; + const shouldAutoAssign = + fieldValue === autoAssignValue || fieldValue === undefined; return (