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 && (
)}
-
+
+
);
};
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 (