diff --git a/packages/manager/.changeset/pr-10116-added-1706293067938.md b/packages/manager/.changeset/pr-10116-added-1706293067938.md new file mode 100644 index 00000000000..b95ebe9a78c --- /dev/null +++ b/packages/manager/.changeset/pr-10116-added-1706293067938.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Support for VPC IPv4 Ranges in Linode Create flow and 'VPC IPv4 Ranges' column to inner Subnets table on VPC Detail page ([#10116](https://github.com/linode/manager/pull/10116)) diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 3f9b0446a09..63642a7184e 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -177,6 +177,7 @@ export const MultipleIPInput = React.memo((props: Props) => { direction="row" justifyContent="center" key={`domain-transfer-ip-${idx}`} + maxWidth={forVPCIPv4Ranges ? '415px' : undefined} spacing={2} > diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx index 1f7b58a7e4b..cad641822ed 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx @@ -99,8 +99,10 @@ import { } from './types'; import type { Tab } from 'src/components/Tabs/TabLinkList'; +import { ExtendedIP } from 'src/utilities/ipUtils'; export interface LinodeCreateProps { + additionalIPv4RangesForVPC: ExtendedIP[]; assignPublicIPv4Address: boolean; autoassignIPv4WithinVPC: boolean; checkValidation: LinodeCreateValidation; @@ -108,6 +110,7 @@ export interface LinodeCreateProps { firewallId?: number; handleAgreementChange: () => void; handleFirewallChange: (firewallId: number) => void; + handleIPv4RangesForVPC: (ranges: ExtendedIP[]) => void; handleShowApiAwarenessModal: () => void; handleSubmitForm: HandleSubmit; handleSubnetChange: (subnetId: number) => void; @@ -641,9 +644,11 @@ export class LinodeCreate extends React.PureComponent< toggleAutoassignIPv4WithinVPCEnabled={ this.props.toggleAutoassignIPv4WithinVPCEnabled } + additionalIPv4RangesForVPC={this.props.additionalIPv4RangesForVPC} assignPublicIPv4Address={this.props.assignPublicIPv4Address} autoassignIPv4WithinVPC={this.props.autoassignIPv4WithinVPC} from="linodeCreate" + handleIPv4RangeChange={this.props.handleIPv4RangesForVPC} handleSelectVPC={this.props.setSelectedVPC} handleSubnetChange={this.props.handleSubnetChange} handleVPCIPv4Change={this.props.handleVPCIPv4Change} @@ -822,6 +827,9 @@ export class LinodeCreate extends React.PureComponent< this.props.selectedVPCId !== -1 ) { const vpcInterfaceData: InterfacePayload = { + ip_ranges: this.props.additionalIPv4RangesForVPC + .map((ipRange) => ipRange.address) + .filter((ipRange) => ipRange !== ''), ipam_address: null, ipv4: { nat_1_1: this.props.assignPublicIPv4Address ? 'any' : undefined, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index c666a2d414f..3e46c43e8ac 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -69,6 +69,7 @@ import { } from 'src/utilities/formatRegion'; import { isEURegion } from 'src/utilities/formatRegion'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; +import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { validatePassword } from 'src/utilities/validatePassword'; @@ -85,11 +86,12 @@ import type { LinodeTypeClass, PriceObject, } from '@linode/api-v4/lib/linodes'; -import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; +import { ExtendedIP } from 'src/utilities/ipUtils'; const DEFAULT_IMAGE = 'linode/debian11'; interface State { + additionalIPv4RangesForVPC: ExtendedIP[]; assignPublicIPv4Address: boolean; attachedVLANLabel: null | string; authorized_users: string[]; @@ -140,6 +142,7 @@ type CombinedProps = WithSnackbarProps & WithAccountSettingsProps; const defaultState: State = { + additionalIPv4RangesForVPC: [], assignPublicIPv4Address: false, attachedVLANLabel: '', authorized_users: [], @@ -277,6 +280,7 @@ class LinodeCreateContainer extends React.PureComponent { firewallId={this.state.selectedfirewallId} handleAgreementChange={this.handleAgreementChange} handleFirewallChange={this.handleFirewallChange} + handleIPv4RangesForVPC={this.handleVPCIPv4RangesChange} handleSelectUDFs={this.setUDFs} handleShowApiAwarenessModal={this.handleShowApiAwarenessModal} handleSubmitForm={this.submitForm} @@ -515,6 +519,10 @@ class LinodeCreateContainer extends React.PureComponent { this.setState({ vpcIPv4AddressOfLinode: IPv4 }); }; + handleVPCIPv4RangesChange = (ranges: ExtendedIP[]) => { + this.setState({ additionalIPv4RangesForVPC: ranges }); + }; + params = getQueryParamsFromQueryString(this.props.location.search) as Record< string, string diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx index a203e30fee4..e5018a0679e 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx @@ -17,9 +17,11 @@ afterEach(() => { }); const props = { + additionalIPv4RangesForVPC: [], assignPublicIPv4Address: false, autoassignIPv4WithinVPC: true, from: 'linodeCreate' as VPCPanelProps['from'], + handleIPv4RangeChange: vi.fn(), handleSelectVPC: vi.fn(), handleSubnetChange: vi.fn(), handleVPCIPv4Change: vi.fn(), @@ -105,7 +107,12 @@ describe('VPCPanel', () => { }); it('should have the VPC IPv4 auto-assign checkbox checked by default', async () => { - const _props = { ...props, region: 'us-east', selectedVPCId: 5 }; + const _props = { + ...props, + region: 'us-east', + selectedSubnetId: 2, + selectedVPCId: 5, + }; server.use( rest.get('*/regions', (req, res, ctx) => { @@ -234,6 +241,7 @@ describe('VPCPanel', () => { ...props, autoassignIPv4WithinVPC: false, region: 'us-east', + selectedSubnetId: 2, selectedVPCId: 5, vpcIPv4AddressOfLinode: '10.0.4.3', }; @@ -269,6 +277,7 @@ describe('VPCPanel', () => { ...props, assignPublicIPv4Address: true, region: 'us-east', + selectedSubnetId: 2, selectedVPCId: 5, }; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index a960f2c2bff..31dafd60d7d 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -14,6 +14,7 @@ import { Stack } from 'src/components/Stack'; import { TextField } from 'src/components/TextField'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; +import { AssignIPRanges } from 'src/features/VPCs/VPCDetail/AssignIPRanges'; import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP } from 'src/features/VPCs/constants'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { useFlags } from 'src/hooks/useFlags'; @@ -22,15 +23,18 @@ 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 { ExtendedIP } from 'src/utilities/ipUtils'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { VPCCreateDrawer } from './VPCCreateDrawer'; import { REGION_CAVEAT_HELPER_TEXT } from './constants'; export interface VPCPanelProps { + additionalIPv4RangesForVPC: ExtendedIP[]; assignPublicIPv4Address: boolean; autoassignIPv4WithinVPC: boolean; from: 'linodeConfig' | 'linodeCreate'; + handleIPv4RangeChange: (ranges: ExtendedIP[]) => void; handleSelectVPC: (vpcId: number) => void; handleSubnetChange: (subnetId: number) => void; handleVPCIPv4Change: (IPv4: string) => void; @@ -50,9 +54,11 @@ const ERROR_GROUP_STRING = 'vpc-errors'; export const VPCPanel = (props: VPCPanelProps) => { const { + additionalIPv4RangesForVPC, assignPublicIPv4Address, autoassignIPv4WithinVPC, from, + handleIPv4RangeChange, handleSelectVPC, handleSubnetChange, handleVPCIPv4Change, @@ -247,87 +253,105 @@ export const VPCPanel = (props: VPCPanelProps) => { options={subnetDropdownOptions} placeholder="Select Subnet" /> - ({ - marginLeft: '2px', - paddingTop: theme.spacing(), - })} - alignItems="center" - display="flex" - flexDirection="row" - > - + ({ + 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" /> - } - 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" - > - + {!autoassignIPv4WithinVPC && ( + handleVPCIPv4Change(e.target.value)} + required={!autoassignIPv4WithinVPC} + value={vpcIPv4AddressOfLinode} /> - } - label={ - - - Assign a public IPv4 address for this Linode - - - - } - /> - - {assignPublicIPv4Address && publicIPv4Error && ( - ({ - color: theme.color.red, - })} - > - {publicIPv4Error} - + )} + ({ + marginLeft: '2px', + marginTop: !autoassignIPv4WithinVPC ? theme.spacing() : 0, + })} + alignItems="center" + display="flex" + > + + } + label={ + + + Assign a public IPv4 address for this Linode + + + + } + /> + + {assignPublicIPv4Address && publicIPv4Error && ( + ({ + color: theme.color.red, + })} + > + {publicIPv4Error} + + )} + + )} )} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx index 6a299c39682..c0f768ba5c1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/InterfaceSelect.tsx @@ -386,9 +386,11 @@ export const InterfaceSelect = (props: CombinedProps) => { toggleAutoassignIPv4WithinVPCEnabled={() => setAutoAssignVPCIPv4((autoAssignVPCIPv4) => !autoAssignVPCIPv4) } + additionalIPv4RangesForVPC={[]} // @TODO VPC: temporary placeholder to before M3-7645 is worked on to prevent errors assignPublicIPv4Address={autoAssignLinodeIPv4} autoassignIPv4WithinVPC={autoAssignVPCIPv4} from="linodeConfig" + handleIPv4RangeChange={() => null} // @TODO VPC: temporary placeholder to before M3-7645 is worked on to prevent errors handleSelectVPC={handleVPCLabelChange} handleSubnetChange={handleSubnetChange} handleVPCIPv4Change={handleVPCIPv4Input} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx index 279ed791858..5d2c103b148 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx @@ -55,7 +55,7 @@ describe('SubnetLinodeRow', () => { const handleUnassignLinode = vi.fn(); - it('should display linode label, reboot status, VPC IPv4 address, associated firewalls, and Reboot and Unassign buttons', async () => { + it('should display linode label, reboot status, VPC IPv4 address, associated firewalls, IPv4 chip, and Reboot and Unassign buttons', async () => { const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' }); server.use( rest.get('*/instances/*/configs', async (req, res, ctx) => { @@ -100,11 +100,15 @@ describe('SubnetLinodeRow', () => { getAllByText('10.0.0.0'); getByText(mockFirewall0); - const rebootLinodeButton = getAllByRole('button')[1]; + const plusChipButton = getAllByRole('button')[1]; + expect(plusChipButton).toHaveTextContent('+1'); + + const rebootLinodeButton = getAllByRole('button')[2]; expect(rebootLinodeButton).toHaveTextContent('Reboot'); fireEvent.click(rebootLinodeButton); expect(handlePowerActionsLinode).toHaveBeenCalled(); - const unassignLinodeButton = getAllByRole('button')[2]; + + const unassignLinodeButton = getAllByRole('button')[3]; expect(unassignLinodeButton).toHaveTextContent('Unassign Linode'); fireEvent.click(unassignLinodeButton); expect(handleUnassignLinode).toHaveBeenCalled(); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx index 43977566633..be9474d2da9 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx @@ -22,6 +22,7 @@ import { useLinodeQuery, } from 'src/queries/linodes/linodes'; import { capitalizeAllWords } from 'src/utilities/capitalize'; +import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; import { NETWORK_INTERFACES_GUIDE_URL, @@ -204,6 +205,16 @@ export const SubnetLinodeRow = (props: Props) => { )} + + + {getIPRangesCellContents( + configs ?? [], + configsLoading, + subnetId, + configsError ?? undefined + )} + + {getFirewallsCellString( @@ -294,6 +305,30 @@ const getIPv4Link = (configInterface: Interface | undefined): JSX.Element => { ); }; +const getIPRangesCellContents = ( + configs: Config[], + loading: boolean, + subnetId: number, + error?: APIError[] +): JSX.Element | string => { + if (loading) { + return 'Loading...'; + } + + if (error) { + return 'Error retrieving VPC IPv4s'; + } + + if (configs.length === 0) { + return 'None'; + } + + const configInterface = getSubnetInterfaceFromConfigs(configs, subnetId); + return determineNoneSingleOrMultipleWithChip( + configInterface?.ip_ranges ?? [] + ); +}; + const getFirewallLinks = (data: Firewall[]): JSX.Element => { const firstThreeFirewalls = data.slice(0, 3); return ( @@ -325,6 +360,9 @@ export const SubnetLinodeTableRowHead = ( VPC IPv4 + + VPC IPv4 Ranges + Firewalls