From 85ed5bac7e1d29c3a876c2babb2e7e64cedb69de Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 8 Oct 2024 21:29:40 -0400 Subject: [PATCH 1/7] initial work to support multiple private ipv4s on a Linode --- .../NodeBalancers/ConfigNodeIPSelect.tsx | 163 +++++++++--------- .../ConfigNodeIPSelect.utils.test.ts | 32 ++++ .../NodeBalancers/ConfigNodeIPSelect.utils.ts | 36 ++++ .../NodeBalancers/NodeBalancerConfigNode.tsx | 7 +- .../manager/src/utilities/ipUtils.test.ts | 13 ++ packages/manager/src/utilities/ipUtils.ts | 8 + 6 files changed, 171 insertions(+), 88 deletions(-) create mode 100644 packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts create mode 100644 packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts create mode 100644 packages/manager/src/utilities/ipUtils.test.ts diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx index 6630b7509c9..0657072aa78 100644 --- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx @@ -1,96 +1,95 @@ -import { Box } from '@mui/material'; -import * as React from 'react'; +import React from 'react'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { SelectedIcon } from 'src/components/Autocomplete/Autocomplete.styles'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; -import { privateIPRegex } from 'src/utilities/ipUtils'; +import { Box } from 'src/components/Box'; +import { Stack } from 'src/components/Stack'; +import { Typography } from 'src/components/Typography'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; -import type { Linode } from '@linode/api-v4/lib/linodes'; -import type { TextFieldProps } from 'src/components/TextField'; +import { getPrivateIPOptions } from './ConfigNodeIPSelect.utils'; -interface ConfigNodeIPSelectProps { +interface Props { + /** + * Disables the select + */ disabled?: boolean; - errorText?: string; + /** + * Validation error text + */ + errorText: string | undefined; + /** + * Function that is called when the select's value changes + */ handleChange: (nodeIndex: number, ipAddress: null | string) => void; + /** + * Override the default input `id` for the select + */ inputId?: string; - nodeAddress?: string; + /** + * The selected private IP address + */ + nodeAddress: string | undefined; + /** + * The index of the config node in state + */ nodeIndex: number; - selectedRegion?: string; - textfieldProps: Omit; + /** + * The region for which to load Linodes and to show private IPs + * @note IPs won't load until a region is passed + */ + region: string | undefined; } -export const ConfigNodeIPSelect = React.memo( - (props: ConfigNodeIPSelectProps) => { - const { - handleChange: _handleChange, - inputId, - nodeAddress, - nodeIndex, - } = props; - const handleChange = (linode: Linode | null) => { - if (!linode) { - _handleChange(nodeIndex, null); - } +export const ConfigNodeIPSelect = React.memo((props: Props) => { + const { + disabled, + errorText, + handleChange, + inputId, + nodeAddress, + nodeIndex, + region, + } = props; - const thisLinodesPrivateIP = linode?.ipv4.find((ipv4) => - ipv4.match(privateIPRegex) - ); + const { data: linodes, error, isLoading } = useAllLinodesQuery( + {}, + { region }, + region !== undefined + ); - if (!thisLinodesPrivateIP) { - return; - } + const options = getPrivateIPOptions(linodes); - /** - * we can be sure the selection has a private IP because of the - * filterCondition prop in the render method below - */ - _handleChange(nodeIndex, thisLinodesPrivateIP); - }; - - return ( - { - /** - * if the Linode doesn't have an private IP OR if the Linode - * is in a different region that the NodeBalancer, don't show it - * in the select dropdown - */ - return ( - !!linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex)) && - linode.region === props.selectedRegion - ); - }} - renderOption={(linode, selected) => ( - <> - - - {linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex))} - -
{linode.label}
-
- - - )} - renderOptionLabel={(linode) => - linode.ipv4.find((eachIP) => eachIP.match(privateIPRegex)) ?? '' - } - clearable - disabled={props.disabled} - errorText={props.errorText} - id={inputId} - label="IP Address" - noMarginTop - onSelectionChange={handleChange} - placeholder="Enter IP Address" - value={(linode) => linode.ipv4.some((ip) => ip === nodeAddress)} - /> - ); - } -); + return ( + ( +
  • + + + theme.font.bold} + > + {option.label} + + {option.linode.label} + + {selected && } + +
  • + )} + disabled={disabled} + errorText={errorText ?? error?.[0].reason} + id={inputId} + label="IP Address" + loading={isLoading} + noMarginTop + noOptionsText="No options - please ensure you have at least 1 Linode with a private IP located in the selected region." + onChange={(e, value) => handleChange(nodeIndex, value?.label ?? null)} + options={options} + placeholder="Enter IP Address" + value={options.find((o) => o.label === nodeAddress) ?? null} + /> + ); +}); diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts new file mode 100644 index 00000000000..e95b738d915 --- /dev/null +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts @@ -0,0 +1,32 @@ +import { linodeFactory } from 'src/factories'; + +import { getPrivateIPOptions } from './ConfigNodeIPSelect.utils'; + +describe('getPrivateIPOptions', () => { + it('returns an empty array when linodes are undefined', () => { + expect(getPrivateIPOptions(undefined)).toStrictEqual([]); + }); + + it('returns an empty array when there are no Linodes', () => { + expect(getPrivateIPOptions([])).toStrictEqual([]); + }); + + it('returns an option for each private IPv4 on a Linode', () => { + const linode = linodeFactory.build({ ipv4: ['192.168.1.1', '172.16.0.1'] }); + + expect(getPrivateIPOptions([linode])).toStrictEqual([ + { label: '192.168.1.1', linode }, + { label: '172.16.0.1', linode }, + ]); + }); + + it('does not return an option for public IPv4s on a Linode', () => { + const linode = linodeFactory.build({ + ipv4: ['143.198.125.230', '192.168.1.1'], + }); + + expect(getPrivateIPOptions([linode])).toStrictEqual([ + { label: '192.168.1.1', linode }, + ]); + }); +}); diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts new file mode 100644 index 00000000000..685ba7fa9c9 --- /dev/null +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts @@ -0,0 +1,36 @@ +import { getIsPrivateIP } from 'src/utilities/ipUtils'; + +import type { Linode } from '@linode/api-v4'; + +interface PrivateIPOption { + /** + * A private IPv4 address + */ + label: string; + /** + * The Linode associated with the private IPv4 address + */ + linode: Linode; +} + +/** + * Given an array of Linodes, this function returns an array of private + * IPv4 options intended to be used in a Select component. + */ +export const getPrivateIPOptions = (linodes: Linode[] | undefined) => { + if (!linodes) { + return []; + } + + const options: PrivateIPOption[] = []; + + for (const linode of linodes) { + for (const ip of linode.ipv4) { + if (getIsPrivateIP(ip)) { + options.push({ label: ip, linode }); + } + } + } + + return options; +}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx index 05f47659c98..c97f7bc1345 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx @@ -134,18 +134,13 @@ export const NodeBalancerConfigNode = React.memo( diff --git a/packages/manager/src/utilities/ipUtils.test.ts b/packages/manager/src/utilities/ipUtils.test.ts new file mode 100644 index 00000000000..282f6506a6b --- /dev/null +++ b/packages/manager/src/utilities/ipUtils.test.ts @@ -0,0 +1,13 @@ +import { getIsPrivateIP } from './ipUtils'; + +describe('getIsPrivateIP', () => { + it('returns true for a private IPv4', () => { + expect(getIsPrivateIP('192.168.1.1')).toBe(true); + expect(getIsPrivateIP('172.16.5.12')).toBe(true); + }); + + it('returns false for a public IPv4', () => { + expect(getIsPrivateIP('45.79.245.236')).toBe(false); + expect(getIsPrivateIP('100.78.0.8')).toBe(false); + }); +}); diff --git a/packages/manager/src/utilities/ipUtils.ts b/packages/manager/src/utilities/ipUtils.ts index 74368b21f55..d7dbe94cbdf 100644 --- a/packages/manager/src/utilities/ipUtils.ts +++ b/packages/manager/src/utilities/ipUtils.ts @@ -12,6 +12,14 @@ export const removePrefixLength = (ip: string) => ip.replace(/\/\d+/, ''); */ export const privateIPRegex = /^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.|^fd/; +/** + * Determines if an IPv4 address is private + * @returns true if the given IPv4 address is private + */ +export const getIsPrivateIP = (ip: string) => { + return privateIPRegex.test(ip); +}; + export interface ExtendedIP { address: string; error?: string; From 17dd23efcb8562172f2658591bca9b0c141a4de8 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 8 Oct 2024 22:02:07 -0400 Subject: [PATCH 2/7] fix style bug --- .../src/features/NodeBalancers/ConfigNodeIPSelect.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx index 0657072aa78..2372102a9d3 100644 --- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx @@ -65,7 +65,14 @@ export const ConfigNodeIPSelect = React.memo((props: Props) => { (
  • - + Date: Tue, 8 Oct 2024 22:30:13 -0400 Subject: [PATCH 3/7] clean up validation and utils --- .../LinodeCreate/shared/LinodeSelectTable.tsx | 4 ++-- .../src/features/Linodes/LinodeCreate/utilities.ts | 5 ++--- .../features/Linodes/LinodesLanding/IPAddress.tsx | 5 +++-- .../Managed/SSHAccess/EditSSHAccessDrawer.tsx | 6 ++---- .../NodeBalancers/ConfigNodeIPSelect.utils.ts | 4 ++-- packages/manager/src/utilities/ipUtils.test.ts | 12 ++++++------ packages/manager/src/utilities/ipUtils.ts | 10 +++------- packages/validation/src/nodebalancers.schema.ts | 7 +++---- 8 files changed, 23 insertions(+), 30 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx index 8321d07ee4f..fc7673d7e3e 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx @@ -24,7 +24,7 @@ import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useLinodesQuery } from 'src/queries/linodes/linodes'; import { sendLinodePowerOffEvent } from 'src/utilities/analytics/customEventAnalytics'; -import { privateIPRegex } from 'src/utilities/ipUtils'; +import { isPrivateIP } from 'src/utilities/ipUtils'; import { isNumeric } from 'src/utilities/stringUtils'; import { @@ -105,7 +105,7 @@ export const LinodeSelectTable = (props: Props) => { const queryClient = useQueryClient(); const handleSelect = async (linode: Linode) => { - const hasPrivateIP = linode.ipv4.some((ipv4) => privateIPRegex.test(ipv4)); + const hasPrivateIP = linode.ipv4.some(isPrivateIP); reset((prev) => ({ ...prev, backup_id: null, diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts index 55641bee1ae..92fd7443e30 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts @@ -6,7 +6,7 @@ import { linodeQueries } from 'src/queries/linodes/linodes'; import { stackscriptQueries } from 'src/queries/stackscripts'; import { sendCreateLinodeEvent } from 'src/utilities/analytics/customEventAnalytics'; import { sendLinodeCreateFormErrorEvent } from 'src/utilities/analytics/formEventAnalytics'; -import { privateIPRegex } from 'src/utilities/ipUtils'; +import { isPrivateIP } from 'src/utilities/ipUtils'; import { utoa } from 'src/utilities/metadata'; import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; import { omitProps } from 'src/utilities/omittedProps'; @@ -299,8 +299,7 @@ export const defaultValues = async ( ? await queryClient.ensureQueryData(linodeQueries.linode(params.linodeID)) : null; - const privateIp = - linode?.ipv4.some((ipv4) => privateIPRegex.test(ipv4)) ?? false; + const privateIp = linode?.ipv4.some(isPrivateIP) ?? false; const values: LinodeCreateFormValues = { backup_id: params.backupID, diff --git a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx index a7e1f3de72e..37ed6afd9dc 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx @@ -1,9 +1,9 @@ +import { PRIVATE_IPv4_REGEX } from '@linode/validation'; import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { ShowMore } from 'src/components/ShowMore/ShowMore'; import { PublicIpsUnassignedTooltip } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; -import { privateIPRegex } from 'src/utilities/ipUtils'; import { tail } from 'src/utilities/tail'; import { @@ -55,7 +55,8 @@ export interface IPAddressProps { } export const sortIPAddress = (ip1: string, ip2: string) => - (privateIPRegex.test(ip1) ? 1 : -1) - (privateIPRegex.test(ip2) ? 1 : -1); + (PRIVATE_IPv4_REGEX.test(ip1) ? 1 : -1) - + (PRIVATE_IPv4_REGEX.test(ip2) ? 1 : -1); export const IPAddress = (props: IPAddressProps) => { const { diff --git a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx index 1684f27b21e..be63b6a4e08 100644 --- a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx @@ -15,7 +15,7 @@ import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { privateIPRegex, removePrefixLength } from 'src/utilities/ipUtils'; +import { isPrivateIP, removePrefixLength } from 'src/utilities/ipUtils'; import { StyledIPGrid, @@ -168,9 +168,7 @@ const EditSSHAccessDrawer = (props: EditSSHAccessDrawerProps) => { }, ...options // Remove Private IPs - .filter( - (option) => !privateIPRegex.test(option.value) - ) + .filter((option) => !isPrivateIP(option.value)) // Remove the prefix length from each option. .map((option) => ({ label: removePrefixLength(option.value), diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts index 685ba7fa9c9..1dfc830bbf8 100644 --- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.ts @@ -1,4 +1,4 @@ -import { getIsPrivateIP } from 'src/utilities/ipUtils'; +import { isPrivateIP } from 'src/utilities/ipUtils'; import type { Linode } from '@linode/api-v4'; @@ -26,7 +26,7 @@ export const getPrivateIPOptions = (linodes: Linode[] | undefined) => { for (const linode of linodes) { for (const ip of linode.ipv4) { - if (getIsPrivateIP(ip)) { + if (isPrivateIP(ip)) { options.push({ label: ip, linode }); } } diff --git a/packages/manager/src/utilities/ipUtils.test.ts b/packages/manager/src/utilities/ipUtils.test.ts index 282f6506a6b..371cbf7b855 100644 --- a/packages/manager/src/utilities/ipUtils.test.ts +++ b/packages/manager/src/utilities/ipUtils.test.ts @@ -1,13 +1,13 @@ -import { getIsPrivateIP } from './ipUtils'; +import { isPrivateIP } from './ipUtils'; -describe('getIsPrivateIP', () => { +describe('isPrivateIP', () => { it('returns true for a private IPv4', () => { - expect(getIsPrivateIP('192.168.1.1')).toBe(true); - expect(getIsPrivateIP('172.16.5.12')).toBe(true); + expect(isPrivateIP('192.168.1.1')).toBe(true); + expect(isPrivateIP('172.16.5.12')).toBe(true); }); it('returns false for a public IPv4', () => { - expect(getIsPrivateIP('45.79.245.236')).toBe(false); - expect(getIsPrivateIP('100.78.0.8')).toBe(false); + expect(isPrivateIP('45.79.245.236')).toBe(false); + expect(isPrivateIP('100.78.0.8')).toBe(false); }); }); diff --git a/packages/manager/src/utilities/ipUtils.ts b/packages/manager/src/utilities/ipUtils.ts index d7dbe94cbdf..06392a895a2 100644 --- a/packages/manager/src/utilities/ipUtils.ts +++ b/packages/manager/src/utilities/ipUtils.ts @@ -1,3 +1,4 @@ +import { PRIVATE_IPv4_REGEX } from '@linode/validation'; import { parseCIDR, parse as parseIP } from 'ipaddr.js'; /** @@ -7,17 +8,12 @@ import { parseCIDR, parse as parseIP } from 'ipaddr.js'; */ export const removePrefixLength = (ip: string) => ip.replace(/\/\d+/, ''); -/** - * Regex for determining if a string is a private IP Addresses - */ -export const privateIPRegex = /^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.|^fd/; - /** * Determines if an IPv4 address is private * @returns true if the given IPv4 address is private */ -export const getIsPrivateIP = (ip: string) => { - return privateIPRegex.test(ip); +export const isPrivateIP = (ip: string) => { + return PRIVATE_IPv4_REGEX.test(ip); }; export interface ExtendedIP { diff --git a/packages/validation/src/nodebalancers.schema.ts b/packages/validation/src/nodebalancers.schema.ts index 46d2d889e2b..4cc2c15793c 100644 --- a/packages/validation/src/nodebalancers.schema.ts +++ b/packages/validation/src/nodebalancers.schema.ts @@ -3,6 +3,8 @@ import { array, boolean, mixed, number, object, string } from 'yup'; const PORT_WARNING = 'Port must be between 1 and 65535.'; const LABEL_WARNING = 'Label must be between 3 and 32 characters.'; +export const PRIVATE_IPv4_REGEX = /^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.|^fd/; + export const nodeBalancerConfigNodeSchema = object({ label: string() .matches( @@ -16,10 +18,7 @@ export const nodeBalancerConfigNodeSchema = object({ address: string() .typeError('IP address is required.') .required('IP address is required.') - .matches( - /^192\.168\.\d{1,3}\.\d{1,3}$/, - 'Must be a valid private IPv4 address.' - ), + .matches(PRIVATE_IPv4_REGEX, 'Must be a valid private IPv4 address.'), port: number() .typeError('Port must be a number.') From 7a40a6657f42ee06b98a1afd0c567b01f16aa1cc Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 8 Oct 2024 23:18:25 -0400 Subject: [PATCH 4/7] add changeset --- packages/manager/.changeset/pr-11069-added-1728443895478.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-11069-added-1728443895478.md diff --git a/packages/manager/.changeset/pr-11069-added-1728443895478.md b/packages/manager/.changeset/pr-11069-added-1728443895478.md new file mode 100644 index 00000000000..1b40723bdef --- /dev/null +++ b/packages/manager/.changeset/pr-11069-added-1728443895478.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Support linodes with multiple private IPs in NodeBalancer configurations ([#11069](https://github.com/linode/manager/pull/11069)) From e99ecf43f3e7de23575c6cb7a2ad6600895081a4 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 8 Oct 2024 23:23:08 -0400 Subject: [PATCH 5/7] add more detailed changesets --- .../validation/.changeset/pr-11069-added-1728444089255.md | 5 +++++ .../validation/.changeset/pr-11069-changed-1728444173048.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/validation/.changeset/pr-11069-added-1728444089255.md create mode 100644 packages/validation/.changeset/pr-11069-changed-1728444173048.md diff --git a/packages/validation/.changeset/pr-11069-added-1728444089255.md b/packages/validation/.changeset/pr-11069-added-1728444089255.md new file mode 100644 index 00000000000..42512f52050 --- /dev/null +++ b/packages/validation/.changeset/pr-11069-added-1728444089255.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Added +--- + +`PRIVATE_IPv4_REGEX` for determining if an IPv4 address is private ([#11069](https://github.com/linode/manager/pull/11069)) diff --git a/packages/validation/.changeset/pr-11069-changed-1728444173048.md b/packages/validation/.changeset/pr-11069-changed-1728444173048.md new file mode 100644 index 00000000000..286ee280e1f --- /dev/null +++ b/packages/validation/.changeset/pr-11069-changed-1728444173048.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +Updated `nodeBalancerConfigNodeSchema` to allow any private IPv4 rather than just \`192\.168\` IPs ([#11069](https://github.com/linode/manager/pull/11069)) From e1a09da5b2c676a4cc2b5b2f26e5b87fc86a9713 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 10 Oct 2024 12:04:10 -0400 Subject: [PATCH 6/7] feedback @hkhalil-akamai --- packages/manager/.changeset/pr-11069-added-1728443895478.md | 5 ----- packages/manager/.changeset/pr-11069-fixed-1728443895478.md | 5 +++++ .../src/features/Linodes/LinodesLanding/IPAddress.tsx | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 packages/manager/.changeset/pr-11069-added-1728443895478.md create mode 100644 packages/manager/.changeset/pr-11069-fixed-1728443895478.md diff --git a/packages/manager/.changeset/pr-11069-added-1728443895478.md b/packages/manager/.changeset/pr-11069-added-1728443895478.md deleted file mode 100644 index 1b40723bdef..00000000000 --- a/packages/manager/.changeset/pr-11069-added-1728443895478.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Support linodes with multiple private IPs in NodeBalancer configurations ([#11069](https://github.com/linode/manager/pull/11069)) diff --git a/packages/manager/.changeset/pr-11069-fixed-1728443895478.md b/packages/manager/.changeset/pr-11069-fixed-1728443895478.md new file mode 100644 index 00000000000..057ac261ea2 --- /dev/null +++ b/packages/manager/.changeset/pr-11069-fixed-1728443895478.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Support Linodes with multiple private IPs in NodeBalancer configurations ([#11069](https://github.com/linode/manager/pull/11069)) diff --git a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx index 37ed6afd9dc..424e4f7c87f 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx @@ -1,9 +1,9 @@ -import { PRIVATE_IPv4_REGEX } from '@linode/validation'; import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { ShowMore } from 'src/components/ShowMore/ShowMore'; import { PublicIpsUnassignedTooltip } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; +import { isPrivateIP } from 'src/utilities/ipUtils'; import { tail } from 'src/utilities/tail'; import { @@ -55,8 +55,7 @@ export interface IPAddressProps { } export const sortIPAddress = (ip1: string, ip2: string) => - (PRIVATE_IPv4_REGEX.test(ip1) ? 1 : -1) - - (PRIVATE_IPv4_REGEX.test(ip2) ? 1 : -1); + (isPrivateIP(ip1) ? 1 : -1) - (isPrivateIP(ip2) ? 1 : -1); export const IPAddress = (props: IPAddressProps) => { const { From 6f7cd1c162147a8bbcb69c8676f1f7b0465f1385 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 10 Oct 2024 12:10:23 -0400 Subject: [PATCH 7/7] clean `LinodeSelect` extra props --- .../LinodeSelect/LinodeSelect.test.tsx | 42 +------------------ .../Linodes/LinodeSelect/LinodeSelect.tsx | 20 --------- 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx index a6c44f5eef3..8a0b44c0abf 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx @@ -1,4 +1,3 @@ -import { Linode } from '@linode/api-v4'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -8,49 +7,12 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { LinodeSelect } from './LinodeSelect'; -const fakeLinodeData = linodeFactory.build({ - id: 1, - image: 'metadata-test-image', - label: 'metadata-test-region', - region: 'eu-west', -}); +import type { Linode } from '@linode/api-v4'; + const TEXTFIELD_ID = 'textfield-input'; describe('LinodeSelect', () => { - test('renders custom options using renderOption', async () => { - // Create a mock renderOption function - const mockRenderOption = (linode: Linode, selected: boolean) => ( - - {`${linode.label} - ${selected ? 'Selected' : 'Not Selected'}`} - - ); - - // Render the component with the custom renderOption function - renderWithTheme( - - ); - - const input = screen.getByTestId(TEXTFIELD_ID); - - // Open the dropdown - await userEvent.click(input); - - // Wait for the options to load (use some unique identifier for the options) - await waitFor(() => { - const customOption = screen.getByTestId('custom-option-1'); - expect(customOption).toBeInTheDocument(); - expect(customOption).toHaveTextContent( - 'metadata-test-region - Not Selected' - ); - }); - }); test('should display custom no options message', async () => { const customNoOptionsMessage = 'Custom No Options Message'; const options: Linode[] = []; // Assuming no options are available diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx index 84722abb76d..0b0de416bac 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx @@ -45,10 +45,6 @@ interface LinodeSelectProps { optionsFilter?: (linode: Linode) => boolean; /* Displayed when the input is blank. */ placeholder?: string; - /* Render a custom option. */ - renderOption?: (linode: Linode, selected: boolean) => JSX.Element; - /* Render a custom option label. */ - renderOptionLabel?: (linode: Linode) => string; /* Displays an indication that the input is required. */ required?: boolean; /* Adds custom styles to the component. */ @@ -98,8 +94,6 @@ export const LinodeSelect = ( options, optionsFilter, placeholder, - renderOption, - renderOptionLabel, sx, value, } = props; @@ -122,9 +116,6 @@ export const LinodeSelect = ( return ( - renderOptionLabel ? renderOptionLabel(linode) : linode.label - } isOptionEqualToValue={ checkIsOptionEqualToValue ? (option, value) => option.id === value.id @@ -145,17 +136,6 @@ export const LinodeSelect = ( ? 'Select Linodes' : 'Select a Linode' } - renderOption={ - renderOption - ? (props, option, { selected }) => { - return ( -
  • - {renderOption(option, selected)} -
  • - ); - } - : undefined - } value={ typeof value === 'function' ? multiple && Array.isArray(value)