From ba0302349d8734fce9933e88b496dab0c06e7608 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Wed, 17 Jan 2024 13:32:02 -0500 Subject: [PATCH 01/21] Make display of 'BETA Feedback' link conditoned based solely on feature flag so the link disappears once we turn off the feature flag for GA --- packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx | 4 +++- packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx index 12b46d41625..2f2191a7e98 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx @@ -16,6 +16,7 @@ import { VPC_FEEDBACK_FORM_URL, VPC_LABEL, } from 'src/features/VPCs/constants'; +import { useFlags } from 'src/hooks/useFlags'; import { useRegionsQuery } from 'src/queries/regions'; import { useVPCQuery } from 'src/queries/vpcs'; import { truncate } from 'src/utilities/truncate'; @@ -39,6 +40,7 @@ const VPCDetail = () => { const { data: vpc, error, isLoading } = useVPCQuery(+vpcId); const { data: regions } = useRegionsQuery(); + const flags = useFlags(); const [editVPCDrawerOpen, setEditVPCDrawerOpen] = React.useState(false); const [deleteVPCDialogOpen, setDeleteVPCDialogOpen] = React.useState(false); @@ -112,7 +114,7 @@ const VPCDetail = () => { labelOptions: { noCap: true }, pathname: `/vpcs/${vpc.label}`, }} - betaFeedbackLink={VPC_FEEDBACK_FORM_URL} + betaFeedbackLink={flags.vpc ? VPC_FEEDBACK_FORM_URL : undefined} // @TODO VPC: remove this once VPC goes into GA docsLabel="Docs" docsLink={VPC_DOCS_LINK} /> diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx index f32fef1af9c..c1f108a6920 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx @@ -18,6 +18,7 @@ import { VPC_FEEDBACK_FORM_URL, VPC_LABEL, } from 'src/features/VPCs/constants'; +import { useFlags } from 'src/hooks/useFlags'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useVPCsQuery } from 'src/queries/vpcs'; @@ -55,6 +56,7 @@ const VPCLanding = () => { ); const history = useHistory(); + const flags = useFlags(); const [selectedVPC, setSelectedVPC] = React.useState(); @@ -96,7 +98,7 @@ const VPCLanding = () => { return ( <> Date: Wed, 17 Jan 2024 18:03:42 -0500 Subject: [PATCH 02/21] Display 'Auto-assign a VPC IPv4 address for this Linode' checkbox and field tied to it only if a linode has been selected --- .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 1e9b3d35fa3..746fc9dc038 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -398,37 +398,41 @@ export const SubnetAssignLinodesDrawer = ( sx={{ marginBottom: '8px' }} value={values.selectedLinode?.id || null} /> - - + + + } + label={ + + Auto-assign a VPC IPv4 address for this Linode + + } + data-testid="vpc-ipv4-checkbox" + disabled={userCannotAssignLinodes} + sx={{ marginRight: 0 }} /> - } - label={ - - Auto-assign a VPC IPv4 address for this Linode - - } - data-testid="vpc-ipv4-checkbox" - disabled={userCannotAssignLinodes} - sx={{ marginRight: 0 }} - /> - - - {!autoAssignIPv4 && ( - { - setFieldValue('chosenIP', e.target.value); - setAssignLinodesErrors({}); - }} - disabled={userCannotAssignLinodes} - errorText={assignLinodesErrors['ipv4.vpc']} - label={'VPC IPv4'} - sx={{ marginBottom: '8px' }} - value={values.chosenIP} - /> + + + {!autoAssignIPv4 && ( + { + setFieldValue('chosenIP', e.target.value); + setAssignLinodesErrors({}); + }} + disabled={userCannotAssignLinodes} + errorText={assignLinodesErrors['ipv4.vpc']} + label={'VPC IPv4'} + sx={{ marginBottom: '8px' }} + value={values.chosenIP} + /> + )} + )} {linodeConfigs.length > 1 && ( <> From 9af0bfb4eba099ed7d38e3906de5a76f11d3c1e0 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 19 Jan 2024 19:29:23 -0500 Subject: [PATCH 03/21] Basic structure & functionality in place for IPv4 range support; removed schema limit on how many IP ranges can be provided; added prop to in LinodeSelect.tsx to prevent 'The value provided to Autocomplete is invalid' console error --- .../MultipleIPInput/MultipleIPInput.tsx | 14 +- .../RemovableSelectionsList.tsx | 169 ++++++++++++++---- .../Linodes/LinodeSelect/LinodeSelect.tsx | 1 + .../VPCs/VPCDetail/AssignIPRanges.tsx | 49 +++++ .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 119 ++++++++---- .../manager/src/features/VPCs/constants.ts | 5 + packages/validation/src/linodes.schema.ts | 2 +- 7 files changed, 289 insertions(+), 70 deletions(-) create mode 100644 packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 9d92323b8e4..886f81c87a7 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -56,9 +56,11 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); interface Props { + buttonText?: string; className?: string; error?: string; forDatabaseAccessControls?: boolean; + forVPCIPv4Ranges?: boolean; helperText?: string; inputProps?: InputBaseProps; ips: ExtendedIP[]; @@ -72,9 +74,11 @@ interface Props { export const MultipleIPInput = React.memo((props: Props) => { const { + buttonText, className, error, forDatabaseAccessControls, + forVPCIPv4Ranges, helperText, ips, onBlur, @@ -177,16 +181,18 @@ export const MultipleIPInput = React.memo((props: Props) => { value={thisIP.address} /> - {/** Don't show the button for the first input since it won't do anything, unless this component is used in DBaaS */} + {/** Don't show the button for the first input since it won't do anything, unless this component is + * used in DBaaS or for Linode VPC interfaces + */} - {idx > 0 || forDatabaseAccessControls ? ( + {(idx > 0 || forDatabaseAccessControls || forVPCIPv4Ranges) && ( - ) : null} + )} ))} @@ -196,7 +202,7 @@ export const MultipleIPInput = React.memo((props: Props) => { compactX onClick={addNewInput} > - Add an IP + {buttonText ?? 'Add an IP'} ); diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 706d4ada54a..52fd40e7f1f 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -2,6 +2,12 @@ import Close from '@mui/icons-material/Close'; import * as React from 'react'; import { IconButton } from 'src/components/IconButton'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { Tooltip } from 'src/components/Tooltip'; import { SelectedOptionsHeader, @@ -56,6 +62,10 @@ export interface RemovableSelectionsListProps { * The data to display in the list */ selectionData: RemovableItem[]; + /** + * Headers for the table containing the list of selected options + */ + tableHeaders?: string[]; } export const RemovableSelectionsList = ( @@ -70,6 +80,7 @@ export const RemovableSelectionsList = ( onRemove, preferredDataLabel, selectionData, + tableHeaders, } = props; // used to determine when to display a box-shadow to indicate scrollability @@ -86,42 +97,106 @@ export const RemovableSelectionsList = ( onRemove(selection); }; + // Used for non-table version + const selectedOptionsJSX = ( + maxHeight} + maxWidth={maxWidth} + > + + + {selectionData.map((selection) => ( + + + {preferredDataLabel + ? selection[preferredDataLabel] + : selection.label} + + {isRemovable && ( + handleOnClick(selection)} + size="medium" + > + + + )} + + ))} + + + + ); + + // Used for table version + const selectedOptionsJSXForTable = ( + <> + {selectionData.map((selection) => ( + + + + {preferredDataLabel + ? selection[preferredDataLabel] + : selection.label} + + + {selection.interfaceData?.ipv4?.vpc ?? null} + + {determineNoneSingleOrMultipleWithChip( + selection.interfaceData?.ip_ranges ?? [] + )} + + + {isRemovable && ( + handleOnClick(selection)} + size="medium" + > + + + )} + + + ))} + + ); + + const tableOfSelectedOptions = ( + + + + {tableHeaders?.map((thisHeader) => ( + + {thisHeader} + + ))} + + + + {selectedOptionsJSXForTable} +
+ ); + return ( <> {headerText} {selectionData.length > 0 ? ( - maxHeight} - maxWidth={maxWidth} - > - - - {selectionData.map((selection) => ( - - - {preferredDataLabel - ? selection[preferredDataLabel] - : selection.label} - - {isRemovable && ( - handleOnClick(selection)} - size="medium" - > - - - )} - - ))} - - - + !tableHeaders || tableHeaders.length === 0 ? ( + selectedOptionsJSX + ) : ( + tableOfSelectedOptions + ) ) : ( {noDataText} @@ -130,3 +205,33 @@ export const RemovableSelectionsList = ( ); }; + +const determineNoneSingleOrMultipleWithChip = ( + dataArray: string[] +): JSX.Element | string => { + if (dataArray.length === 0) { + return 'None'; + } + + if (dataArray.length === 1) { + return dataArray[0]; + } + + const allDataExceptFirstElement = dataArray.slice(1); + + const remainingData = allDataExceptFirstElement.map((datum) => ( + <> + {datum} +
+ + )); + + return ( + <> + {dataArray[0]}{' '} + + +{remainingData.length} + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx index f2a3690ebe4..a7ab20b5610 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx @@ -161,6 +161,7 @@ export const LinodeSelect = ( helperText={helperText} id={id} inputValue={inputValue} + isOptionEqualToValue={(option, value) => option.id === value.id} label={label ? label : multiple ? 'Linodes' : 'Linode'} loading={isLoading || loading} multiple={multiple} diff --git a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx new file mode 100644 index 00000000000..1b8ddc27430 --- /dev/null +++ b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { Divider } from 'src/components/Divider'; +import { Link } from 'src/components/Link'; +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; +import { Notice } from 'src/components/Notice/Notice'; +import { Typography } from 'src/components/Typography'; +import { + ASSIGN_IPV4_RANGES_DESCRIPTION, + ASSIGN_IPV4_RANGES_TITLE, +} from 'src/features/VPCs/constants'; +import { ExtendedIP } from 'src/utilities/ipUtils'; + +interface Props { + handleIPRangeChange: (ips: ExtendedIP[]) => void; + ipRanges: ExtendedIP[]; + ipRangesError?: string; +} + +export const AssignIPRanges = (props: Props) => { + const { handleIPRangeChange, ipRanges, ipRangesError } = props; + + return ( + <> + + {ipRangesError && ( + + )} + ({ fontFamily: theme.font.bold })}> + {ASSIGN_IPV4_RANGES_TITLE} + + + {ASSIGN_IPV4_RANGES_DESCRIPTION}{' '} + + Learn more + + . + + + + ); +}; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 746fc9dc038..5432a529cb8 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -16,6 +16,7 @@ import { RemovableSelectionsList } from 'src/components/RemovableSelectionsList/ import { TextField } from 'src/components/TextField'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; +import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { defaultPublicInterface } from 'src/features/Linodes/LinodesCreate/LinodeCreate'; import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP, @@ -27,6 +28,7 @@ import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { getAllLinodeConfigs } from 'src/queries/linodes/requests'; import { useGrants, useProfile } from 'src/queries/profile'; import { getErrorMap } from 'src/utilities/errorUtils'; +import { ExtendedIP } from 'src/utilities/ipUtils'; import { SUBNET_LINODE_CSV_HEADERS } from 'src/utilities/subnets'; import { @@ -34,19 +36,20 @@ import { MULTIPLE_CONFIGURATIONS_MESSAGE, REGIONAL_LINODE_MESSAGE, } from '../constants'; +import { AssignIPRanges } from './AssignIPRanges'; import { StyledButtonBox } from './SubnetAssignLinodesDrawer.styles'; import type { APIError, Config, + Interface, InterfacePayload, Linode, Subnet, } from '@linode/api-v4'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; // @TODO VPC: if all subnet action menu item related components use (most of) this as their props, might be worth -// putting this in a common file and naming it something like SubnetActionMenuItemProps or somthing +// putting this in a common file and naming it something like SubnetActionMenuItemProps or something interface SubnetAssignLinodesDrawerProps { onClose: () => void; open: boolean; @@ -72,7 +75,7 @@ export const SubnetAssignLinodesDrawer = ( unassignLinodesErrors, } = useUnassignLinode(); const csvRef = React.useRef(); - const newInterfaceId = React.useRef(-1); + const newInterface = React.useRef(); const removedLinodeId = React.useRef(-1); const formattedDate = useFormattedDate(); @@ -145,7 +148,7 @@ export const SubnetAssignLinodesDrawer = ( } const handleAssignLinode = async () => { - const { chosenIP, selectedConfig, selectedLinode } = values; + const { chosenIP, ipRanges, selectedConfig, selectedLinode } = values; const configId = getConfigId(linodeConfigs, selectedConfig); @@ -154,6 +157,7 @@ export const SubnetAssignLinodesDrawer = ( ); const interfacePayload: InterfacePayload = { + ip_ranges: ipRanges.map((ipRange) => ipRange.address), ipam_address: null, ipv4: { nat_1_1: 'any', // 'any' in all cases here to help the user towards a functional configuration & hide complexity per stakeholder feedback @@ -177,7 +181,7 @@ export const SubnetAssignLinodesDrawer = ( ); } - const newInterface = await appendConfigInterface( + const _newInterface = await appendConfigInterface( selectedLinode?.id ?? -1, configId, interfacePayload @@ -186,7 +190,7 @@ export const SubnetAssignLinodesDrawer = ( // We're storing this in a ref to access this later in order // to update `assignedLinodesAndConfigData` with the new // interfaceId without causing a re-render - newInterfaceId.current = newInterface.id; + newInterface.current = _newInterface; await invalidateQueries({ configId, @@ -195,7 +199,32 @@ export const SubnetAssignLinodesDrawer = ( vpcId, }); } catch (errors) { - const errorMap = getErrorMap(['ipv4.vpc'], errors); + const fieldsOfIPRangesErrors = errors.reduce( + (accum: any, _err: { field: string }) => { + if (_err.field && _err.field.includes('ip_ranges[')) { + return [...accum, _err.field]; + } else { + return [...accum]; + } + }, + [] + ); + + const errorMap = getErrorMap( + [...fieldsOfIPRangesErrors, 'ipv4.vpc', 'ip_ranges'], + errors + ); + + const ipRangesWithErrors = ipRanges.map((ipRange, idx) => { + const errorForThisIdx = errorMap[`ip_ranges[${idx}]`]; + return { + address: ipRange.address, + error: errorForThisIdx, + }; + }); + + setFieldValue('ipRanges', ipRangesWithErrors); + const errorMessage = determineErrorMessage(configId, errorMap); setAssignLinodesErrors({ ...errorMap, none: errorMessage }); @@ -244,6 +273,7 @@ export const SubnetAssignLinodesDrawer = ( enableReinitialize: true, initialValues: { chosenIP: '', + ipRanges: [] as ExtendedIP[], selectedConfig: null as Config | null, selectedLinode: null as Linode | null, }, @@ -252,6 +282,13 @@ export const SubnetAssignLinodesDrawer = ( validateOnChange: false, }); + const handleIPRangeChange = React.useCallback( + (_ipRanges: ExtendedIP[]) => { + setFieldValue('ipRanges', _ipRanges); + }, + [setFieldValue] + ); + React.useEffect(() => { // Return early if no Linode is selected if (!values.selectedLinode) { @@ -270,7 +307,8 @@ export const SubnetAssignLinodesDrawer = ( const newLinodeData = { ...values.selectedLinode, configId, - interfaceId: newInterfaceId.current, + interfaceData: newInterface?.current, + interfaceId: newInterface?.current?.id ?? -1, // Create a label that combines Linode label and configuration label (if available) linodeConfigLabel: `${values.selectedLinode.label}${ values.selectedConfig?.label @@ -290,6 +328,7 @@ export const SubnetAssignLinodesDrawer = ( setLinodeConfigs([]); setValues({ chosenIP: '', + ipRanges: [], selectedConfig: null, selectedLinode: null, }); @@ -297,6 +336,7 @@ export const SubnetAssignLinodesDrawer = ( }, [ subnet, assignedLinodesAndConfigData, + values.ipRanges, values.selectedLinode, values.selectedConfig, linodeConfigs, @@ -391,10 +431,10 @@ export const SubnetAssignLinodesDrawer = ( setAssignLinodesErrors({}); }} disabled={userCannotAssignLinodes} - label={'Linodes'} + label="Linode" // We only want to be able to assign linodes that were not already assigned to this subnet options={linodeOptionsToAssign} - placeholder="Select Linodes or type to search" + placeholder="Select Linode or type to search" sx={{ marginBottom: '8px' }} value={values.selectedLinode?.id || null} /> @@ -427,33 +467,45 @@ export const SubnetAssignLinodesDrawer = ( }} disabled={userCannotAssignLinodes} errorText={assignLinodesErrors['ipv4.vpc']} - label={'VPC IPv4'} + label="VPC IPv4" sx={{ marginBottom: '8px' }} value={values.chosenIP} /> )} - - )} - {linodeConfigs.length > 1 && ( - <> - - {MULTIPLE_CONFIGURATIONS_MESSAGE}{' '} - - Learn more - - . - - { - setFieldValue('selectedConfig', value); - setAssignLinodesErrors({}); - }} - disabled={userCannotAssignLinodes} - label={'Configuration profile'} - options={linodeConfigs} - placeholder="Select a configuration profile" - value={values.selectedConfig || null} - /> + {linodeConfigs.length > 1 && ( + <> + + {MULTIPLE_CONFIGURATIONS_MESSAGE}{' '} + + Learn more + + . + + { + setFieldValue('selectedConfig', value); + setAssignLinodesErrors({}); + }} + disabled={userCannotAssignLinodes} + label={'Configuration profile'} + options={linodeConfigs} + placeholder="Select a configuration profile" + value={values.selectedConfig || null} + /> + + )} + {/* Display the 'Assign additional IPv4 ranges' section if + the Configuration Profile section has been populated, or + if it doesn't display b/c the linode has a single config + */} + {((linodeConfigs.length > 1 && values.selectedConfig) || + linodeConfigs.length === 1) && ( + + )} )} @@ -490,6 +542,7 @@ export const SubnetAssignLinodesDrawer = ( noDataText={'No Linodes have been assigned.'} preferredDataLabel="linodeConfigLabel" selectionData={assignedLinodesAndConfigData} + tableHeaders={['Linode', 'VPC IPv4', 'VPC IPv4 Ranges']} /> {assignedLinodesAndConfigData.length > 0 && ( Date: Mon, 22 Jan 2024 16:57:30 -0500 Subject: [PATCH 04/21] Filter empty strings out from IP Ranges payload; update IPv4 Range input placeholder and button text per updated mockups --- .../manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx | 4 ++-- .../src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx index 1b8ddc27430..58161784412 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx @@ -37,11 +37,11 @@ export const AssignIPRanges = (props: Props) => { . diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 5432a529cb8..f62dfce603b 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -157,7 +157,9 @@ export const SubnetAssignLinodesDrawer = ( ); const interfacePayload: InterfacePayload = { - ip_ranges: ipRanges.map((ipRange) => ipRange.address), + ip_ranges: ipRanges + .map((ipRange) => ipRange.address) + .filter((ipRange) => ipRange !== ''), ipam_address: null, ipv4: { nat_1_1: 'any', // 'any' in all cases here to help the user towards a functional configuration & hide complexity per stakeholder feedback From 088a1e4f3a4d2f1285c8f0b561cdca75370e8e9a Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 22 Jan 2024 18:20:35 -0500 Subject: [PATCH 05/21] Use for '+X' piece --- .../RemovableSelectionsList/RemovableSelectionsList.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 52fd40e7f1f..82756ba5f1e 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -18,6 +18,7 @@ import { StyledNoAssignedLinodesBox, StyledScrollBox, } from './RemovableSelectionsList.style'; +import { Chip } from '../Chip'; export type RemovableItem = { id: number; @@ -227,11 +228,11 @@ const determineNoneSingleOrMultipleWithChip = ( )); return ( - <> + {dataArray[0]}{' '} - +{remainingData.length} + - + ); }; From 0b0e5bb3d0f018fcc850eb217193e7c37eace20a Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 22 Jan 2024 18:21:47 -0500 Subject: [PATCH 06/21] Use for '+X' piece pt 2 --- .../RemovableSelectionsList.style.ts | 7 +++++++ .../RemovableSelectionsList/RemovableSelectionsList.tsx | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.style.ts b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.style.ts index 392807ae9d1..c49bf85f512 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.style.ts +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.style.ts @@ -88,3 +88,10 @@ export const StyledScrollBox = styled(Box, { maxWidth: `${maxWidth}px`, overflow: 'auto', })); + +export const StyledItemWithPlusChip = styled('span', { + label: 'ItemWithPlusChip', +})({ + alignItems: 'center', + display: 'inline-flex', +}); diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 82756ba5f1e..4f43aca306f 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -1,6 +1,7 @@ import Close from '@mui/icons-material/Close'; import * as React from 'react'; +import { Chip } from 'src/components/Chip'; import { IconButton } from 'src/components/IconButton'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; @@ -14,11 +15,11 @@ import { SelectedOptionsList, SelectedOptionsListItem, StyledBoxShadowWrapper, + StyledItemWithPlusChip, StyledLabel, StyledNoAssignedLinodesBox, StyledScrollBox, } from './RemovableSelectionsList.style'; -import { Chip } from '../Chip'; export type RemovableItem = { id: number; @@ -228,11 +229,11 @@ const determineNoneSingleOrMultipleWithChip = ( )); return ( - + {dataArray[0]}{' '} - + ); }; From 78ca148d02b55412ad821e864b32e8204a224f4a Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Tue, 23 Jan 2024 15:18:29 -0500 Subject: [PATCH 07/21] Utility file and tests for chip function; spacing adjustments for ; temp skip in SubnetAssignLinodesDrawer.test.tsx and remove some assertions given changed behavior --- .../RemovableSelectionsList.tsx | 34 +--------- .../VPCs/VPCDetail/AssignIPRanges.tsx | 7 +- .../SubnetAssignLinodesDrawer.test.tsx | 65 +++++++++++++------ .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 11 +++- .../noneSingleOrMultipleWithChip.test.tsx | 22 +++++++ .../noneSingleOrMultipleWithChip.tsx | 42 ++++++++++++ 6 files changed, 126 insertions(+), 55 deletions(-) create mode 100644 packages/manager/src/utilities/noneSingleOrMultipleWithChip.test.tsx create mode 100644 packages/manager/src/utilities/noneSingleOrMultipleWithChip.tsx diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 4f43aca306f..3ed3311ef88 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -1,21 +1,19 @@ import Close from '@mui/icons-material/Close'; import * as React from 'react'; -import { Chip } from 'src/components/Chip'; import { IconButton } from 'src/components/IconButton'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; -import { Tooltip } from 'src/components/Tooltip'; +import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; import { SelectedOptionsHeader, SelectedOptionsList, SelectedOptionsListItem, StyledBoxShadowWrapper, - StyledItemWithPlusChip, StyledLabel, StyledNoAssignedLinodesBox, StyledScrollBox, @@ -207,33 +205,3 @@ export const RemovableSelectionsList = ( ); }; - -const determineNoneSingleOrMultipleWithChip = ( - dataArray: string[] -): JSX.Element | string => { - if (dataArray.length === 0) { - return 'None'; - } - - if (dataArray.length === 1) { - return dataArray[0]; - } - - const allDataExceptFirstElement = dataArray.slice(1); - - const remainingData = allDataExceptFirstElement.map((datum) => ( - <> - {datum} -
- - )); - - return ( - - {dataArray[0]}{' '} - - - - - ); -}; diff --git a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx index 58161784412..a9035bbb631 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx @@ -11,18 +11,21 @@ import { } from 'src/features/VPCs/constants'; import { ExtendedIP } from 'src/utilities/ipUtils'; +import type { SxProps } from '@mui/material/styles'; + interface Props { handleIPRangeChange: (ips: ExtendedIP[]) => void; ipRanges: ExtendedIP[]; ipRangesError?: string; + sx?: SxProps; } export const AssignIPRanges = (props: Props) => { - const { handleIPRangeChange, ipRanges, ipRangesError } = props; + const { handleIPRangeChange, ipRanges, ipRangesError, sx } = props; return ( <> - + {ipRangesError && ( )} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx index 2250290b5df..e3724d60fc5 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx @@ -1,11 +1,22 @@ import { Subnet } from '@linode/api-v4'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; +import { QueryClient } from 'react-query'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { linodeFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { rest, server } from 'src/mocks/testServer'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import { SubnetAssignLinodesDrawer } from './SubnetAssignLinodesDrawer'; +const queryClient = new QueryClient(); + +beforeAll(() => mockMatchMedia()); +afterEach(() => { + queryClient.clear(); +}); + const props = { onClose: vi.fn(), open: true, @@ -15,13 +26,27 @@ const props = { label: 'subnet-1', } as Subnet, vpcId: 1, - vpcRegion: '', + vpcRegion: 'us-east', }; describe('Subnet Assign Linodes Drawer', () => { + const linode = linodeFactory.build({ + label: 'this-linode', + region: props.vpcRegion, + }); + + server.use( + rest.get('*/linode/instances', (req, res, ctx) => { + return res(ctx.json(makeResourcePage([linode]))); + }) + ); + it('should render a subnet assign linodes drawer', () => { - const { getByText, queryByText } = renderWithTheme( - + const { getByText } = renderWithTheme( + , + { + queryClient, + } ); const header = getByText( @@ -36,14 +61,9 @@ describe('Subnet Assign Linodes Drawer', () => { `Select the Linodes you would like to assign to this subnet. Only Linodes in this VPC's region are displayed.` ); expect(helperText).toBeVisible(); - const linodeSelect = getByText('Linodes'); + const linodeSelect = getByText('Linode'); expect(linodeSelect).toBeVisible(); - const checkbox = getByText( - 'Auto-assign a VPC IPv4 address for this Linode' - ); - expect(checkbox).toBeVisible(); - const ipv4Textbox = queryByText('VPC IPv4'); - expect(ipv4Textbox).toBeNull(); + const assignButton = getByText('Assign Linode'); expect(assignButton).toBeVisible(); const alreadyAssigned = getByText('Linodes Assigned to Subnet (0)'); @@ -52,19 +72,26 @@ describe('Subnet Assign Linodes Drawer', () => { expect(doneButton).toBeVisible(); }); - it('should show the IPv4 textbox when the checkmark is clicked', () => { - const { getByText } = renderWithTheme( - + it.skip('should show the IPv4 textbox when the checkmark is clicked', async () => { + const { findByText, getByLabelText } = renderWithTheme( + , + { + queryClient, + } ); - const checkbox = getByText( + const selectField = getByLabelText('Linode'); + fireEvent.change(selectField, { target: { value: 'this-linode' } }); + + const checkbox = await findByText( 'Auto-assign a VPC IPv4 address for this Linode' ); - expect(checkbox).toBeVisible(); + + await waitFor(() => expect(checkbox).toBeVisible()); fireEvent.click(checkbox); - const ipv4Textbox = getByText('VPC IPv4'); - expect(ipv4Textbox).toBeVisible(); + const ipv4Textbox = await findByText('VPC IPv4'); + await waitFor(() => expect(ipv4Textbox).toBeVisible()); }); it('should close the drawer', () => { diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index f62dfce603b..716ced33ff1 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -1,4 +1,5 @@ import { appendConfigInterface } from '@linode/api-v4'; +import { useTheme } from '@mui/material/styles'; import { useFormik } from 'formik'; import * as React from 'react'; @@ -78,6 +79,7 @@ export const SubnetAssignLinodesDrawer = ( const newInterface = React.useRef(); const removedLinodeId = React.useRef(-1); const formattedDate = useFormattedDate(); + const theme = useTheme(); const [assignLinodesErrors, setAssignLinodesErrors] = React.useState< Record @@ -131,7 +133,7 @@ export const SubnetAssignLinodesDrawer = ( // Moved the list of linodes that are currently assignable to a subnet into a state variable (linodeOptionsToAssign) // and update that list whenever this subnet or the list of all linodes in this subnet's region changes. This takes - // care of the MUI invalid value warning that was occuring before in the Linodes autocomplete [M3-6752] + // care of the MUI invalid value warning that was occurring before in the Linodes autocomplete [M3-6752] React.useEffect(() => { if (linodes) { setLinodeOptionsToAssign(findUnassignedLinodes() ?? []); @@ -503,6 +505,13 @@ export const SubnetAssignLinodesDrawer = ( {((linodeConfigs.length > 1 && values.selectedConfig) || linodeConfigs.length === 1) && ( 1 + ? theme.spacing(2) + : theme.spacing(), + }} handleIPRangeChange={handleIPRangeChange} ipRanges={values.ipRanges} ipRangesError={assignLinodesErrors['ip_ranges']} diff --git a/packages/manager/src/utilities/noneSingleOrMultipleWithChip.test.tsx b/packages/manager/src/utilities/noneSingleOrMultipleWithChip.test.tsx new file mode 100644 index 00000000000..91894093214 --- /dev/null +++ b/packages/manager/src/utilities/noneSingleOrMultipleWithChip.test.tsx @@ -0,0 +1,22 @@ +import { determineNoneSingleOrMultipleWithChip } from './noneSingleOrMultipleWithChip'; + +describe('determineNoneSingleOrMultipleWithChip', () => { + it('should return None for empty arrays', () => { + expect(determineNoneSingleOrMultipleWithChip([])).toEqual('None'); + }); + + it('should return the element if the array only consists of one element', () => { + const array = ['Test']; + + expect(determineNoneSingleOrMultipleWithChip(array)).toEqual(array[0]); + }); + + it('should not return "None" nor equal the first element of the array if the array contains multiple elements', () => { + const array = ['Test', 'Test 2', 'Test 3', 'Test 4']; + + const returned = determineNoneSingleOrMultipleWithChip(array); + + expect(returned).not.toEqual('None'); + expect(returned).not.toEqual('Test'); + }); +}); diff --git a/packages/manager/src/utilities/noneSingleOrMultipleWithChip.tsx b/packages/manager/src/utilities/noneSingleOrMultipleWithChip.tsx new file mode 100644 index 00000000000..67b58fb2dcc --- /dev/null +++ b/packages/manager/src/utilities/noneSingleOrMultipleWithChip.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import { Chip } from 'src/components/Chip'; +import { StyledItemWithPlusChip } from 'src/components/RemovableSelectionsList/RemovableSelectionsList.style'; +import { Tooltip } from 'src/components/Tooltip'; + +export const remainingDataLengthChip = 'remaining-data-length-chip'; + +export const determineNoneSingleOrMultipleWithChip = ( + dataArray: string[] +): JSX.Element | string => { + if (dataArray.length === 0) { + return 'None'; + } + + if (dataArray.length === 1) { + return dataArray[0]; + } + + const allDataExceptFirstElement = dataArray.slice(1); + + const remainingData = allDataExceptFirstElement.map((datum) => ( + <> + {datum} +
+ + )); + + return ( + + {dataArray[0]}{' '} + + + + + ); +}; From c777aa54a4b40ed5cb9158ae08edc25a94cd406c Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Tue, 23 Jan 2024 16:33:06 -0500 Subject: [PATCH 08/21] Use to get desired styling for 'Add IPv4 Range' button --- .../MultipleIPInput/MultipleIPInput.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 886f81c87a7..3f9b0446a09 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -7,7 +7,9 @@ import { makeStyles } from 'tss-react/mui'; import { Button } from 'src/components/Button/Button'; import { InputLabel } from 'src/components/InputLabel'; +import { LinkButton } from 'src/components/LinkButton'; import { Notice } from 'src/components/Notice/Notice'; +import { StyledLinkButtonBox } from 'src/components/SelectFirewallPanel/SelectFirewallPanel'; import { TextField } from 'src/components/TextField'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; @@ -126,6 +128,21 @@ export const MultipleIPInput = React.memo((props: Props) => { return null; } + const addIPButton = forVPCIPv4Ranges ? ( + + {buttonText} + + ) : ( + + ); + return (
{tooltip ? ( @@ -196,14 +213,7 @@ export const MultipleIPInput = React.memo((props: Props) => { ))} - + {addIPButton}
); }); From ff18de7baf0e91fec79f666ae1a7364a73866abb Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Tue, 23 Jan 2024 17:24:01 -0500 Subject: [PATCH 09/21] desired RemovableSelectionsList UI (refactor forthcoming) --- .../RemovableSelectionsList.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 3ed3311ef88..ebb3d48a2cd 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -9,6 +9,7 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; +import { TableRowEmpty } from '../TableRowEmpty/TableRowEmpty'; import { SelectedOptionsHeader, SelectedOptionsList, @@ -188,19 +189,37 @@ export const RemovableSelectionsList = ( ); + const tableWithoutData = ( + + + + {tableHeaders?.map((thisHeader) => ( + + {thisHeader} + + ))} + + + + +
+ ); + return ( <> {headerText} {selectionData.length > 0 ? ( - !tableHeaders || tableHeaders.length === 0 ? ( + !tableHeaders ? ( selectedOptionsJSX ) : ( tableOfSelectedOptions ) - ) : ( + ) : !tableHeaders ? ( {noDataText} + ) : ( + tableWithoutData )} ); From 493b4f4f039f6be97459dd31bcd3546d38e7bc93 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Wed, 24 Jan 2024 13:11:07 -0500 Subject: [PATCH 10/21] RemovableSelectionsListTable.tsx --- .../RemovableSelectionsListTable.tsx | 133 ++++++++++++++++++ .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 4 +- 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsListTable.tsx diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsListTable.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsListTable.tsx new file mode 100644 index 00000000000..afb213e5801 --- /dev/null +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsListTable.tsx @@ -0,0 +1,133 @@ +import Close from '@mui/icons-material/Close'; +import * as React from 'react'; + +import { IconButton } from 'src/components/IconButton'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; +import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; + +import { TableRowEmpty } from '../TableRowEmpty/TableRowEmpty'; +import { + SelectedOptionsHeader, + StyledLabel, +} from './RemovableSelectionsList.style'; + +export type RemovableItem = { + id: number; + label: string; + // The remaining key-value pairs must have their values typed + // as 'any' because we do not know what types they could be. + // Trying to type them as 'unknown' led to type errors. +} & { [key: string]: any }; + +export interface RemovableSelectionsListTableProps { + /** + * The descriptive text to display above the list + */ + headerText: string; + /** + * If false, hide the remove button + */ + isRemovable?: boolean; + /** + * The text to display if there is no data + */ + noDataText: string; + /** + * The action to perform when a data item is clicked + */ + onRemove: (data: RemovableItem) => void; + /** + * Assumes the passed in prop is a key within the selectionData, and that the + * value of this key is a string. + * Displays the value of this key as the label of the data item, rather than data.label + */ + preferredDataLabel?: string; + /** + * The data to display in the list + */ + selectionData: RemovableItem[]; + /** + * Headers for the table containing the list of selected options + */ + tableHeaders: string[]; +} + +export const RemovableSelectionsListTable = ( + props: RemovableSelectionsListTableProps +) => { + const { + headerText, + isRemovable = true, + noDataText, + onRemove, + preferredDataLabel, + selectionData, + tableHeaders, + } = props; + + const handleOnClick = (selection: RemovableItem) => { + onRemove(selection); + }; + + const selectedOptionsJSX = + selectionData.length === 0 ? ( + + ) : ( + selectionData.map((selection) => ( + + + + {preferredDataLabel + ? selection[preferredDataLabel] + : selection.label} + + + {selection.interfaceData?.ipv4?.vpc ?? null} + + {determineNoneSingleOrMultipleWithChip( + selection.interfaceData?.ip_ranges ?? [] + )} + + + {isRemovable && ( + handleOnClick(selection)} + size="medium" + > + + + )} + + + )) + ); + + return ( + <> + {headerText} + + + + {tableHeaders.map((thisHeader) => ( + + {thisHeader} + + ))} + + + + {selectedOptionsJSX} +
+ + ); +}; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 716ced33ff1..b42632dfe63 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -13,7 +13,7 @@ import { FormControlLabel } from 'src/components/FormControlLabel'; import { FormHelperText } from 'src/components/FormHelperText'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; -import { RemovableSelectionsList } from 'src/components/RemovableSelectionsList/RemovableSelectionsList'; +import { RemovableSelectionsListTable } from 'src/components/RemovableSelectionsList/RemovableSelectionsListTable'; import { TextField } from 'src/components/TextField'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; @@ -544,7 +544,7 @@ export const SubnetAssignLinodesDrawer = ( /> )) : null} - { handleUnassignLinode(data as LinodeAndConfigData); setUnassignLinodesErrors([]); From 8019eecc1e3db855d2e3366cd85f38cb9d2c4afe Mon Sep 17 00:00:00 2001 From: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:36:31 -0500 Subject: [PATCH 11/21] =?UTF-8?q?refactor:=20[M3-7672]=20=E2=80=93=20Use?= =?UTF-8?q?=20flags.vpc=20to=20determine=20if=20beta=20chip=20gets=20displ?= =?UTF-8?q?ayed=20in=20PrimaryNav=20(#10090)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/manager/src/components/PrimaryNav/PrimaryNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 354c427f634..4382a4ad74c 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -195,7 +195,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { hide: !showVPCs, href: '/vpcs', icon: , - isBeta: true, + isBeta: flags.vpc, // @TODO VPC: after VPC enters GA, remove this property entirely }, { display: 'Firewalls', From 1c06c09c48f0752036a636e630bcdef82e8f40ff Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Wed, 24 Jan 2024 16:55:21 -0500 Subject: [PATCH 12/21] Minor refactor in SubnetAssignLinodesDrawer.tsx surrounding interfaceId; unit test adjustment --- .../VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx | 4 ++-- .../VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx index e3724d60fc5..e4174419378 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx @@ -42,7 +42,7 @@ describe('Subnet Assign Linodes Drawer', () => { ); it('should render a subnet assign linodes drawer', () => { - const { getByText } = renderWithTheme( + const { getByText, queryAllByText } = renderWithTheme( , { queryClient, @@ -61,7 +61,7 @@ describe('Subnet Assign Linodes Drawer', () => { `Select the Linodes you would like to assign to this subnet. Only Linodes in this VPC's region are displayed.` ); expect(helperText).toBeVisible(); - const linodeSelect = getByText('Linode'); + const linodeSelect = queryAllByText('Linode')[0]; expect(linodeSelect).toBeVisible(); const assignButton = getByText('Assign Linode'); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index b42632dfe63..be665f73bc0 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -61,7 +61,7 @@ interface SubnetAssignLinodesDrawerProps { type LinodeAndConfigData = Linode & { configId: number; - interfaceId: number; + interfaceData: Interface | undefined; linodeConfigLabel: string; }; @@ -193,7 +193,7 @@ export const SubnetAssignLinodesDrawer = ( // We're storing this in a ref to access this later in order // to update `assignedLinodesAndConfigData` with the new - // interfaceId without causing a re-render + // interface data without causing a re-render newInterface.current = _newInterface; await invalidateQueries({ @@ -236,12 +236,12 @@ export const SubnetAssignLinodesDrawer = ( }; const handleUnassignLinode = async (data: LinodeAndConfigData) => { - const { configId, id: linodeId, interfaceId } = data; + const { configId, id: linodeId, interfaceData } = data; removedLinodeId.current = linodeId; try { await unassignLinode({ configId, - interfaceId, + interfaceId: interfaceData?.id ?? -1, linodeId, subnetId: subnet?.id ?? -1, vpcId, @@ -312,7 +312,6 @@ export const SubnetAssignLinodesDrawer = ( ...values.selectedLinode, configId, interfaceData: newInterface?.current, - interfaceId: newInterface?.current?.id ?? -1, // Create a label that combines Linode label and configuration label (if available) linodeConfigLabel: `${values.selectedLinode.label}${ values.selectedConfig?.label From 814a48747c92a7b5bde6ffd581f78df3ba76f228 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Wed, 24 Jan 2024 17:26:13 -0500 Subject: [PATCH 13/21] Changeset added for Cloud changes --- packages/manager/.changeset/pr-10089-added-1706135120889.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10089-added-1706135120889.md diff --git a/packages/manager/.changeset/pr-10089-added-1706135120889.md b/packages/manager/.changeset/pr-10089-added-1706135120889.md new file mode 100644 index 00000000000..308323c4d71 --- /dev/null +++ b/packages/manager/.changeset/pr-10089-added-1706135120889.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Support for IPv4 Ranges in VPC 'Assign Linodes to subnet' drawer ([#10089](https://github.com/linode/manager/pull/10089)) From 6dd87a18a80486078d093e6a718667de3fa7a0c6 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Wed, 24 Jan 2024 17:30:47 -0500 Subject: [PATCH 14/21] Added changeset: ip_ranges field in LinodeInterfaceSchema no longer limited to 1 element --- .../validation/.changeset/pr-10089-changed-1706135447361.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/validation/.changeset/pr-10089-changed-1706135447361.md diff --git a/packages/validation/.changeset/pr-10089-changed-1706135447361.md b/packages/validation/.changeset/pr-10089-changed-1706135447361.md new file mode 100644 index 00000000000..1ac75811631 --- /dev/null +++ b/packages/validation/.changeset/pr-10089-changed-1706135447361.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +ip_ranges field in LinodeInterfaceSchema no longer limited to 1 element ([#10089](https://github.com/linode/manager/pull/10089)) From b97c083eeebba3acd10b01a9218d093a2926b6b8 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 26 Jan 2024 10:43:45 -0500 Subject: [PATCH 15/21] Undo changes to RemovableSelectionsList.tsx --- .../RemovableSelectionsList.tsx | 160 ++++-------------- 1 file changed, 33 insertions(+), 127 deletions(-) diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index ebb3d48a2cd..706d4ada54a 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -2,14 +2,7 @@ import Close from '@mui/icons-material/Close'; import * as React from 'react'; import { IconButton } from 'src/components/IconButton'; -import { Table } from 'src/components/Table'; -import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; -import { TableHead } from 'src/components/TableHead'; -import { TableRow } from 'src/components/TableRow'; -import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; -import { TableRowEmpty } from '../TableRowEmpty/TableRowEmpty'; import { SelectedOptionsHeader, SelectedOptionsList, @@ -63,10 +56,6 @@ export interface RemovableSelectionsListProps { * The data to display in the list */ selectionData: RemovableItem[]; - /** - * Headers for the table containing the list of selected options - */ - tableHeaders?: string[]; } export const RemovableSelectionsList = ( @@ -81,7 +70,6 @@ export const RemovableSelectionsList = ( onRemove, preferredDataLabel, selectionData, - tableHeaders, } = props; // used to determine when to display a box-shadow to indicate scrollability @@ -98,128 +86,46 @@ export const RemovableSelectionsList = ( onRemove(selection); }; - // Used for non-table version - const selectedOptionsJSX = ( - maxHeight} - maxWidth={maxWidth} - > - - - {selectionData.map((selection) => ( - - - {preferredDataLabel - ? selection[preferredDataLabel] - : selection.label} - - {isRemovable && ( - handleOnClick(selection)} - size="medium" - > - - - )} - - ))} - - - - ); - - // Used for table version - const selectedOptionsJSXForTable = ( - <> - {selectionData.map((selection) => ( - - - - {preferredDataLabel - ? selection[preferredDataLabel] - : selection.label} - - - {selection.interfaceData?.ipv4?.vpc ?? null} - - {determineNoneSingleOrMultipleWithChip( - selection.interfaceData?.ip_ranges ?? [] - )} - - - {isRemovable && ( - handleOnClick(selection)} - size="medium" - > - - - )} - - - ))} - - ); - - const tableOfSelectedOptions = ( - - - - {tableHeaders?.map((thisHeader) => ( - - {thisHeader} - - ))} - - - - {selectedOptionsJSXForTable} -
- ); - - const tableWithoutData = ( - - - - {tableHeaders?.map((thisHeader) => ( - - {thisHeader} - - ))} - - - - -
- ); - return ( <> {headerText} {selectionData.length > 0 ? ( - !tableHeaders ? ( - selectedOptionsJSX - ) : ( - tableOfSelectedOptions - ) - ) : !tableHeaders ? ( + maxHeight} + maxWidth={maxWidth} + > + + + {selectionData.map((selection) => ( + + + {preferredDataLabel + ? selection[preferredDataLabel] + : selection.label} + + {isRemovable && ( + handleOnClick(selection)} + size="medium" + > + + + )} + + ))} + + + + ) : ( {noDataText} - ) : ( - tableWithoutData )} ); From 2d364ae8492a2308f504cc1a519c152251f63aba Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 25 Jan 2024 22:12:12 -0500 Subject: [PATCH 16/21] functional VPC IPv4 ranges support in Linode Create flow --- .../MultipleIPInput/MultipleIPInput.tsx | 1 + .../Linodes/LinodesCreate/LinodeCreate.tsx | 8 + .../LinodesCreate/LinodeCreateContainer.tsx | 10 +- .../Linodes/LinodesCreate/VPCPanel.test.tsx | 2 + .../Linodes/LinodesCreate/VPCPanel.tsx | 182 ++++++++++-------- .../LinodeSettings/InterfaceSelect.tsx | 2 + 6 files changed, 125 insertions(+), 80 deletions(-) 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..edc2724f26c 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(), 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} From 28d5c2d69d878f1e5c04287542f28bcd91531094 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 25 Jan 2024 22:22:47 -0500 Subject: [PATCH 17/21] add spacing between 'Assign additional IPv4 ranges' title and the description (fixes it across the various views it appears in) --- .../manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx index a9035bbb631..4976b510403 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/AssignIPRanges.tsx @@ -29,7 +29,12 @@ export const AssignIPRanges = (props: Props) => { {ipRangesError && ( )} - ({ fontFamily: theme.font.bold })}> + ({ + fontFamily: theme.font.bold, + paddingBottom: theme.spacing(), + })} + > {ASSIGN_IPV4_RANGES_TITLE} From 4ae02341bb74cb44ed67efa4bf28da4471764412 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 25 Jan 2024 23:04:13 -0500 Subject: [PATCH 18/21] add and populate 'VPC IPv4 Ranges' column to inner Subnet table --- .../VPCs/VPCDetail/SubnetLinodeRow.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx index 43977566633..6431c8d3808 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,31 @@ const getIPv4Link = (configInterface: Interface | undefined): JSX.Element => { ); }; +// determineNoneSingleOrMultipleWithChip +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 +361,9 @@ export const SubnetLinodeTableRowHead = ( VPC IPv4 + + VPC IPv4 Ranges + Firewalls From 3c8432af2d4dd653774807e69e38b3d27b780b54 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 26 Jan 2024 13:17:48 -0500 Subject: [PATCH 19/21] Added changeset: Support for VPC IPv4 Ranges in Linode Create flow and 'VPC IPv4 Ranges' column to inner Subnets table on VPC Detail page --- packages/manager/.changeset/pr-10116-added-1706293067938.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10116-added-1706293067938.md 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)) From 79916f108492bd44598060b04c96e44e7484d495 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Sun, 28 Jan 2024 12:42:45 -0500 Subject: [PATCH 20/21] Fix broken tests --- .../src/features/Linodes/LinodesCreate/VPCPanel.test.tsx | 9 ++++++++- .../src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx index edc2724f26c..e5018a0679e 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.test.tsx @@ -107,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) => { @@ -236,6 +241,7 @@ describe('VPCPanel', () => { ...props, autoassignIPv4WithinVPC: false, region: 'us-east', + selectedSubnetId: 2, selectedVPCId: 5, vpcIPv4AddressOfLinode: '10.0.4.3', }; @@ -271,6 +277,7 @@ describe('VPCPanel', () => { ...props, assignPublicIPv4Address: true, region: 'us-east', + selectedSubnetId: 2, selectedVPCId: 5, }; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx index 6431c8d3808..be9474d2da9 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx @@ -305,7 +305,6 @@ const getIPv4Link = (configInterface: Interface | undefined): JSX.Element => { ); }; -// determineNoneSingleOrMultipleWithChip const getIPRangesCellContents = ( configs: Config[], loading: boolean, From a8791c6a98bd06e3706c6a8c4d706bb55c11d6e8 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 29 Jan 2024 10:51:55 -0500 Subject: [PATCH 21/21] fix unit test --- .../features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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();