From 168fa9873ac64a3818989015dd179d01e73a4b3a Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Tue, 19 Sep 2023 13:12:53 -0400 Subject: [PATCH 01/26] display vpc in the network interfaces column of the config table --- .../LinodesDetail/LinodeConfigs/ConfigRow.tsx | 30 +++---------- .../LinodeConfigs/InterfaceListItem.tsx | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx index bedb36727f5..66611751a80 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx @@ -1,4 +1,4 @@ -import { Config, Interface } from '@linode/api-v4/lib/linodes'; +import { Config } from '@linode/api-v4/lib/linodes'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -9,6 +9,7 @@ import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeKernelQuery } from 'src/queries/linodes/linodes'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; +import { InterfaceListItem } from './InterfaceListItem'; import { ConfigActionMenu } from './LinodeConfigActionMenu'; interface Props { @@ -59,7 +60,7 @@ export const ConfigRow = React.memo((props: Props) => { return undefined; } return ( -
  • +
  • /dev/{thisDevice} - {label}
  • ); @@ -76,17 +77,12 @@ export const ConfigRow = React.memo((props: Props) => { const InterfaceList = ( {interfaces.map((interfaceEntry, idx) => { - // The order of the config.interfaces array as returned by the API is significant. - // Index 0 is eth0, index 1 is eth1, index 2 is eth2. - const interfaceName = `eth${idx}`; - return ( -
  • - {interfaceName} – {getInterfaceLabel(interfaceEntry)} -
  • + /> ); })}
    @@ -132,15 +128,3 @@ const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({ }, padding: '0 !important', }); - -export const getInterfaceLabel = (configInterface: Interface): string => { - if (configInterface.purpose === 'public') { - return 'Public Internet'; - } - - const interfaceLabel = configInterface.label; - const ipamAddress = configInterface.ipam_address; - const hasIPAM = Boolean(ipamAddress); - - return `VLAN: ${interfaceLabel} ${hasIPAM ? `(${ipamAddress})` : ''}`; -}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx new file mode 100644 index 00000000000..d1c0c21f62d --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx @@ -0,0 +1,44 @@ +import { Interface } from '@linode/api-v4/lib/linodes'; +import React from 'react'; + +import { useVPCQuery } from 'src/queries/vpcs'; + +interface Props { + idx: number; + interfaceEntry: Interface; +} + +export const InterfaceListItem = (props: Props) => { + const { idx, interfaceEntry } = props; + const { data: vpc } = useVPCQuery( + interfaceEntry?.vpc_id ?? -1, + Boolean(interfaceEntry.vpc_id) + ); + + // The order of the config.interfaces array as returned by the API is significant. + // Index 0 is eth0, index 1 is eth1, index 2 is eth2. + const interfaceName = `eth${idx}`; + + const getInterfaceLabel = (configInterface: Interface): string => { + if (configInterface.purpose === 'public') { + return 'Public Internet'; + } + + const interfaceLabel = configInterface.label; + + if (configInterface.purpose === 'vlan') { + const ipamAddress = configInterface.ipam_address; + const hasIPAM = Boolean(ipamAddress); + return `VLAN: ${interfaceLabel} ${hasIPAM ? `(${ipamAddress})` : ''}`; + } + + const vpcIpv4 = configInterface.ipv4?.vpc; + return `VPC: ${vpc?.label} ${Boolean(vpcIpv4) ? `(${vpcIpv4})` : ''}`; + }; + + return ( +
  • + {interfaceName} – {getInterfaceLabel(interfaceEntry)} +
  • + ); +}; From e325b51a3991cf5b2662cb95fdaedcc9fdbfe821 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 21 Sep 2023 12:25:19 -0400 Subject: [PATCH 02/26] linode add config vpc state --- packages/api-v4/src/linodes/types.ts | 2 +- packages/api-v4/src/regions/types.ts | 3 +- .../LinodeConfigs/LinodeConfigDialog.tsx | 13 +- .../LinodeSettings/InterfaceSelect.tsx | 192 +++++++++++++++++- packages/manager/src/queries/vpcs.ts | 5 +- 5 files changed, 208 insertions(+), 7 deletions(-) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 61489c18d7f..ec32eec4fd9 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -151,7 +151,7 @@ export type LinodeStatus = export type InterfacePurpose = 'public' | 'vlan' | 'vpc'; export interface ConfigInterfaceIPv4 { - vpc?: string; + vpc?: string | null; nat_1_1?: string; } diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index 3ec6f906035..196b31a512b 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -9,7 +9,8 @@ export type Capabilities = | 'Vlans' | 'Bare Metal' | 'Metadata' - | 'Premium Plans'; + | 'Premium Plans' + | 'VPCs'; export interface DNSResolvers { ipv4: string; // Comma-separated IP addresses diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 8990db27843..dab1560e007 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -231,6 +231,11 @@ export const LinodeConfigDialog = (props: Props) => { thisRegion.id === linode?.region && thisRegion.capabilities.includes('Vlans') ); + const regionHasVPCs = regions.some( + (thisRegion) => + thisRegion.id === linode?.region && + thisRegion.capabilities.includes('VPCs') + ); const showVlans = regionHasVLANS; @@ -313,7 +318,7 @@ export const LinodeConfigDialog = (props: Props) => { configData.initrd = finnixDiskID; } - if (!regionHasVLANS) { + if (!regionHasVLANS || !regionHasVPCs) { delete configData.interfaces; } @@ -551,7 +556,7 @@ export const LinodeConfigDialog = (props: Props) => { }, [setFieldValue] ); - + console.log(values.interfaces); return ( { readOnly={isReadOnly} region={linode?.region} slotNumber={idx} + subnetId={thisInterface.subnet_id} + subnetLabel={thisInterface.subnetLabel} + vpcId={thisInterface.vpc_id} + vpcIpv4={thisInterface.ipv4?.vpc} /> ); })} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 360c0e16d86..a17d0dc991e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -7,10 +7,12 @@ import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; +import { Checkbox } from 'src/components/Checkbox'; import { Divider } from 'src/components/Divider'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { TextField } from 'src/components/TextField'; import { useVlansQuery } from 'src/queries/vlans'; +import { useSubnetsQuery, useVPCsQuery } from 'src/queries/vpcs'; import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics'; export interface Props { @@ -24,12 +26,18 @@ export interface Props { readOnly: boolean; region?: string; slotNumber: number; + subnetId?: null | number; + subnetLabel: null | string; + vpcId?: null | number; + vpcIpv4?: null | string; } // To allow for empty slots, which the API doesn't account for export type ExtendedPurpose = 'none' | InterfacePurpose; -export interface ExtendedInterface extends Omit { +export interface ExtendedInterface + extends Partial> { purpose: ExtendedPurpose; + subnetLabel?: null | string; } export const InterfaceSelect = (props: Props) => { @@ -47,6 +55,10 @@ export const InterfaceSelect = (props: Props) => { readOnly, region, slotNumber, + subnetId, + subnetLabel, + vpcId, + vpcIpv4, } = props; const [newVlan, setNewVlan] = React.useState(''); @@ -60,6 +72,10 @@ export const InterfaceSelect = (props: Props) => { label: 'VLAN', value: 'vlan', }, + { + label: 'VPC', + value: 'vpc', + }, { label: 'None', value: 'none', @@ -82,6 +98,28 @@ export const InterfaceSelect = (props: Props) => { vlanOptions.push({ label: newVlan, value: newVlan }); } + const { data: vpcs, isLoading: vpcsLoading } = useVPCsQuery({}, {}); + const vpcOptions = vpcs?.data.map((vpc) => ({ + id: vpc.id, + label: vpc.label, + value: vpc.label, + })); + + const { data: subnets, isLoading: subnetsLoading } = useSubnetsQuery( + vpcId ?? -1, + {}, + {}, + Boolean(vpcId) + ); + const subnetOptions = subnets?.data.map((subnet) => ({ + label: subnet.label, + subnet_id: subnet.id, + value: subnet.label, + })); + + const [autoAssignVpcIpv4, setAutoAssignVpcIpv4] = React.useState(true); + const [autoAssignLinodeIpv4, setAutoAssignLinodeIpv4] = React.useState(false); + const handlePurposeChange = (selected: Item) => { const purpose = selected.value; handleChange({ @@ -101,6 +139,79 @@ export const InterfaceSelect = (props: Props) => { purpose, }); + const handleVPCLabelChange = (selected: Item) => + handleChange({ + ipam_address: null, + label: selected?.value ?? '', + purpose, + subnet_id: null, + vpc_id: selected?.id, + }); + + const handleSubnetChange = (selected: Item) => + handleChange({ + ipam_address: null, + label, + purpose, + subnet_id: selected?.subnet_id, + subnetLabel: selected?.value, + vpc_id: vpcId, + }); + + const handleVpcIpv4Input = (e: React.ChangeEvent) => + handleChange({ + ipam_address: null, + ipv4: { + nat_1_1: autoAssignLinodeIpv4 ? 'any' : '', + vpc: e.target.value, + }, + label, + purpose, + subnet_id: subnetId, + subnetLabel, + vpc_id: vpcId, + }); + + React.useEffect(() => { + const changeObj = { + ipam_address: null, + label, + purpose, + subnet_id: subnetId, + subnetLabel, + vpc_id: vpcId, + }; + console.log( + `autoAssignVpcIpv4: ${autoAssignVpcIpv4}, autoAssignLinodeIpv4: ${autoAssignLinodeIpv4}` + ); + if (!autoAssignVpcIpv4 && autoAssignLinodeIpv4) { + handleChange({ + ...changeObj, + ipv4: { + nat_1_1: 'any', + vpc: vpcIpv4, + }, + }); + } else if ( + (autoAssignVpcIpv4 && autoAssignLinodeIpv4) || + autoAssignLinodeIpv4 + ) { + handleChange({ + ...changeObj, + ipv4: { + nat_1_1: 'any', + }, + }); + } else if (!autoAssignLinodeIpv4 && !autoAssignVpcIpv4) { + handleChange({ + ...changeObj, + ipv4: { + vpc: vpcIpv4, + }, + }); + } + }, [autoAssignVpcIpv4, autoAssignLinodeIpv4]); + const handleCreateOption = (_newVlan: string) => { setNewVlan(_newVlan); handleChange({ @@ -190,6 +301,85 @@ export const InterfaceSelect = (props: Props) => { ) : null} + {purpose === 'vpc' ? ( + + + + + subnetsLoading ? 'Loading...' : 'You have no Subnets.' + } + value={ + subnetOptions?.find( + (subnet) => subnet.value === subnetLabel + ) ?? null + } + creatable + createOptionPosition="first" + errorText={labelError} + inputId={`subnet-label-${slotNumber}`} + isClearable + isDisabled={readOnly} + label="Subnets" + onChange={handleSubnetChange} + options={subnetOptions} + placeholder="Select a Subnet" + /> + + + + setAutoAssignVpcIpv4( + (autoAssignVpcIpv4) => !autoAssignVpcIpv4 + ) + } + checked={autoAssignVpcIpv4} + text="Auto-assign a VPC IPv4 address for this Linode" + toolTipText="A range of non-internet facing IP addresses used in an internal network." + /> + + {!autoAssignVpcIpv4 && ( + + + + )} + + + setAutoAssignLinodeIpv4( + (autoAssignLinodeIpv4) => !autoAssignLinodeIpv4 + ) + } + checked={autoAssignLinodeIpv4} + text="Assign a public IPv4 address for this Linode" + toolTipText="Assign a public IP address for this VPC via 1:1 static NAT." + /> + + + + ) : null} {!fromAddonsPanel && ( { export const useSubnetsQuery = ( vpcID: number, params: Params, - filter: Filter + filter: Filter, + enabled: boolean = true ) => { return useQuery, APIError[]>( [vpcQueryKey, 'vpc', vpcID, subnetQueryKey, 'paginated', params, filter], () => getSubnets(vpcID, params, filter), - { keepPreviousData: true } + { enabled, keepPreviousData: true } ); }; From 21bc31877019fd2a8ef9c86010a6a8dffefac2f9 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 22 Sep 2023 11:19:24 -0400 Subject: [PATCH 03/26] fix label error on submit --- .../LinodeConfigs/LinodeConfigDialog.tsx | 5 ++++ .../LinodeSettings/InterfaceSelect.tsx | 30 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index dab1560e007..60989c2ff1a 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -363,6 +363,7 @@ export const LinodeConfigDialog = (props: Props) => { scrollErrorIntoView('linode-config-dialog'); }; + console.log(configData); /** Editing */ if (config) { return updateConfig(configData).then(handleSuccess).catch(handleError); @@ -867,6 +868,9 @@ export const LinodeConfigDialog = (props: Props) => { ipamError={ formik.errors[`interfaces[${idx}].ipam_address`] } + subnetError={ + formik.errors[`interfaces[${idx}].subnet_id`] + } ipamAddress={thisInterface.ipam_address} key={`eth${idx}-interface`} label={thisInterface.label} @@ -879,6 +883,7 @@ export const LinodeConfigDialog = (props: Props) => { subnetLabel={thisInterface.subnetLabel} vpcId={thisInterface.vpc_id} vpcIpv4={thisInterface.ipv4?.vpc} + vpcLabel={thisInterface.vpcLabel} /> ); })} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index a17d0dc991e..35825a6eaba 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -26,10 +26,12 @@ export interface Props { readOnly: boolean; region?: string; slotNumber: number; + subnetError?: string; subnetId?: null | number; subnetLabel: null | string; vpcId?: null | number; vpcIpv4?: null | string; + vpcLabel?: null | string; } // To allow for empty slots, which the API doesn't account for @@ -38,6 +40,7 @@ export interface ExtendedInterface extends Partial> { purpose: ExtendedPurpose; subnetLabel?: null | string; + vpcLabel?: null | string; } export const InterfaceSelect = (props: Props) => { @@ -55,10 +58,12 @@ export const InterfaceSelect = (props: Props) => { readOnly, region, slotNumber, + subnetError, subnetId, subnetLabel, vpcId, vpcIpv4, + vpcLabel, } = props; const [newVlan, setNewVlan] = React.useState(''); @@ -142,40 +147,47 @@ export const InterfaceSelect = (props: Props) => { const handleVPCLabelChange = (selected: Item) => handleChange({ ipam_address: null, - label: selected?.value ?? '', + label: null, purpose, subnet_id: null, vpc_id: selected?.id, + vpcLabel: selected?.value ?? '', }); const handleSubnetChange = (selected: Item) => handleChange({ ipam_address: null, - label, + label: null, purpose, subnet_id: selected?.subnet_id, subnetLabel: selected?.value, vpc_id: vpcId, + vpcLabel, }); - const handleVpcIpv4Input = (e: React.ChangeEvent) => - handleChange({ + const handleVpcIpv4Input = (e: React.ChangeEvent) => { + const changeObj = { ipam_address: null, ipv4: { - nat_1_1: autoAssignLinodeIpv4 ? 'any' : '', vpc: e.target.value, }, - label, + label: null, purpose, subnet_id: subnetId, subnetLabel, vpc_id: vpcId, - }); + vpcLabel, + }; + if (autoAssignLinodeIpv4) { + changeObj.ipv4.nat_1_1 = 'any'; + } + handleChange(changeObj); + }; React.useEffect(() => { const changeObj = { ipam_address: null, - label, + label: null, purpose, subnet_id: subnetId, subnetLabel, @@ -334,7 +346,7 @@ export const InterfaceSelect = (props: Props) => { } creatable createOptionPosition="first" - errorText={labelError} + errorText={subnetError} inputId={`subnet-label-${slotNumber}`} isClearable isDisabled={readOnly} From 6e66e006fdde01287157a352925331d16e010752 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 22 Sep 2023 15:32:41 -0400 Subject: [PATCH 04/26] fix vpc select and errors not displaying --- packages/api-v4/src/linodes/types.ts | 2 +- .../LinodeConfigs/LinodeConfigDialog.tsx | 6 +++--- .../LinodeSettings/InterfaceSelect.tsx | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index ec32eec4fd9..a9d1e8a7bf0 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -266,7 +266,7 @@ export interface Disk { export type DiskStatus = 'ready' | 'not ready' | 'deleting'; export interface LinodeConfigCreationData { - label: string; + label?: string; devices: Devices; initrd: string | number | null; kernel?: string; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 60989c2ff1a..4d3709ef701 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -337,6 +337,7 @@ export const LinodeConfigDialog = (props: Props) => { }; const handleError = (error: APIError[]) => { + console.log(error) const mapErrorToStatus = (generalError: string) => formik.setStatus({ generalError }); @@ -558,6 +559,7 @@ export const LinodeConfigDialog = (props: Props) => { [setFieldValue] ); console.log(values.interfaces); + console.log(formik.errors); return ( { ipamError={ formik.errors[`interfaces[${idx}].ipam_address`] } - subnetError={ - formik.errors[`interfaces[${idx}].subnet_id`] - } ipamAddress={thisInterface.ipam_address} key={`eth${idx}-interface`} label={thisInterface.label} @@ -883,6 +882,7 @@ export const LinodeConfigDialog = (props: Props) => { subnetLabel={thisInterface.subnetLabel} vpcId={thisInterface.vpc_id} vpcIpv4={thisInterface.ipv4?.vpc} + vpcIpv4Error={formik.errors['ipv4.vpc']} vpcLabel={thisInterface.vpcLabel} /> ); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 35825a6eaba..37a36c9375e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -26,11 +26,11 @@ export interface Props { readOnly: boolean; region?: string; slotNumber: number; - subnetError?: string; subnetId?: null | number; subnetLabel: null | string; vpcId?: null | number; vpcIpv4?: null | string; + vpcIpv4Error?: string; vpcLabel?: null | string; } @@ -58,11 +58,11 @@ export const InterfaceSelect = (props: Props) => { readOnly, region, slotNumber, - subnetError, subnetId, subnetLabel, vpcId, vpcIpv4, + vpcIpv4Error, vpcLabel, } = props; @@ -192,10 +192,9 @@ export const InterfaceSelect = (props: Props) => { subnet_id: subnetId, subnetLabel, vpc_id: vpcId, + vpcLabel, }; - console.log( - `autoAssignVpcIpv4: ${autoAssignVpcIpv4}, autoAssignLinodeIpv4: ${autoAssignLinodeIpv4}` - ); + if (!autoAssignVpcIpv4 && autoAssignLinodeIpv4) { handleChange({ ...changeObj, @@ -321,9 +320,11 @@ export const InterfaceSelect = (props: Props) => { noOptionsMessage={() => vpcsLoading ? 'Loading...' : 'You have no VPCs.' } + value={ + vpcOptions?.find((vpc) => vpc.value === vpcLabel) ?? null + } creatable createOptionPosition="first" - errorText={labelError} inputId={`vpc-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -331,7 +332,6 @@ export const InterfaceSelect = (props: Props) => { onChange={handleVPCLabelChange} options={vpcOptions} placeholder="Select a VPC" - value={vpcOptions?.find((vpc) => vpc.value === label) ?? null} /> @@ -346,7 +346,6 @@ export const InterfaceSelect = (props: Props) => { } creatable createOptionPosition="first" - errorText={subnetError} inputId={`subnet-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -371,6 +370,7 @@ export const InterfaceSelect = (props: Props) => { {!autoAssignVpcIpv4 && ( Date: Fri, 22 Sep 2023 16:14:26 -0400 Subject: [PATCH 05/26] filter by region --- .../LinodesDetail/LinodeSettings/InterfaceSelect.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 37a36c9375e..3cdd59bf5f4 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -103,7 +103,12 @@ export const InterfaceSelect = (props: Props) => { vlanOptions.push({ label: newVlan, value: newVlan }); } - const { data: vpcs, isLoading: vpcsLoading } = useVPCsQuery({}, {}); + const { data: vpcs, isLoading: vpcsLoading } = useVPCsQuery( + {}, + { + ['region']: region, + } + ); const vpcOptions = vpcs?.data.map((vpc) => ({ id: vpc.id, label: vpc.label, From 272884078724bae7223c4db0258fa02fa14d11fa Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 25 Sep 2023 12:07:30 -0400 Subject: [PATCH 06/26] feature flag vpc option --- .../LinodeSettings/InterfaceSelect.tsx | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 3cdd59bf5f4..64096ac3bef 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -11,8 +11,11 @@ import { Checkbox } from 'src/components/Checkbox'; import { Divider } from 'src/components/Divider'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { TextField } from 'src/components/TextField'; +import { useFlags } from 'src/hooks/useFlags'; +import { useAccount } from 'src/queries/account'; import { useVlansQuery } from 'src/queries/vlans'; import { useSubnetsQuery, useVPCsQuery } from 'src/queries/vpcs'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics'; export interface Props { @@ -26,6 +29,9 @@ export interface Props { readOnly: boolean; region?: string; slotNumber: number; +} + +interface VpcState { subnetId?: null | number; subnetLabel: null | string; vpcId?: null | number; @@ -43,9 +49,19 @@ export interface ExtendedInterface vpcLabel?: null | string; } -export const InterfaceSelect = (props: Props) => { +type CombinedProps = Props & VpcState; + +export const InterfaceSelect = (props: CombinedProps) => { const theme = useTheme(); const isSmallBp = useMediaQuery(theme.breakpoints.down('sm')); + const flags = useFlags(); + const { data: account } = useAccount(); + + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); const { fromAddonsPanel, @@ -67,25 +83,7 @@ export const InterfaceSelect = (props: Props) => { } = props; const [newVlan, setNewVlan] = React.useState(''); - - const purposeOptions: Item[] = [ - { - label: 'Public Internet', - value: 'public', - }, - { - label: 'VLAN', - value: 'vlan', - }, - { - label: 'VPC', - value: 'vpc', - }, - { - label: 'None', - value: 'none', - }, - ]; + const purposeOptions = getPurposeOptions(showVPCs); const { data: vlans, isLoading } = useVlansQuery(); const vlanOptions = @@ -411,3 +409,29 @@ export const InterfaceSelect = (props: Props) => { ); }; + +const getPurposeOptions = (showVPCs: boolean) => { + const purposeOptions: Item[] = [ + { + label: 'Public Internet', + value: 'public', + }, + { + label: 'VLAN', + value: 'vlan', + }, + { + label: 'None', + value: 'none', + }, + ]; + + if (showVPCs) { + purposeOptions.splice(1, 0, { + label: 'VPC', + value: 'vpc', + }); + } + + return purposeOptions; +}; From 307c08927c7c0538234fd2bb8da640e40e87f2d8 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 25 Sep 2023 12:37:29 -0400 Subject: [PATCH 07/26] fix vpc and subnet validation errors --- .../LinodeConfigs/LinodeConfigDialog.tsx | 6 +- .../LinodeSettings/InterfaceSelect.tsx | 14 +- packages/validation/src/linodes.schema.ts | 152 ++++++++++-------- 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 4d3709ef701..47d3e91334d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -337,7 +337,7 @@ export const LinodeConfigDialog = (props: Props) => { }; const handleError = (error: APIError[]) => { - console.log(error) + console.log(error); const mapErrorToStatus = (generalError: string) => formik.setStatus({ generalError }); @@ -870,6 +870,9 @@ export const LinodeConfigDialog = (props: Props) => { ipamError={ formik.errors[`interfaces[${idx}].ipam_address`] } + subnetError={ + formik.errors[`interfaces[${idx}].subnet_id`] + } ipamAddress={thisInterface.ipam_address} key={`eth${idx}-interface`} label={thisInterface.label} @@ -880,6 +883,7 @@ export const LinodeConfigDialog = (props: Props) => { slotNumber={idx} subnetId={thisInterface.subnet_id} subnetLabel={thisInterface.subnetLabel} + vpcError={formik.errors[`interfaces[${idx}].vpc_id`]} vpcId={thisInterface.vpc_id} vpcIpv4={thisInterface.ipv4?.vpc} vpcIpv4Error={formik.errors['ipv4.vpc']} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 64096ac3bef..fb8064f160b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -21,9 +21,9 @@ import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics'; export interface Props { fromAddonsPanel?: boolean; handleChange: (updatedInterface: ExtendedInterface) => void; - ipamAddress: null | string; + ipamAddress?: null | string; ipamError?: string; - label: null | string; + label?: null | string; labelError?: string; purpose: ExtendedPurpose; readOnly: boolean; @@ -32,8 +32,10 @@ export interface Props { } interface VpcState { + subnetError?: string; subnetId?: null | number; - subnetLabel: null | string; + subnetLabel?: null | string; + vpcError?: string; vpcId?: null | number; vpcIpv4?: null | string; vpcIpv4Error?: string; @@ -74,8 +76,10 @@ export const InterfaceSelect = (props: CombinedProps) => { readOnly, region, slotNumber, + subnetError, subnetId, subnetLabel, + vpcError, vpcId, vpcIpv4, vpcIpv4Error, @@ -152,7 +156,7 @@ export const InterfaceSelect = (props: CombinedProps) => { ipam_address: null, label: null, purpose, - subnet_id: null, + subnet_id: undefined, vpc_id: selected?.id, vpcLabel: selected?.value ?? '', }); @@ -328,6 +332,7 @@ export const InterfaceSelect = (props: CombinedProps) => { } creatable createOptionPosition="first" + errorText={vpcError} inputId={`vpc-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -349,6 +354,7 @@ export const InterfaceSelect = (props: CombinedProps) => { } creatable createOptionPosition="first" + errorText={subnetError} inputId={`subnet-label-${slotNumber}`} isClearable isDisabled={readOnly} diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index bb8b7dcf318..9a1a76148b9 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -128,78 +128,88 @@ const ipv6ConfigInterface = object().when('purpose', { }), }); -export const linodeInterfaceSchema = array() - .of( - object().shape({ - purpose: mixed().oneOf( - ['public', 'vlan', 'vpc'], - 'Purpose must be public, vlan, or vpc.' +export const linodeInterfaceSchema = object().shape({ + purpose: mixed().oneOf( + ['public', 'vlan', 'vpc'], + 'Purpose must be public, vlan, or vpc.' + ), + label: string().when('purpose', { + is: 'vlan', + then: string() + .required('VLAN label is required.') + .min(1, 'VLAN label must be between 1 and 64 characters.') + .max(64, 'VLAN label must be between 1 and 64 characters.') + .matches( + /[a-zA-Z0-9-]+/, + 'Must include only ASCII letters, numbers, and dashes' ), - label: string().when('purpose', { - is: 'vlan', - then: string() - .required('VLAN label is required.') - .min(1, 'VLAN label must be between 1 and 64 characters.') - .max(64, 'VLAN label must be between 1 and 64 characters.') - .matches( - /[a-zA-Z0-9-]+/, - 'Must include only ASCII letters, numbers, and dashes' - ), - otherwise: string().when('label', { - is: null, - then: string().nullable(), - otherwise: string().test({ - name: testnameDisallowedBasedOnPurpose('VLAN'), - message: testmessageDisallowedBasedOnPurpose('vlan', 'label'), - test: (value) => typeof value === 'undefined' || value === '', - }), - }), + otherwise: string().when('label', { + is: null, + then: string().nullable(), + otherwise: string().test({ + name: testnameDisallowedBasedOnPurpose('VLAN'), + message: testmessageDisallowedBasedOnPurpose('vlan', 'label'), + test: (value) => typeof value === 'undefined' || value === '', }), - ipam_address: string().when('purpose', { - is: 'vlan', - then: string().notRequired().test({ - name: 'validateIPAM', - message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', - test: validateIP, - }), - otherwise: string().when('ipam_address', { - is: null, - then: string().nullable(), - otherwise: string().test({ - name: testnameDisallowedBasedOnPurpose('VLAN'), - message: testmessageDisallowedBasedOnPurpose( - 'vlan', - 'ipam_address' - ), - test: (value) => typeof value === 'undefined' || value === '', - }), - }), + }), + }), + ipam_address: string().when('purpose', { + is: 'vlan', + then: string().notRequired().test({ + name: 'validateIPAM', + message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', + test: validateIP, + }), + otherwise: string().when('ipam_address', { + is: null, + then: string().nullable(), + otherwise: string().test({ + name: testnameDisallowedBasedOnPurpose('VLAN'), + message: testmessageDisallowedBasedOnPurpose('vlan', 'ipam_address'), + test: (value) => typeof value === 'undefined' || value === '', }), - primary: boolean().notRequired(), - subnet_id: number().when('purpose', { - is: 'vpc', - then: number().required(), - otherwise: number().test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'subnet_id'), - test: (value) => typeof value === 'undefined', - }), + }), + }), + primary: boolean().notRequired(), + subnet_id: number().when('purpose', { + is: 'vpc', + then: number().required('Subnet is required.'), + otherwise: number().test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'subnet_id'), + test: (value) => typeof value === 'undefined', + }), + }), + vpc_id: number().when('purpose', { + is: 'vpc', + then: number().required('VPC is required.'), + otherwise: number().test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'vpc_id'), + test: (value) => typeof value === 'undefined', + }), + }), + ipv4: ipv4ConfigInterface, + ipv6: ipv6ConfigInterface, + ip_ranges: array() + .of(string()) + .when('purpose', { + is: 'vpc', + then: array() + .of(string().test(validateIP)) + .max(1) + .notRequired() + .nullable(), + otherwise: array().test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'ip_ranges'), + test: (value) => typeof value === 'undefined', }), - ipv4: ipv4ConfigInterface, - ipv6: ipv6ConfigInterface, - ip_ranges: array() - .of(string()) - .when('purpose', { - is: 'vpc', - then: array().of(string().test(validateIP)).max(1).notRequired(), - otherwise: array().test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'ip_ranges'), - test: (value) => typeof value === 'undefined', - }), - }), - }) - ) + }), +}); + +export const linodeInterfacesSchema = array() + .of(linodeInterfaceSchema) .test( 'unique-public-interface', 'Only one public interface per config is allowed.', @@ -285,7 +295,7 @@ export const CreateLinodeSchema = object({ // .concat(rootPasswordValidation), otherwise: string().notRequired(), }), - interfaces: linodeInterfaceSchema, + interfaces: linodeInterfacesSchema, metadata: MetadataSchema, firewall_id: number().notRequired(), }); @@ -425,7 +435,7 @@ export const CreateLinodeConfigSchema = object({ virt_mode: mixed().oneOf(['paravirt', 'fullvirt']), helpers, root_device: string(), - interfaces: linodeInterfaceSchema, + interfaces: linodeInterfacesSchema, }); export const UpdateLinodeConfigSchema = object({ @@ -440,7 +450,7 @@ export const UpdateLinodeConfigSchema = object({ virt_mode: mixed().oneOf(['paravirt', 'fullvirt']), helpers, root_device: string(), - interfaces: linodeInterfaceSchema, + interfaces: linodeInterfacesSchema, }); export const CreateLinodeDiskSchema = object({ From d87c0d3b2d26068843123f44444a8fa92abe12cb Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 25 Sep 2023 15:18:47 -0400 Subject: [PATCH 08/26] fix vpc ipv4 input being overwritten --- .../LinodeSettings/InterfaceSelect.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index fb8064f160b..df808d0c439 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -154,6 +154,9 @@ export const InterfaceSelect = (props: CombinedProps) => { const handleVPCLabelChange = (selected: Item) => handleChange({ ipam_address: null, + ipv4: { + vpc: autoAssignVpcIpv4 ? undefined : vpcIpv4, + }, label: null, purpose, subnet_id: undefined, @@ -164,6 +167,9 @@ export const InterfaceSelect = (props: CombinedProps) => { const handleSubnetChange = (selected: Item) => handleChange({ ipam_address: null, + ipv4: { + vpc: autoAssignVpcIpv4 ? undefined : vpcIpv4, + }, label: null, purpose, subnet_id: selected?.subnet_id, @@ -202,6 +208,10 @@ export const InterfaceSelect = (props: CombinedProps) => { vpcLabel, }; + /** + * If a user checks the "Auto-assign a VPC IPv4 address" box, then we send the user inputted address, otherwise we send nothing/undefined. + * If a user checks the "Assign a public IPv4" address box, then we send nat_1_1: 'any' to the API for auto assignment. + */ if (!autoAssignVpcIpv4 && autoAssignLinodeIpv4) { handleChange({ ...changeObj, @@ -220,6 +230,10 @@ export const InterfaceSelect = (props: CombinedProps) => { nat_1_1: 'any', }, }); + } else if (autoAssignVpcIpv4 && !autoAssignLinodeIpv4) { + handleChange({ + ...changeObj, + }); } else if (!autoAssignLinodeIpv4 && !autoAssignVpcIpv4) { handleChange({ ...changeObj, From 0c2fed9be11bcfcae1d3c6da81c077dd4b51d7f3 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 25 Sep 2023 15:48:42 -0400 Subject: [PATCH 09/26] clean up payload --- .../LinodeConfigs/LinodeConfigDialog.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 47d3e91334d..7b11afedf1f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -174,14 +174,21 @@ const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { if (!interfaces || interfaces.length === 0) { return []; } + const nonEmptyInterfaces = interfaces.filter( + (thisInterface) => thisInterface.purpose !== 'none' + ) as Interface[]; + + const removeUnnecessaryVpcState = nonEmptyInterfaces.map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ subnetLabel, vpcLabel, ...restInterface }: ExtendedInterface) => + restInterface + ); return equals(interfaces, defaultInterfaceList) ? // In this case, where eth0 is set to public interface // and no other interfaces are specified, the API prefers // to receive an empty array. [] - : (interfaces.filter( - (thisInterface) => thisInterface.purpose !== 'none' - ) as Interface[]); + : removeUnnecessaryVpcState; }; const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh']; @@ -337,7 +344,6 @@ export const LinodeConfigDialog = (props: Props) => { }; const handleError = (error: APIError[]) => { - console.log(error); const mapErrorToStatus = (generalError: string) => formik.setStatus({ generalError }); @@ -558,8 +564,7 @@ export const LinodeConfigDialog = (props: Props) => { }, [setFieldValue] ); - console.log(values.interfaces); - console.log(formik.errors); + return ( Date: Mon, 25 Sep 2023 15:53:24 -0400 Subject: [PATCH 10/26] fix vlan ipam_address not nullable --- packages/validation/src/linodes.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index 9a1a76148b9..4fd9f51ba8b 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -155,7 +155,7 @@ export const linodeInterfaceSchema = object().shape({ }), ipam_address: string().when('purpose', { is: 'vlan', - then: string().notRequired().test({ + then: string().notRequired().nullable().test({ name: 'validateIPAM', message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', test: validateIP, From 55bfffba99fed2abc103feb8e38361402a7351eb Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 25 Sep 2023 17:09:24 -0400 Subject: [PATCH 11/26] clean up error code --- .../LinodeConfigs/LinodeConfigDialog.tsx | 21 +++++----- .../LinodeSettings/InterfaceSelect.tsx | 39 ++++++++++++------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 7b11afedf1f..8f1f8b7d39e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -176,7 +176,7 @@ const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { } const nonEmptyInterfaces = interfaces.filter( (thisInterface) => thisInterface.purpose !== 'none' - ) as Interface[]; + ); const removeUnnecessaryVpcState = nonEmptyInterfaces.map( // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -869,29 +869,30 @@ export const LinodeConfigDialog = (props: Props) => { {values.interfaces.map((thisInterface, idx) => { return ( handleInterfaceChange(idx, newInterface) } - ipamError={ - formik.errors[`interfaces[${idx}].ipam_address`] - } - subnetError={ - formik.errors[`interfaces[${idx}].subnet_id`] - } ipamAddress={thisInterface.ipam_address} key={`eth${idx}-interface`} label={thisInterface.label} - labelError={formik.errors[`interfaces[${idx}].label`]} purpose={thisInterface.purpose} readOnly={isReadOnly} region={linode?.region} slotNumber={idx} subnetId={thisInterface.subnet_id} subnetLabel={thisInterface.subnetLabel} - vpcError={formik.errors[`interfaces[${idx}].vpc_id`]} vpcId={thisInterface.vpc_id} vpcIpv4={thisInterface.ipv4?.vpc} - vpcIpv4Error={formik.errors['ipv4.vpc']} vpcLabel={thisInterface.vpcLabel} /> ); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index df808d0c439..3893ba63bd9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -10,6 +10,7 @@ import * as React from 'react'; import { Checkbox } from 'src/components/Checkbox'; import { Divider } from 'src/components/Divider'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; +import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; @@ -22,23 +23,28 @@ export interface Props { fromAddonsPanel?: boolean; handleChange: (updatedInterface: ExtendedInterface) => void; ipamAddress?: null | string; - ipamError?: string; label?: null | string; - labelError?: string; purpose: ExtendedPurpose; readOnly: boolean; region?: string; slotNumber: number; } -interface VpcState { +interface VpcStateErrors { + ipamError?: string; + labelError?: string; + nat_1_1Error?: string; subnetError?: string; + vpcError?: string; + vpcIpv4Error?: string; +} + +interface VpcState { + errors: VpcStateErrors; subnetId?: null | number; subnetLabel?: null | string; - vpcError?: string; vpcId?: null | number; vpcIpv4?: null | string; - vpcIpv4Error?: string; vpcLabel?: null | string; } @@ -66,23 +72,19 @@ export const InterfaceSelect = (props: CombinedProps) => { ); const { + errors, fromAddonsPanel, handleChange, ipamAddress, - ipamError, label, - labelError, purpose, readOnly, region, slotNumber, - subnetError, subnetId, subnetLabel, - vpcError, vpcId, vpcIpv4, - vpcIpv4Error, vpcLabel, } = props; @@ -301,7 +303,7 @@ export const InterfaceSelect = (props: CombinedProps) => { } creatable createOptionPosition="first" - errorText={labelError} + errorText={errors.labelError} inputId={`vlan-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -321,7 +323,7 @@ export const InterfaceSelect = (props: CombinedProps) => { 'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.' } disabled={readOnly} - errorText={ipamError} + errorText={errors.ipamError} inputId={`ipam-input-${slotNumber}`} label="IPAM Address" onChange={handleAddressChange} @@ -346,7 +348,7 @@ export const InterfaceSelect = (props: CombinedProps) => { } creatable createOptionPosition="first" - errorText={vpcError} + errorText={errors.vpcError} inputId={`vpc-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -368,7 +370,7 @@ export const InterfaceSelect = (props: CombinedProps) => { } creatable createOptionPosition="first" - errorText={subnetError} + errorText={errors.subnetError} inputId={`subnet-label-${slotNumber}`} isClearable isDisabled={readOnly} @@ -393,7 +395,7 @@ export const InterfaceSelect = (props: CombinedProps) => { {!autoAssignVpcIpv4 && ( { text="Assign a public IPv4 address for this Linode" toolTipText="Assign a public IP address for this VPC via 1:1 static NAT." /> + {errors.nat_1_1Error && ( + + )} From c1c41068600d09a62e1402bbc4938e4c7d315c03 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Tue, 26 Sep 2023 11:53:00 -0400 Subject: [PATCH 12/26] fix type errors --- .../features/Linodes/LinodesCreate/AttachVLAN.tsx | 8 +++++--- .../Linodes/LinodesCreate/VLANAccordion.tsx | 6 ++++-- .../LinodeConfigs/LinodeConfigDialog.tsx | 2 +- .../LinodeSettings/InterfaceSelect.tsx | 13 +++++++++++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx b/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx index 2012b6345a9..123b5cb916d 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx @@ -71,7 +71,7 @@ export const AttachVLAN = React.memo((props: Props) => { )}.`; return ( - + { . handleVLANChange(newInterface) } fromAddonsPanel ipamAddress={ipamAddress} - ipamError={ipamError} label={vlanLabel} - labelError={labelError} purpose="vlan" readOnly={readOnly || !regionSupportsVLANs || false} region={region} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx index f91d74283b1..63b2dca0bdc 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx @@ -107,14 +107,16 @@ export const VLANAccordion = React.memo((props: Props) => { . handleVLANChange(newInterface) } fromAddonsPanel ipamAddress={ipamAddress} - ipamError={ipamError} label={vlanLabel} - labelError={labelError} purpose="vlan" readOnly={readOnly || !regionSupportsVLANs || false} region={region} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 8f1f8b7d39e..24778979ada 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -188,7 +188,7 @@ const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { // and no other interfaces are specified, the API prefers // to receive an empty array. [] - : removeUnnecessaryVpcState; + : (removeUnnecessaryVpcState as Interface[]); }; const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh']; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 3893ba63bd9..b60b25e1542 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -48,6 +48,14 @@ interface VpcState { vpcLabel?: null | string; } +interface ExtendedItem { + data?: any; + id: number; + label: L; + subnet_id: number; + value: T; +} + // To allow for empty slots, which the API doesn't account for export type ExtendedPurpose = 'none' | InterfacePurpose; export interface ExtendedInterface @@ -153,7 +161,7 @@ export const InterfaceSelect = (props: CombinedProps) => { purpose, }); - const handleVPCLabelChange = (selected: Item) => + const handleVPCLabelChange = (selected: ExtendedItem) => handleChange({ ipam_address: null, ipv4: { @@ -166,7 +174,7 @@ export const InterfaceSelect = (props: CombinedProps) => { vpcLabel: selected?.value ?? '', }); - const handleSubnetChange = (selected: Item) => + const handleSubnetChange = (selected: ExtendedItem) => handleChange({ ipam_address: null, ipv4: { @@ -184,6 +192,7 @@ export const InterfaceSelect = (props: CombinedProps) => { const changeObj = { ipam_address: null, ipv4: { + nat_1_1: '', vpc: e.target.value, }, label: null, From 94cc92e19e68b5092235b43d8c5744602666e2b7 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 27 Sep 2023 16:00:09 -0400 Subject: [PATCH 13/26] use VPCPanel --- packages/api-v4/src/linodes/types.ts | 6 +- .../Linodes/LinodesCreate/VPCPanel.tsx | 259 ++++++++++++++++++ .../Linodes/LinodesCreate/constants.ts | 3 + .../LinodeConfigs/LinodeConfigDialog.tsx | 15 +- .../LinodeSettings/InterfaceSelect.tsx | 210 +++++--------- packages/manager/src/queries/vpcs.ts | 9 +- 6 files changed, 336 insertions(+), 166 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index a9d1e8a7bf0..a560f36325a 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -151,7 +151,7 @@ export type LinodeStatus = export type InterfacePurpose = 'public' | 'vlan' | 'vpc'; export interface ConfigInterfaceIPv4 { - vpc?: string | null; + vpc?: string; nat_1_1?: string; } @@ -165,8 +165,8 @@ export interface Interface { purpose: InterfacePurpose; ipam_address: string | null; primary?: boolean; - subnet_id?: number | null; - vpc_id?: number | null; + subnet_id?: number; + vpc_id?: number; ipv4?: ConfigInterfaceIPv4; ipv6?: ConfigInterfaceIPv6; ip_ranges?: string[]; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx new file mode 100644 index 00000000000..83bc66866b3 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -0,0 +1,259 @@ +import Stack from '@mui/material/Stack'; +import * as React from 'react'; + +import { Box } from 'src/components/Box'; +import { Checkbox } from 'src/components/Checkbox'; +import Select, { Item } from 'src/components/EnhancedSelect'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { Link } from 'src/components/Link'; +import { Paper } from 'src/components/Paper'; +import { TextField } from 'src/components/TextField'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { Typography } from 'src/components/Typography'; +// import { APP_ROOT } from 'src/constants'; +import { useAccountManagement } from 'src/hooks/useAccountManagement'; +import { useFlags } from 'src/hooks/useFlags'; +import { useRegionsQuery } from 'src/queries/regions'; +import { useVPCsQuery } from 'src/queries/vpcs'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; +import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; + +// import { StyledCreateLink } from './LinodeCreate.styles'; +import { REGION_CAVEAT_HELPER_TEXT } from './constants'; + +interface VPCPanelProps { + assignPublicIPv4Address: boolean; + autoassignIPv4WithinVPC: boolean; + handleSelectVPC: (vpcId: number) => void; + handleSubnetChange: (subnetId: number) => void; + handleVPCIPv4Change: (IPv4: string) => void; + region: string | undefined; + selectedSubnetId: number | undefined; + selectedVPCId: number | undefined; + subnetError?: string; + toggleAssignPublicIPv4Address: () => void; + toggleAutoassignIPv4WithinVPCEnabled: () => void; + vpcIPv4AddressOfLinode: string | undefined; + vpcIPv4Error?: string; + vpcIdError?: string; +} + +export const VPCPanel = (props: VPCPanelProps) => { + const { + assignPublicIPv4Address, + autoassignIPv4WithinVPC, + handleSelectVPC, + handleSubnetChange, + handleVPCIPv4Change, + region, + selectedSubnetId, + selectedVPCId, + subnetError, + toggleAssignPublicIPv4Address, + toggleAutoassignIPv4WithinVPCEnabled, + vpcIPv4AddressOfLinode, + vpcIPv4Error, + vpcIdError, + } = props; + + const flags = useFlags(); + const { account } = useAccountManagement(); + const { data: vpcData, error, isLoading } = useVPCsQuery({}, {}, true, true); + + const regions = useRegionsQuery().data ?? []; + const selectedRegion = region || ''; + + const regionSupportsVPCs = doesRegionSupportFeature( + selectedRegion, + regions, + 'VPCs' + ); + + const displayVPCPanel = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + + if (!displayVPCPanel) { + return null; + } + + const vpcs = vpcData?.data ?? []; + + const vpcDropdownOptions: Item[] = vpcs.reduce((accumulator, vpc) => { + return vpc.region === region + ? [...accumulator, { label: vpc.label, value: vpc.id }] + : accumulator; + }, []); + + vpcDropdownOptions.unshift({ + label: 'None', + value: -1, + }); + + const subnetDropdownOptions: Item[] = + vpcs + .find((vpc) => vpc.id === selectedVPCId) + ?.subnets.map((subnet) => ({ + label: `${subnet.label} (${subnet.ipv4 ?? 'No IPv4 range provided'})`, // @TODO VPC: Support for IPv6 down the line + value: subnet.id, + })) ?? []; + + const vpcError = error + ? getAPIErrorOrDefault(error, 'Unable to load VPCs')[0].reason + : undefined; + + const mainCopyVPC = + vpcDropdownOptions.length <= 1 + ? 'Allow Linode to communicate in an isolated environment.' + : 'Assign this Linode to an existing VPC.'; + + return ( + ({ marginTop: theme.spacing(3) })} + > + ({ marginBottom: theme.spacing(2) })} + variant="h2" + > + VPC + + + + {/* @TODO VPC: Update link */} + {mainCopyVPC} Learn more. + + ) => + handleSubnetChange(selectedSubnet.value) + } + value={ + subnetDropdownOptions.find( + (option) => option.value === selectedSubnetId + ) || null + } + errorText={subnetError} + isClearable={false} + label="Subnet" + options={subnetDropdownOptions} + placeholder="Select Subnet" + /> + ({ + marginLeft: '2px', + paddingTop: theme.spacing(), + })} + alignItems="center" + display="flex" + flexDirection="row" + > + + } + label={ + + + Auto-assign a VPC IPv4 address for this Linode in the VPC + + + + } + data-testid="vpc-ipv4-checkbox" + /> + + {!autoassignIPv4WithinVPC && ( + handleVPCIPv4Change(e.target.value)} + required={!autoassignIPv4WithinVPC} + value={vpcIPv4AddressOfLinode} + /> + )} + ({ + marginLeft: '2px', + marginTop: !autoassignIPv4WithinVPC ? theme.spacing() : 0, + })} + alignItems="center" + display="flex" + flexDirection="row" + > + + } + label={ + + + Assign a public IPv4 address for this Linode + + + + } + /> + + + )} + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/constants.ts b/packages/manager/src/features/Linodes/LinodesCreate/constants.ts index 9a88ed512bc..2e4b38abf06 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/constants.ts +++ b/packages/manager/src/features/Linodes/LinodesCreate/constants.ts @@ -1,2 +1,5 @@ export const CROSS_DATA_CENTER_CLONE_WARNING = 'Cloning a powered off instance across data centers may cause long periods of down time.'; + +export const REGION_CAVEAT_HELPER_TEXT = + 'A Linode may only be assigned to a VPC in the same region.'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 24778979ada..3b00ab62c02 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -174,21 +174,14 @@ const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { if (!interfaces || interfaces.length === 0) { return []; } - const nonEmptyInterfaces = interfaces.filter( - (thisInterface) => thisInterface.purpose !== 'none' - ); - - const removeUnnecessaryVpcState = nonEmptyInterfaces.map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ subnetLabel, vpcLabel, ...restInterface }: ExtendedInterface) => - restInterface - ); return equals(interfaces, defaultInterfaceList) ? // In this case, where eth0 is set to public interface // and no other interfaces are specified, the API prefers // to receive an empty array. [] - : (removeUnnecessaryVpcState as Interface[]); + : (interfaces.filter( + (thisInterface) => thisInterface.purpose !== 'none' + ) as Interface[]); }; const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh']; @@ -890,10 +883,8 @@ export const LinodeConfigDialog = (props: Props) => { region={linode?.region} slotNumber={idx} subnetId={thisInterface.subnet_id} - subnetLabel={thisInterface.subnetLabel} vpcId={thisInterface.vpc_id} vpcIpv4={thisInterface.ipv4?.vpc} - vpcLabel={thisInterface.vpcLabel} /> ); })} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index b60b25e1542..a6113f6899a 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -7,15 +7,15 @@ import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; -import { Checkbox } from 'src/components/Checkbox'; import { Divider } from 'src/components/Divider'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; -import { Notice } from 'src/components/Notice/Notice'; +// import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; +import { VPCPanel } from 'src/features/Linodes/LinodesCreate/VPCPanel'; import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; import { useVlansQuery } from 'src/queries/vlans'; -import { useSubnetsQuery, useVPCsQuery } from 'src/queries/vpcs'; +// import { useSubnetsQuery, useVPCsQuery } from 'src/queries/vpcs'; import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics'; @@ -41,19 +41,9 @@ interface VpcStateErrors { interface VpcState { errors: VpcStateErrors; - subnetId?: null | number; - subnetLabel?: null | string; - vpcId?: null | number; - vpcIpv4?: null | string; - vpcLabel?: null | string; -} - -interface ExtendedItem { - data?: any; - id: number; - label: L; - subnet_id: number; - value: T; + subnetId?: number; + vpcId?: number; + vpcIpv4?: string; } // To allow for empty slots, which the API doesn't account for @@ -61,8 +51,6 @@ export type ExtendedPurpose = 'none' | InterfacePurpose; export interface ExtendedInterface extends Partial> { purpose: ExtendedPurpose; - subnetLabel?: null | string; - vpcLabel?: null | string; } type CombinedProps = Props & VpcState; @@ -90,10 +78,8 @@ export const InterfaceSelect = (props: CombinedProps) => { region, slotNumber, subnetId, - subnetLabel, vpcId, vpcIpv4, - vpcLabel, } = props; const [newVlan, setNewVlan] = React.useState(''); @@ -115,29 +101,19 @@ export const InterfaceSelect = (props: CombinedProps) => { vlanOptions.push({ label: newVlan, value: newVlan }); } - const { data: vpcs, isLoading: vpcsLoading } = useVPCsQuery( - {}, - { - ['region']: region, - } - ); - const vpcOptions = vpcs?.data.map((vpc) => ({ - id: vpc.id, - label: vpc.label, - value: vpc.label, - })); + // const { data: vpcs, isLoading: vpcsLoading } = useVPCsQuery( + // {}, + // { + // ['region']: region, + // } + // ); - const { data: subnets, isLoading: subnetsLoading } = useSubnetsQuery( - vpcId ?? -1, - {}, - {}, - Boolean(vpcId) - ); - const subnetOptions = subnets?.data.map((subnet) => ({ - label: subnet.label, - subnet_id: subnet.id, - value: subnet.label, - })); + // const { data: subnets, isLoading: subnetsLoading } = useSubnetsQuery( + // vpcId ?? -1, + // {}, + // {}, + // Boolean(vpcId) + // ); const [autoAssignVpcIpv4, setAutoAssignVpcIpv4] = React.useState(true); const [autoAssignLinodeIpv4, setAutoAssignLinodeIpv4] = React.useState(false); @@ -161,7 +137,7 @@ export const InterfaceSelect = (props: CombinedProps) => { purpose, }); - const handleVPCLabelChange = (selected: ExtendedItem) => + const handleVPCLabelChange = (selectedVpcId: number) => handleChange({ ipam_address: null, ipv4: { @@ -170,11 +146,10 @@ export const InterfaceSelect = (props: CombinedProps) => { label: null, purpose, subnet_id: undefined, - vpc_id: selected?.id, - vpcLabel: selected?.value ?? '', + vpc_id: selectedVpcId, }); - const handleSubnetChange = (selected: ExtendedItem) => + const handleSubnetChange = (selectedSubnetId: number) => handleChange({ ipam_address: null, ipv4: { @@ -182,30 +157,34 @@ export const InterfaceSelect = (props: CombinedProps) => { }, label: null, purpose, - subnet_id: selected?.subnet_id, - subnetLabel: selected?.value, + subnet_id: selectedSubnetId, vpc_id: vpcId, - vpcLabel, }); - const handleVpcIpv4Input = (e: React.ChangeEvent) => { + const handleVpcIpv4Input = (vpcIpv4Input: string) => { const changeObj = { ipam_address: null, - ipv4: { - nat_1_1: '', - vpc: e.target.value, - }, label: null, purpose, subnet_id: subnetId, - subnetLabel, vpc_id: vpcId, - vpcLabel, }; if (autoAssignLinodeIpv4) { - changeObj.ipv4.nat_1_1 = 'any'; + handleChange({ + ...changeObj, + ipv4: { + nat_1_1: 'any', + vpc: vpcIpv4Input, + }, + }); + } else { + handleChange({ + ...changeObj, + ipv4: { + vpc: vpcIpv4Input, + }, + }); } - handleChange(changeObj); }; React.useEffect(() => { @@ -214,9 +193,7 @@ export const InterfaceSelect = (props: CombinedProps) => { label: null, purpose, subnet_id: subnetId, - subnetLabel, vpc_id: vpcId, - vpcLabel, }; /** @@ -344,95 +321,30 @@ export const InterfaceSelect = (props: CombinedProps) => { ) : null} - {purpose === 'vpc' ? ( - - - - - subnetsLoading ? 'Loading...' : 'You have no Subnets.' - } - value={ - subnetOptions?.find( - (subnet) => subnet.value === subnetLabel - ) ?? null - } - creatable - createOptionPosition="first" - errorText={errors.subnetError} - inputId={`subnet-label-${slotNumber}`} - isClearable - isDisabled={readOnly} - label="Subnets" - onChange={handleSubnetChange} - options={subnetOptions} - placeholder="Select a Subnet" - /> - - - - setAutoAssignVpcIpv4( - (autoAssignVpcIpv4) => !autoAssignVpcIpv4 - ) - } - checked={autoAssignVpcIpv4} - text="Auto-assign a VPC IPv4 address for this Linode" - toolTipText="A range of non-internet facing IP addresses used in an internal network." - /> - - {!autoAssignVpcIpv4 && ( - - - - )} - - - setAutoAssignLinodeIpv4( - (autoAssignLinodeIpv4) => !autoAssignLinodeIpv4 - ) - } - checked={autoAssignLinodeIpv4} - text="Assign a public IPv4 address for this Linode" - toolTipText="Assign a public IP address for this VPC via 1:1 static NAT." - /> - {errors.nat_1_1Error && ( - - )} - - - - ) : null} + {purpose === 'vpc' && ( + + setAutoAssignLinodeIpv4( + (autoAssignLinodeIpv4) => !autoAssignLinodeIpv4 + ) + } + toggleAutoassignIPv4WithinVPCEnabled={() => + setAutoAssignVpcIpv4((autoAssignVpcIpv4) => !autoAssignVpcIpv4) + } + assignPublicIPv4Address={autoAssignLinodeIpv4} + autoassignIPv4WithinVPC={autoAssignVpcIpv4} + handleSelectVPC={handleVPCLabelChange} + handleSubnetChange={handleSubnetChange} + handleVPCIPv4Change={handleVpcIpv4Input} + region={region} + selectedSubnetId={subnetId} + selectedVPCId={vpcId} + subnetError={errors.subnetError} + vpcIPv4AddressOfLinode={vpcIpv4} + vpcIPv4Error={errors.vpcIpv4Error} + vpcIdError={errors.vpcError} + /> + )} {!fromAddonsPanel && ( { return useQuery, APIError[]>( [vpcQueryKey, 'paginated', params, filter], () => getVPCs(params, filter), - { enabled, keepPreviousData: true } + { + enabled, + keepPreviousData: true, + refetchOnWindowFocus: alwaysRefetch ? 'always' : false, + } ); }; From fc008f2f2ba8dc76aed26a5a21d2cabb6b90ebf2 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 27 Sep 2023 17:54:01 -0400 Subject: [PATCH 14/26] clean up and fix styling --- .../Linodes/LinodesCreate/VPCPanel.tsx | 71 +++++++++++++------ .../LinodeSettings/InterfaceSelect.tsx | 66 +++++++---------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index 83bc66866b3..7307f777871 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -25,9 +25,11 @@ import { REGION_CAVEAT_HELPER_TEXT } from './constants'; interface VPCPanelProps { assignPublicIPv4Address: boolean; autoassignIPv4WithinVPC: boolean; + from: 'linodeConfig' | 'linodeCreate'; handleSelectVPC: (vpcId: number) => void; handleSubnetChange: (subnetId: number) => void; handleVPCIPv4Change: (IPv4: string) => void; + nat_1_1Error?: string; region: string | undefined; selectedSubnetId: number | undefined; selectedVPCId: number | undefined; @@ -43,9 +45,11 @@ export const VPCPanel = (props: VPCPanelProps) => { const { assignPublicIPv4Address, autoassignIPv4WithinVPC, + from, handleSelectVPC, handleSubnetChange, handleVPCIPv4Change, + nat_1_1Error, region, selectedSubnetId, selectedVPCId, @@ -105,26 +109,37 @@ export const VPCPanel = (props: VPCPanelProps) => { ? getAPIErrorOrDefault(error, 'Unable to load VPCs')[0].reason : undefined; - const mainCopyVPC = - vpcDropdownOptions.length <= 1 - ? 'Allow Linode to communicate in an isolated environment.' - : 'Assign this Linode to an existing VPC.'; + const getMainCopyVPC = () => { + if (from === 'linodeConfig') { + return ''; + } + + const copy = + vpcDropdownOptions.length <= 1 + ? 'Allow Linode to communicate in an isolated environment.' + : 'Assign this Linode to an existing VPC.'; + + return ( + <> + {copy} Learn more + + ); + }; return ( - ({ marginTop: theme.spacing(3) })} - > - ({ marginBottom: theme.spacing(2) })} - variant="h2" - > - VPC - + + {from === 'linodeCreate' && ( + ({ marginBottom: theme.spacing(2) })} + variant="h2" + > + VPC + + )} {/* @TODO VPC: Update link */} - {mainCopyVPC} Learn more. + {getMainCopyVPC()} { ) : null} {purpose === 'vpc' && ( - - setAutoAssignLinodeIpv4( - (autoAssignLinodeIpv4) => !autoAssignLinodeIpv4 - ) - } - toggleAutoassignIPv4WithinVPCEnabled={() => - setAutoAssignVpcIpv4((autoAssignVpcIpv4) => !autoAssignVpcIpv4) - } - assignPublicIPv4Address={autoAssignLinodeIpv4} - autoassignIPv4WithinVPC={autoAssignVpcIpv4} - handleSelectVPC={handleVPCLabelChange} - handleSubnetChange={handleSubnetChange} - handleVPCIPv4Change={handleVpcIpv4Input} - region={region} - selectedSubnetId={subnetId} - selectedVPCId={vpcId} - subnetError={errors.subnetError} - vpcIPv4AddressOfLinode={vpcIpv4} - vpcIPv4Error={errors.vpcIpv4Error} - vpcIdError={errors.vpcError} - /> + + + setAutoAssignLinodeIpv4( + (autoAssignLinodeIpv4) => !autoAssignLinodeIpv4 + ) + } + toggleAutoassignIPv4WithinVPCEnabled={() => + setAutoAssignVpcIpv4((autoAssignVpcIpv4) => !autoAssignVpcIpv4) + } + assignPublicIPv4Address={autoAssignLinodeIpv4} + autoassignIPv4WithinVPC={autoAssignVpcIpv4} + from="linodeConfig" + handleSelectVPC={handleVPCLabelChange} + handleSubnetChange={handleSubnetChange} + handleVPCIPv4Change={handleVpcIpv4Input} + nat_1_1Error={errors.nat_1_1Error} + region={region} + selectedSubnetId={subnetId} + selectedVPCId={vpcId} + subnetError={errors.subnetError} + vpcIPv4AddressOfLinode={vpcIpv4} + vpcIPv4Error={errors.vpcIpv4Error} + vpcIdError={errors.vpcError} + /> + )} {!fromAddonsPanel && ( From 8e508c8da9be0164b276493b732e115ab5d76e9f Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 28 Sep 2023 17:14:42 -0400 Subject: [PATCH 15/26] Fix styling of VPC panel in Linode Create flow --- .../Linodes/LinodesCreate/VPCPanel.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index 88bd39dee2c..e508092faed 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -127,9 +127,12 @@ export const VPCPanel = (props: VPCPanelProps) => { ? getAPIErrorOrDefault(error, 'Unable to load VPCs')[0].reason : undefined; + const fromLinodeCreate = from === 'linodeCreate'; + const fromLinodeConfig = from === 'linodeConfig'; + const getMainCopyVPC = () => { - if (from === 'linodeConfig') { - return ''; + if (fromLinodeConfig) { + return null; } const copy = @@ -139,14 +142,24 @@ export const VPCPanel = (props: VPCPanelProps) => { return ( <> - {copy} Learn more + {copy} Learn more. ); }; return ( - - {from === 'linodeCreate' && ( + ({ + ...(fromLinodeCreate && { + marginTop: theme.spacing(3), + }), + ...(fromLinodeConfig && { + padding: 0, + }), + })} + data-testid="vpc-panel" + > + {fromLinodeCreate && ( ({ marginBottom: theme.spacing(2) })} variant="h2" From 02433567d3ade5f1fd52d19e2f9effcc6e8b4602 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 29 Sep 2023 16:01:31 -0400 Subject: [PATCH 16/26] Formatting fixed for VPCPanel in Add/Edit Config dialog; Primary Interface dropdown in Add/Edit Config dialog; casing adjustments for some variable names --- .../Linodes/LinodesCreate/VPCPanel.tsx | 1 - .../LinodeConfigs/LinodeConfigDialog.tsx | 129 ++++++++++++++++-- .../LinodeSettings/InterfaceSelect.tsx | 62 ++++----- 3 files changed, 145 insertions(+), 47 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index e508092faed..2d87869af6c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -277,7 +277,6 @@ export const VPCPanel = (props: VPCPanelProps) => { })} alignItems="center" display="flex" - flexDirection={from === 'linodeCreate' ? 'row' : 'column'} > { return defaultInterfaceList; } const interfacesPayload = interfaces.map( - ({ ipam_address, label, purpose }) => ({ ipam_address, label, purpose }) + ({ + id, + ipam_address, + ipv4, + label, + primary, + purpose, + subnet_id, + vpc_id, + }) => ({ + id, + ipam_address, + ipv4, + label, + primary, + purpose, + subnet_id, + vpc_id, + }) ); return padInterfaceList(interfacesPayload); }; -const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { +const interfacesToPayload = ( + interfaces?: ExtendedInterface[], + primaryInterfaceIndex?: number +) => { if (!interfaces || interfaces.length === 0) { return []; } - return equals(interfaces, defaultInterfaceList) - ? // In this case, where eth0 is set to public interface - // and no other interfaces are specified, the API prefers - // to receive an empty array. - [] - : (interfaces.filter( - (thisInterface) => thisInterface.purpose !== 'none' - ) as Interface[]); + + if (equals(interfaces, defaultInterfaceList)) { + // In this case, where eth0 is set to public interface + // and no other interfaces are specified, the API prefers + // to receive an empty array. + return []; + } + + if (primaryInterfaceIndex !== undefined) { + interfaces[primaryInterfaceIndex].primary = true; + } + + return interfaces.filter( + (thisInterface) => thisInterface.purpose !== 'none' + ) as Interface[]; + + // return equals(interfaces, defaultInterfaceList) + // ? + // [] + // : (interfaces.filter( + // (thisInterface) => thisInterface.purpose !== 'none' + // ) as Interface[]); }; const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh']; @@ -216,6 +254,9 @@ export const LinodeConfigDialog = (props: Props) => { ); const theme = useTheme(); + const flags = useFlags(); + const { account } = useAccountManagement(); + const regions = useRegionsQuery().data ?? []; const queryClient = useQueryClient(); @@ -224,6 +265,11 @@ export const LinodeConfigDialog = (props: Props) => { deviceCounterDefault ); + const [ + primaryInterfaceIndex, + setPrimaryInterfaceIndex, + ] = React.useState(); + const [useCustomRoot, setUseCustomRoot] = React.useState(false); const regionHasVLANS = regions.some( @@ -239,6 +285,13 @@ export const LinodeConfigDialog = (props: Props) => { const showVlans = regionHasVLANS; + // @TODO VPC: remove once VPC is fully rolled out + const vpcEnabled = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const { resetForm, setFieldValue, values, ...formik } = useFormik({ initialValues: defaultFieldsValues, onSubmit: (values) => onSubmit(values), @@ -270,7 +323,7 @@ export const LinodeConfigDialog = (props: Props) => { devices: createDevicesFromStrings(devices), helpers, initrd: initrd !== '' ? initrd : null, - interfaces: interfacesToPayload(interfaces), + interfaces: interfacesToPayload(interfaces, primaryInterfaceIndex), kernel, label, /** if the user did not toggle the limit radio button, send a value of 0 */ @@ -363,7 +416,6 @@ export const LinodeConfigDialog = (props: Props) => { scrollErrorIntoView('linode-config-dialog'); }; - console.log(configData); /** Editing */ if (config) { return updateConfig(configData).then(handleSuccess).catch(handleError); @@ -406,6 +458,14 @@ export const LinodeConfigDialog = (props: Props) => { ) ); + const indexOfExistingPrimaryInterface = config.interfaces.findIndex( + (_interface) => _interface.primary === true + ); + + if (vpcEnabled && indexOfExistingPrimaryInterface !== -1) { + setPrimaryInterfaceIndex(indexOfExistingPrimaryInterface); + } + resetForm({ values: { comments: config.comments, @@ -506,6 +566,21 @@ export const LinodeConfigDialog = (props: Props) => { value: '', }); + const getPrimaryInterfaceOptions = (interfaces: ExtendedInterface[]) => { + return interfaces.map((_interface, idx) => { + return { + label: `eth${idx}`, + value: idx, + }; + }); + }; + + const primaryInterfaceOptions = getPrimaryInterfaceOptions(values.interfaces); + + const handlePrimaryInterfaceChange = (selected: Item) => { + setPrimaryInterfaceIndex(selected.value); + }; + /** * Form change handlers * (where formik.handleChange is insufficient) @@ -830,7 +905,9 @@ export const LinodeConfigDialog = (props: Props) => { {showVlans ? ( - Network Interfaces + + {vpcEnabled ? 'Networking' : 'Network Interfaces'} + { variant="error" /> ) : null} + {vpcEnabled && ( + <> + + isLoading + ? 'Loading...' + : 'You have no VLANs in this region. Type to create one.' + } + creatable + createOptionPosition="first" + errorText={errors.labelError} + inputId={`vlan-label-${slotNumber}`} + isClearable + isDisabled={readOnly} + label="VLAN" + onChange={handleLabelChange} + onCreateOption={handleCreateOption} + options={vlanOptions} + placeholder="Create or select a VLAN" + value={vlanOptions.find((thisVlan) => thisVlan.value === label) ?? null} + /> + ); + + const jsxIPAMForVLAN = ( + + sendLinodeCreateDocsEvent('IPAM Address Tooltip Hover') + } + tooltipText={ + 'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.' + } + disabled={readOnly} + errorText={errors.ipamError} + inputId={`ipam-input-${slotNumber}`} + label="IPAM Address" + onChange={handleAddressChange} + optional + placeholder="192.0.2.0/24" + value={ipamAddress} + /> + ); + + const enclosingJSXForVLANFields = ( + jsxSelectVLAN: JSX.Element, + jsxIPAMForVLAN: JSX.Element + ) => { + return fromAddonsPanel ? ( + + + + {jsxSelectVLAN} + + + {jsxIPAMForVLAN} + + + + ) : ( + + + {jsxSelectVLAN} + {jsxIPAMForVLAN} + + + ); + }; + return ( {fromAddonsPanel ? null : ( - + - isLoading - ? 'Loading...' - : 'You have no VLANs in this region. Type to create one.' - } - value={ - vlanOptions.find((thisVlan) => thisVlan.value === label) ?? - null - } - creatable - createOptionPosition="first" - errorText={errors.labelError} - inputId={`vlan-label-${slotNumber}`} - isClearable - isDisabled={readOnly} - label="VLAN" - onChange={handleLabelChange} - onCreateOption={handleCreateOption} - options={vlanOptions} - placeholder="Create or select a VLAN" - /> - - - - sendLinodeCreateDocsEvent('IPAM Address Tooltip Hover') - } - tooltipText={ - 'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.' - } - disabled={readOnly} - errorText={errors.ipamError} - inputId={`ipam-input-${slotNumber}`} - label="IPAM Address" - onChange={handleAddressChange} - optional - placeholder="192.0.2.0/24" - value={ipamAddress} - /> - - - - ) : null} + {purpose === 'vlan' && + enclosingJSXForVLANFields(jsxSelectVLAN, jsxIPAMForVLAN)} {purpose === 'vpc' && ( { ) } toggleAutoassignIPv4WithinVPCEnabled={() => - setAutoAssignVpcIPv4((autoAssignVpcIPv4) => !autoAssignVpcIPv4) + setautoAssignVPCIPv4((autoAssignVPCIPv4) => !autoAssignVPCIPv4) } assignPublicIPv4Address={autoAssignLinodeIPv4} - autoassignIPv4WithinVPC={autoAssignVpcIPv4} + autoassignIPv4WithinVPC={autoAssignVPCIPv4} from="linodeConfig" handleSelectVPC={handleVPCLabelChange} handleSubnetChange={handleSubnetChange} From 8f919ceaa6ac39d838f32b8be15a3edbfd40191d Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 29 Sep 2023 20:17:35 -0400 Subject: [PATCH 18/26] Move destructured props higher in function component body in InterfaceSelect.tsx --- .../LinodeSettings/InterfaceSelect.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 7c9b9b7901e..782479c6e8b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -55,17 +55,6 @@ export interface ExtendedInterface type CombinedProps = Props & VPCState; export const InterfaceSelect = (props: CombinedProps) => { - const theme = useTheme(); - const isSmallBp = useMediaQuery(theme.breakpoints.down('sm')); - const flags = useFlags(); - const { data: account } = useAccount(); - - const showVPCs = isFeatureEnabled( - 'VPCs', - Boolean(flags.vpc), - account?.capabilities ?? [] - ); - const { errors, fromAddonsPanel, @@ -81,6 +70,17 @@ export const InterfaceSelect = (props: CombinedProps) => { vpcId, } = props; + const theme = useTheme(); + const isSmallBp = useMediaQuery(theme.breakpoints.down('sm')); + const flags = useFlags(); + const { data: account } = useAccount(); + + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const [newVlan, setNewVlan] = React.useState(''); const purposeOptions = getPurposeOptions(showVPCs); From 8af257c7edb2a7128115f2e2e3e1c9a72082d458 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 2 Oct 2023 13:36:17 -0400 Subject: [PATCH 19/26] Ensure primaryInterfaceIndex is 0 if we are in Create mode upon dialog open; surface errors --- .../LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index a34b670574a..3d0bc147c8e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -482,6 +482,7 @@ export const LinodeConfigDialog = (props: Props) => { resetForm({ values: defaultFieldsValues }); setUseCustomRoot(false); setDeviceCounter(deviceCounterDefault); + setPrimaryInterfaceIndex(0); } } }, [open, config, initrdFromConfig, resetForm, queryClient, vpcEnabled]); @@ -962,11 +963,13 @@ export const LinodeConfigDialog = (props: Props) => { ipamError: formik.errors[`interfaces[${idx}].ipam_address`], labelError: formik.errors[`interfaces[${idx}].label`], - nat_1_1Error: formik.errors['ipv4.nat_1_1'], + nat_1_1Error: + formik.errors[`interfaces[${idx}].ipv4.nat_1_1`], subnetError: formik.errors[`interfaces[${idx}].subnet_id`], vpcError: formik.errors[`interfaces[${idx}].vpc_id`], - vpcIPv4Error: formik.errors['ipv4.vpc'], + vpcIPv4Error: + formik.errors[`interfaces[${idx}].ipv4.vpc`], }} handleChange={(newInterface: Interface) => handleInterfaceChange(idx, newInterface) From bc6ea6e666a909299f733c7fa6a95fc46449fee3 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 2 Oct 2023 18:12:11 -0400 Subject: [PATCH 20/26] Adjust useEffect() logic to handle for non-VPC cases to prevent VLAN bug --- .../LinodesDetail/LinodeSettings/InterfaceSelect.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 782479c6e8b..51baaa7c359 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -173,6 +173,14 @@ export const InterfaceSelect = (props: CombinedProps) => { }; React.useEffect(() => { + if (purpose !== 'vpc') { + return handleChange({ + ipam_address: ipamAddress, + label, + purpose, + }); + } + const changeObj = { ipam_address: null, label: null, @@ -215,7 +223,7 @@ export const InterfaceSelect = (props: CombinedProps) => { }, }); } - }, [autoAssignVPCIPv4, autoAssignLinodeIPv4]); + }, [autoAssignVPCIPv4, autoAssignLinodeIPv4, purpose]); const handleCreateOption = (_newVlan: string) => { setNewVlan(_newVlan); From 1c5d2101864f1e2137c58fc591fbfe50284fc38e Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Tue, 3 Oct 2023 14:28:54 -0400 Subject: [PATCH 21/26] Don't display 'None' option for VPC interfaces in Config dialog --- .../Linodes/LinodesCreate/VPCPanel.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index 2d87869af6c..700f4f7fa42 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -110,10 +110,15 @@ export const VPCPanel = (props: VPCPanelProps) => { : accumulator; }, []); - vpcDropdownOptions.unshift({ - label: 'None', - value: -1, - }); + const fromLinodeCreate = from === 'linodeCreate'; + const fromLinodeConfig = from === 'linodeConfig'; + + if (fromLinodeCreate) { + vpcDropdownOptions.unshift({ + label: 'None', + value: -1, + }); + } const subnetDropdownOptions: Item[] = vpcs @@ -127,9 +132,6 @@ export const VPCPanel = (props: VPCPanelProps) => { ? getAPIErrorOrDefault(error, 'Unable to load VPCs')[0].reason : undefined; - const fromLinodeCreate = from === 'linodeCreate'; - const fromLinodeConfig = from === 'linodeConfig'; - const getMainCopyVPC = () => { if (fromLinodeConfig) { return null; @@ -182,7 +184,7 @@ export const VPCPanel = (props: VPCPanelProps) => { value={vpcDropdownOptions.find( (option) => option.value === selectedVPCId )} - defaultValue={vpcDropdownOptions[0]} + defaultValue={fromLinodeConfig ? null : vpcDropdownOptions[0]} // If we're in the Config dialog, there is no "None" option at index 0 disabled={!regionSupportsVPCs} errorText={vpcIdError ?? vpcError} isClearable={false} @@ -190,7 +192,7 @@ export const VPCPanel = (props: VPCPanelProps) => { label={from === 'linodeCreate' ? 'Assign VPC' : 'VPC'} noOptionsMessage={() => 'Create a VPC to assign to this Linode.'} options={vpcDropdownOptions} - placeholder={''} + placeholder={'Select a VPC'} /> {vpcDropdownOptions.length <= 1 && regionSupportsVPCs && ( ({ paddingTop: theme.spacing(1.5) })}> From 6950b896731ab689293ea88daf0247253fbbbf72 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 5 Oct 2023 13:54:04 -0400 Subject: [PATCH 22/26] Fix Assign Linode bug; minor casing adjustment in variable name --- .../Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx | 4 ++-- .../src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 51baaa7c359..74ae480aee9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -100,7 +100,7 @@ export const InterfaceSelect = (props: CombinedProps) => { vlanOptions.push({ label: newVlan, value: newVlan }); } - const [autoAssignVPCIPv4, setautoAssignVPCIPv4] = React.useState(true); + const [autoAssignVPCIPv4, setAutoAssignVPCIPv4] = React.useState(true); const [autoAssignLinodeIPv4, setAutoAssignLinodeIPv4] = React.useState(false); const handlePurposeChange = (selected: Item) => { @@ -343,7 +343,7 @@ export const InterfaceSelect = (props: CombinedProps) => { ) } toggleAutoassignIPv4WithinVPCEnabled={() => - setautoAssignVPCIPv4((autoAssignVPCIPv4) => !autoAssignVPCIPv4) + setAutoAssignVPCIPv4((autoAssignVPCIPv4) => !autoAssignVPCIPv4) } assignPublicIPv4Address={autoAssignLinodeIPv4} autoassignIPv4WithinVPC={autoAssignVPCIPv4} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 5e92576d87a..2fbfb967d11 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -149,6 +149,7 @@ export const SubnetAssignLinodesDrawer = ( label: null, purpose: 'vpc', subnet_id: subnet?.id, + vpc_id: vpcId, ...(!autoAssignIPv4 && { ipv4: { vpc: chosenIP } }), }; From bfa3ea682f650d307804a2e70d5ad832828e3f0c Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 6 Oct 2023 14:30:31 -0400 Subject: [PATCH 23/26] Temporary solution for surfacing a couple of errors pending API fix --- .../LinodeConfigs/LinodeConfigDialog.tsx | 19 +++++++++++++++---- .../manager/src/utilities/formikErrorUtils.ts | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 3d0bc147c8e..768fe043775 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -258,13 +258,13 @@ export const LinodeConfigDialog = (props: Props) => { deviceCounterDefault ); + const [useCustomRoot, setUseCustomRoot] = React.useState(false); + const [ primaryInterfaceIndex, setPrimaryInterfaceIndex, ] = React.useState(); - const [useCustomRoot, setUseCustomRoot] = React.useState(false); - const regionHasVLANS = regions.some( (thisRegion) => thisRegion.id === linode?.region && @@ -395,9 +395,20 @@ export const LinodeConfigDialog = (props: Props) => { }); }; + // @TODO VPC: Remove this override and surface the field errors appropriately + // once API fixes interface index bug for ipv4.vpc & ipv4.nat_1_1 errors + const overrideFieldForIPv4 = (error: APIError[]) => { + error.forEach((err) => { + if (err.field && ['ipv4.nat_1_1', 'ipv4.vpc'].includes(err.field)) { + err.field = 'interfaces'; + } + }); + }; + formik.setSubmitting(false); overrideFieldForDevices(error); + overrideFieldForIPv4(error); handleFieldErrors(formik.setErrors, error); @@ -928,12 +939,12 @@ export const LinodeConfigDialog = (props: Props) => { text={networkInterfacesHelperText} /> - {formik.errors.interfaces ? ( + {formik.errors.interfaces && ( - ) : null} + )} {vpcEnabled && ( <> Date: Tue, 10 Oct 2023 14:58:55 -0400 Subject: [PATCH 26/26] POC to surface and clear errors at index level for VPC IPv4 field when the VPC IPv4 Autoassign checkbox is unchecked --- .../LinodeConfigs/LinodeConfigDialog.tsx | 31 ++++++++++++++++++- .../LinodeSettings/InterfaceSelect.tsx | 14 +++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 768fe043775..f78cb8225f7 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -265,6 +265,10 @@ export const LinodeConfigDialog = (props: Props) => { setPrimaryInterfaceIndex, ] = React.useState(); + const [insufficientVPCIPv4Data, setInsufficientVPCIPv4Data] = React.useState( + {} + ); + const regionHasVLANS = regions.some( (thisRegion) => thisRegion.id === linode?.region && @@ -968,6 +972,26 @@ export const LinodeConfigDialog = (props: Props) => { )} {values.interfaces.map((thisInterface, idx) => { + const surfaceVPCIPv4Error = () => { + const testing = {}; + testing[`${idx}`] = true; + + setInsufficientVPCIPv4Data((insufficientVPCIPv4Data) => ({ + ...insufficientVPCIPv4Data, + ...testing, + })); + }; + + const clearVPCIPv4Error = () => { + const testing = {}; + testing[`${idx}`] = false; + + setInsufficientVPCIPv4Data((insufficientVPCIPv4Data) => ({ + ...insufficientVPCIPv4Data, + ...testing, + })); + }; + return ( { formik.errors[`interfaces[${idx}].subnet_id`], vpcError: formik.errors[`interfaces[${idx}].vpc_id`], vpcIPv4Error: - formik.errors[`interfaces[${idx}].ipv4.vpc`], + formik.errors[`interfaces[${idx}].ipv4.vpc`] || + insufficientVPCIPv4Data[idx] + ? 'Must be a valid IPv4 address, e.g. 192.168.2.0' + : undefined, }} handleChange={(newInterface: Interface) => handleInterfaceChange(idx, newInterface) } + clearVPCIPv4Error={clearVPCIPv4Error} ipamAddress={thisInterface.ipam_address} key={`eth${idx}-interface`} label={thisInterface.label} @@ -993,6 +1021,7 @@ export const LinodeConfigDialog = (props: Props) => { region={linode?.region} slotNumber={idx} subnetId={thisInterface.subnet_id} + surfaceVPCIPv4Error={surfaceVPCIPv4Error} vpcIPv4={thisInterface.ipv4?.vpc} vpcId={thisInterface.vpc_id} /> diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index f70a30dede2..309570cc353 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -19,6 +19,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics'; export interface Props { + clearVPCIPv4Error?: () => void; fromAddonsPanel?: boolean; handleChange: (updatedInterface: ExtendedInterface) => void; ipamAddress?: null | string; @@ -27,6 +28,7 @@ export interface Props { readOnly: boolean; region?: string; slotNumber: number; + surfaceVPCIPv4Error?: () => void; } interface VPCStateErrors { @@ -56,6 +58,7 @@ type CombinedProps = Props & VPCState; export const InterfaceSelect = (props: CombinedProps) => { const { + clearVPCIPv4Error, errors, fromAddonsPanel, handleChange, @@ -66,6 +69,7 @@ export const InterfaceSelect = (props: CombinedProps) => { region, slotNumber, subnetId, + surfaceVPCIPv4Error, vpcIPv4, vpcId, } = props; @@ -227,6 +231,16 @@ export const InterfaceSelect = (props: CombinedProps) => { } }, [autoAssignVPCIPv4, autoAssignLinodeIPv4, purpose]); + React.useEffect(() => { + if (!autoAssignVPCIPv4 && !vpcIPv4 && surfaceVPCIPv4Error) { + surfaceVPCIPv4Error(); + } + + if (!autoAssignVPCIPv4 && vpcIPv4 && clearVPCIPv4Error) { + clearVPCIPv4Error(); + } + }, [autoAssignVPCIPv4, vpcIPv4]); + const handleCreateOption = (_newVlan: string) => { setNewVlan(_newVlan); handleChange({