From fdb225ee9ba5e329ff465a56273ae0fbb048c518 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 24 Mar 2025 18:11:20 -0400 Subject: [PATCH 01/16] save progress --- .../Linodes/LinodeCreate/Networking/VPC.tsx | 85 ++++++++++++++++++- .../LinodeCreate/Networking/VPCRanges.tsx | 63 ++++++++++++++ .../LinodeCreate/Networking/utilities.ts | 22 ++++- .../features/Linodes/LinodeCreate/VPC/VPC.tsx | 2 +- .../Linodes/LinodeCreate/utilities.ts | 6 +- 5 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx index fe7c5d3ee6b..e5c59669d99 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx @@ -1,13 +1,27 @@ import { useAllVPCsQuery, useRegionQuery } from '@linode/queries'; -import { Autocomplete, Box, Notice, Stack } from '@linode/ui'; +import { + Autocomplete, + Box, + Checkbox, + FormControlLabel, + Notice, + Stack, + TextField, + TooltipIcon, + Typography, +} from '@linode/ui'; import React, { useState } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { LinkButton } from 'src/components/LinkButton'; -import { REGION_CAVEAT_HELPER_TEXT } from 'src/features/VPCs/constants'; +import { + REGION_CAVEAT_HELPER_TEXT, + VPC_AUTO_ASSIGN_IPV4_TOOLTIP, +} from 'src/features/VPCs/constants'; import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer'; import type { LinodeCreateFormValues } from '../utilities'; +import { VPCRanges } from './VPCRanges'; interface Props { index: number; @@ -16,8 +30,9 @@ interface Props { export const VPC = ({ index }: Props) => { const { control, - setValue, resetField, + setValue, + formState } = useFormContext(); const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false); @@ -38,6 +53,8 @@ export const VPC = ({ index }: Props) => { const selectedVPC = vpcs?.find((vpc) => vpc.id === selectedVPCId); + console.log(formState.errors) + return ( @@ -118,6 +135,68 @@ export const VPC = ({ index }: Props) => { control={control} name={`linodeInterfaces.${index}.vpc.subnet_id`} /> + ( + <> + + + Auto-assign a VPC IPv4 address for this Linode in the VPC + + + + } + checked={field.value === 'auto'} + control={} + disabled={!regionSupportsVPCs} + onChange={(e, checked) => field.onChange(checked ? 'auto' : '')} + /> + {field.value !== 'auto' && ( + + )} + + )} + control={control} + name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.address`} + /> + ( + + + Assign a public IPv4 address for this Linode + + + + } + checked={field.value === 'auto'} + control={} + disabled={!regionSupportsVPCs} + onChange={(e, checked) => field.onChange(checked ? 'auto' : null)} + /> + )} + control={control} + name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`} + /> + { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx new file mode 100644 index 00000000000..7f1ee579fdb --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx @@ -0,0 +1,63 @@ +import { Box, IconButton, Stack, TextField } from '@linode/ui'; +import CloseIcon from '@mui/icons-material/Close'; +import React from 'react'; +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; + +import { LinkButton } from 'src/components/LinkButton'; + +import type { LinodeCreateFormValues } from '../utilities'; + +interface Props { + interfaceIndex: number; +} + +export const VPCRanges = ({ interfaceIndex }: Props) => { + const { control } = useFormContext(); + + const { append, fields, remove } = useFieldArray({ + control, + name: `linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges`, + }); + + return ( + + {fields.map((field, index) => ( + + ( + + )} + control={control} + name={`linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges.${index}.range`} + /> + remove(index)} + sx={{ padding: 0.75 }} + > + + + + ))} + + append({ range: '' })}> + Add IPv4 Range + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts index 734857a706d..5ef13a60d4a 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts @@ -62,11 +62,23 @@ export const getLegacyInterfaceFromLinodeInterface = ( } if (purpose === 'vpc') { + // New interfaces use "auto" to auto-assign a 1:1 NAT address, but legacy uses "any". + const nat = + linodeInterface.vpc?.ipv4?.addresses?.[0].nat_1_1_address === 'auto' + ? 'any' + : linodeInterface.vpc?.ipv4?.addresses?.[0].nat_1_1_address; + + // New interfaces use "auto" to auto-assign a VPC IP, but legacy will auto-assign with `null`. + const vpcIp = + linodeInterface.vpc?.ipv4?.addresses?.[0].address === 'auto' + ? null + : linodeInterface.vpc?.ipv4?.addresses?.[0].address; + return { ip_ranges: linodeInterface.vpc?.ipv4?.ranges?.map(({ range }) => range), ipv4: { - nat_1_1: linodeInterface.vpc?.ipv4?.addresses?.[0].nat_1_1_address, - vpc: linodeInterface.vpc?.ipv4?.addresses?.[0].address, + nat_1_1: nat, + vpc: vpcIp, }, purpose, subnet_id: linodeInterface.vpc?.subnet_id, @@ -77,7 +89,13 @@ export const getLegacyInterfaceFromLinodeInterface = ( return { purpose: 'public' }; }; +/** + * For example: + * `interfaces[0].ip_ranges[1]` should map to `linodeInterfaces.0.vpc.ipv4.ranges.1.range` + */ const legacyFieldToNewFieldMap = { + '].ipv4.nat_1_1': '].vpc.ipv4.addresses.0.nat_1_1_address', + '].ipv4.vpc': '].vpc.ipv4.addresses.0.address', '].label': '].vlan.vlan_lanel', '].subnet_id': '].vpc.subnet_id', }; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx index f01f31cbf2b..fd1f098592b 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx @@ -1,3 +1,4 @@ +import { useAllVPCsQuery, useRegionsQuery } from '@linode/queries'; import { Autocomplete, Box, @@ -21,7 +22,6 @@ import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP, } from 'src/features/VPCs/constants'; import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer'; -import { useRegionsQuery, useAllVPCsQuery } from '@linode/queries'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts index 187b44eb538..fe1683950ab 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts @@ -276,7 +276,11 @@ const defaultLinodeInterfaces: LinodeCreateInterface[] = [ public: {}, purpose: 'public', vlan: null, - vpc: null, + vpc: { + ipv4: { addresses: [{ address: 'auto', nat_1_1_address: null }]}, + vpc_id: null, + subnet_id: null, + }, }, ]; From 03addadf00a50e65c1882013c303e03918ea835f Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 24 Mar 2025 18:22:33 -0400 Subject: [PATCH 02/16] handle complex error case --- .../Linodes/LinodeCreate/Networking/utilities.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts index 5ef13a60d4a..8afb55d771e 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/utilities.ts @@ -89,10 +89,6 @@ export const getLegacyInterfaceFromLinodeInterface = ( return { purpose: 'public' }; }; -/** - * For example: - * `interfaces[0].ip_ranges[1]` should map to `linodeInterfaces.0.vpc.ipv4.ranges.1.range` - */ const legacyFieldToNewFieldMap = { '].ipv4.nat_1_1': '].vpc.ipv4.addresses.0.nat_1_1_address', '].ipv4.vpc': '].vpc.ipv4.addresses.0.address', @@ -118,6 +114,14 @@ export const transformLegacyInterfaceErrorsToLinodeInterfaceErrors = ( legacyFieldToNewFieldMap[key as keyof typeof legacyFieldToNewFieldMap] ); } + if (error.field && error.field.includes('ip_ranges')) { + // Handle the more complex case where: + // `interfaces[0].ip_ranges[1]` should map to `linodeInterfaces.0.vpc.ipv4.ranges.1.range` + error.field = error.field.replace( + /ip_ranges\[(\d+)\]/, + 'vpc.ipv4.ranges.$1.range' + ); + } if (error.field && error.field.startsWith('interfaces')) { error.field = error.field.replace('interfaces', 'linodeInterfaces'); } From 77ea0ecefba9fabf24a5c61fff850a865e510e10 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 24 Mar 2025 19:22:18 -0400 Subject: [PATCH 03/16] more fixes --- .../Linodes/LinodeCreate/Networking/VPC.tsx | 139 ++++++++++-------- .../LinodeCreate/Networking/VPCRanges.tsx | 80 ++++++---- packages/validation/src/linodes.schema.ts | 4 +- 3 files changed, 126 insertions(+), 97 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx index e5c59669d99..19ad44f545f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx @@ -3,6 +3,7 @@ import { Autocomplete, Box, Checkbox, + Divider, FormControlLabel, Notice, Stack, @@ -13,6 +14,7 @@ import { import React, { useState } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; +import { Link } from 'src/components/Link'; import { LinkButton } from 'src/components/LinkButton'; import { REGION_CAVEAT_HELPER_TEXT, @@ -20,9 +22,10 @@ import { } from 'src/features/VPCs/constants'; import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer'; -import type { LinodeCreateFormValues } from '../utilities'; import { VPCRanges } from './VPCRanges'; +import type { LinodeCreateFormValues } from '../utilities'; + interface Props { index: number; } @@ -32,7 +35,6 @@ export const VPC = ({ index }: Props) => { control, resetField, setValue, - formState } = useFormContext(); const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false); @@ -53,8 +55,6 @@ export const VPC = ({ index }: Props) => { const selectedVPC = vpcs?.find((vpc) => vpc.id === selectedVPCId); - console.log(formState.errors) - return ( @@ -135,67 +135,80 @@ export const VPC = ({ index }: Props) => { control={control} name={`linodeInterfaces.${index}.vpc.subnet_id`} /> - ( - <> - - - Auto-assign a VPC IPv4 address for this Linode in the VPC - - - - } - checked={field.value === 'auto'} - control={} - disabled={!regionSupportsVPCs} - onChange={(e, checked) => field.onChange(checked ? 'auto' : '')} - /> - {field.value !== 'auto' && ( - + ( + <> + + + Auto-assign a VPC IPv4 address for this Linode in the + VPC + + + + } + onChange={(e, checked) => + field.onChange(checked ? 'auto' : '') + } + checked={field.value === 'auto'} + control={} + disabled={!regionSupportsVPCs} /> - )} - - )} - control={control} - name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.address`} - /> - ( - - - Assign a public IPv4 address for this Linode - - - - } - checked={field.value === 'auto'} - control={} - disabled={!regionSupportsVPCs} - onChange={(e, checked) => field.onChange(checked ? 'auto' : null)} - /> - )} - control={control} - name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`} - /> + )} + + )} + control={control} + name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.address`} + /> + ( + <> + {fieldState.error?.message && ( + + )} + + + Assign a public IPv4 address for this Linode + + + + } + onChange={(e, checked) => + field.onChange(checked ? 'auto' : null) + } + checked={field.value === 'auto'} + control={} + disabled={!regionSupportsVPCs} + /> + + )} + control={control} + name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`} + /> + { return ( - {fields.map((field, index) => ( - - ( - - )} - control={control} - name={`linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges.${index}.range`} - /> - remove(index)} - sx={{ padding: 0.75 }} + + ({ font: theme.font.bold })}> + Assign additional IPv4 ranges + + + Assign additional IPv4 address ranges that the VPC can use to reach + services running on this Linode.{' '} + + Learn more + + . + + + + {fields.map((field, index) => ( + - - - - ))} + ( + + )} + control={control} + name={`linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges.${index}.range`} + /> + remove(index)} + sx={{ padding: 0.75 }} + > + + + + ))} + append({ range: '' })}> Add IPv4 Range diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index abfb12d6c65..3e179ce65dd 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -532,7 +532,7 @@ const BaseInterfaceIPv4AddressSchema = object({ }); const VPCInterfaceIPv4RangeSchema = object({ - range: string().required(), + range: string().required("Range is required."), }); const PublicInterfaceRangeSchema = object({ @@ -540,7 +540,7 @@ const PublicInterfaceRangeSchema = object({ }); const CreateVPCInterfaceIpv4AddressSchema = object({ - address: string().required(), + address: string().required("VPC IPv4 is required."), primary: boolean(), nat_1_1_address: string().nullable(), }); From 7624e85844a75ac183728f240be94a39abd2aa52 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 24 Mar 2025 19:30:44 -0400 Subject: [PATCH 04/16] dial in UI --- .../Linodes/LinodeCreate/Networking/VPC.tsx | 2 +- .../LinodeCreate/Networking/VPCRanges.tsx | 46 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx index 19ad44f545f..c5fac6329fa 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx @@ -209,7 +209,7 @@ export const VPC = ({ index }: Props) => { name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`} /> - + { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx index e279f439c96..24f69fb5b91 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx @@ -1,4 +1,10 @@ -import { Box, IconButton, Stack, TextField, Typography } from '@linode/ui'; +import { + IconButton, + Stack, + TextField, + TooltipIcon, + Typography, +} from '@linode/ui'; import CloseIcon from '@mui/icons-material/Close'; import React from 'react'; import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; @@ -9,10 +15,11 @@ import { LinkButton } from 'src/components/LinkButton'; import type { LinodeCreateFormValues } from '../utilities'; interface Props { + disabled: boolean; interfaceIndex: number; } -export const VPCRanges = ({ interfaceIndex }: Props) => { +export const VPCRanges = ({ disabled, interfaceIndex }: Props) => { const { control } = useFormContext(); const { append, fields, remove } = useFieldArray({ @@ -21,20 +28,7 @@ export const VPCRanges = ({ interfaceIndex }: Props) => { }); return ( - - - ({ font: theme.font.bold })}> - Assign additional IPv4 ranges - - - Assign additional IPv4 address ranges that the VPC can use to reach - services running on this Linode.{' '} - - Learn more - - . - - + {fields.map((field, index) => ( { ))} - - append({ range: '' })}> + + append({ range: '' })}> Add IPv4 Range - + + Assign additional IPv4 address ranges that the VPC can use to + reach services running on this Linode.{' '} + + Learn more + + . + + } + status="help" + sxTooltipIcon={{ p: 0.5 }} + /> + ); }; From d5a14c608d1a9524558133d117b203a950d4705c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 24 Mar 2025 20:24:25 -0400 Subject: [PATCH 05/16] default firewalls work on Linode Create flow --- .../LinodeCreate/Networking/InterfaceType.tsx | 33 ++++++++++++++- .../LinodeCreate/Networking/Networking.tsx | 17 ++++---- .../Linodes/LinodeCreate/Networking/VPC.tsx | 4 +- .../LinodeCreate/Networking/utilities.ts | 36 ++++++++++++++++ .../Linodes/LinodeCreate/utilities.ts | 41 +++++++++++-------- 5 files changed, 99 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx index 2f76d9f2e67..e86c8a35fd6 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceType.tsx @@ -1,3 +1,4 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; import { FormControl, FormControlLabel, Radio, RadioGroup } from '@linode/ui'; import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; @@ -11,7 +12,9 @@ interface Props { } export const InterfaceType = ({ index }: Props) => { - const { control } = useFormContext(); + const { control, setValue } = useFormContext(); + + const { data: firewallSettings } = useFirewallSettingsQuery(); const { field } = useController({ control, @@ -24,8 +27,34 @@ export const InterfaceType = ({ index }: Props) => { Network Connection { + field.onChange(value); + + if ( + value === 'vpc' && + firewallSettings?.default_firewall_ids.vpc_interface + ) { + setValue( + `linodeInterfaces.${index}.firewall_id`, + firewallSettings?.default_firewall_ids.vpc_interface + ); + } + + if ( + value === 'public' && + firewallSettings?.default_firewall_ids.public_interface + ) { + setValue( + `linodeInterfaces.${index}.firewall_id`, + firewallSettings?.default_firewall_ids.public_interface + ); + } + + if (value === 'vlan') { + setValue(`linodeInterfaces.${index}.firewall_id`, null); + } + }} aria-labelledby="network-interface" - onChange={field.onChange} row sx={{ mb: '0px !important' }} value={field.value} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx index 9ecbf859fd3..bd1ebda2ab0 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx @@ -1,3 +1,4 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; import { Button, Divider, @@ -13,6 +14,7 @@ import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { Firewall } from './Firewall'; import { InterfaceGeneration } from './InterfaceGeneration'; import { LinodeInterface } from './LinodeInterface'; +import { getDefaultInterfacePayload } from './utilities'; import type { LinodeCreateFormValues } from '../utilities'; @@ -22,6 +24,8 @@ export const Networking = () => { formState: { errors }, } = useFormContext(); + const { data: firewallSettings } = useFirewallSettingsQuery(); + const { append, fields, remove } = useFieldArray({ control, name: 'linodeInterfaces', @@ -42,16 +46,9 @@ export const Networking = () => { > Networking