Skip to content

Commit c22c202

Browse files
authored
upcoming: [M3-9863] - Add Subnet IPv6 Prefix Length to VPC create page (#12563)
## Description 📝 Add ability to specify a Subnet IPv6 Prefix (`/52 - /62`) in the VPC create page if the selected VPC is Dual Stack ## Changes 🔄 - Add IPv6 support to `MultipleSubnetInput` and `SubnetNode` - Refactoring of shared code into a `useVPCDualStack` hook - Update `createVPCSchema` to support IPv6 subnets ### Verification steps (How to verify changes) - [ ] Ensure the VPC IPv6 feature flag is enabled and your account has the VPC Dual Stack account capability - [ ] Go to the VPC Create page and select Dual Stack - [ ] You should see a `IPv6 Prefix Length` option in the Subnets section with `/56` selected as the default - [ ] You should see the helper text `Number of Linodes` instead of a `Number of Available IP Addresses` helper text - [ ] Test adding/removing multiple subnets and switching from IPv4 and DualStack - [ ] Click Create VPC. You should see `ipv6: [{range: "/X"}]` in the Network Payload under subnets and the auto-allocated IPv6 address with the prefix length from the API response - [ ] There should be no regressions with VPC IPv4 ``` pnpm test SubnetNode ```
1 parent 6f20b24 commit c22c202

File tree

10 files changed

+319
-164
lines changed

10 files changed

+319
-164
lines changed

packages/api-v4/src/vpcs/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
interface VPCIPv6 {
1+
export interface VPCIPv6 {
22
range?: string;
33
}
44

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add subnet IPv6 to VPC create page ([#12563](https://github.com/linode/manager/pull/12563))

packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx

Lines changed: 124 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useAccount } from '@linode/queries';
21
import { useIsGeckoEnabled } from '@linode/shared';
32
import {
43
Box,
@@ -11,13 +10,15 @@ import {
1110
Typography,
1211
} from '@linode/ui';
1312
import { Radio, RadioGroup } from '@linode/ui';
14-
import {
15-
getQueryParamsFromQueryString,
16-
isFeatureEnabledV2,
17-
} from '@linode/utilities';
13+
import { getQueryParamsFromQueryString } from '@linode/utilities';
1814
import Grid from '@mui/material/Grid';
1915
import * as React from 'react';
20-
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
16+
import {
17+
Controller,
18+
useFieldArray,
19+
useFormContext,
20+
useWatch,
21+
} from 'react-hook-form';
2122
// eslint-disable-next-line no-restricted-imports
2223
import { useLocation } from 'react-router-dom';
2324

@@ -27,6 +28,7 @@ import { Link } from 'src/components/Link';
2728
import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
2829
import { SelectionCard } from 'src/components/SelectionCard/SelectionCard';
2930
import { useFlags } from 'src/hooks/useFlags';
31+
import { useVPCDualStack } from 'src/hooks/useVPCDualStack';
3032
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';
3133

3234
import { VPC_CREATE_FORM_VPC_HELPER_TEXT } from '../../constants';
@@ -60,25 +62,17 @@ export const VPCTopSectionContent = (props: Props) => {
6062
control,
6163
formState: { errors },
6264
} = useFormContext<CreateVPCPayload>();
63-
const { append, fields, remove } = useFieldArray({
65+
66+
const { update } = useFieldArray({
6467
control,
65-
name: 'ipv6',
68+
name: 'subnets',
6669
});
6770

68-
const isDualStackSelected = fields.some((f) => f.range);
71+
const subnets = useWatch({ control, name: 'subnets' });
72+
const vpcIPv6 = useWatch({ control, name: 'ipv6' });
6973

70-
const { data: account } = useAccount();
71-
const isDualStackEnabled = isFeatureEnabledV2(
72-
'VPC Dual Stack',
73-
Boolean(flags.vpcIpv6),
74-
account?.capabilities ?? []
75-
);
76-
77-
const isEnterpriseCustomer = isFeatureEnabledV2(
78-
'VPC IPv6 Large Prefixes',
79-
Boolean(flags.vpcIpv6),
80-
account?.capabilities ?? []
81-
);
74+
const { isDualStackEnabled, isDualStackSelected, isEnterpriseCustomer } =
75+
useVPCDualStack(vpcIPv6);
8276

8377
return (
8478
<>
@@ -152,114 +146,137 @@ export const VPCTopSectionContent = (props: Props) => {
152146
{isDualStackEnabled && (
153147
<Box marginTop={2}>
154148
<FormLabel>Networking IP Stack </FormLabel>
155-
<RadioGroup sx={{ display: 'block' }}>
156-
<Grid container spacing={2}>
157-
<SelectionCard
158-
checked={!isDualStackSelected}
159-
gridSize={{
160-
md: isDrawer ? 12 : 3,
161-
sm: 12,
162-
xs: 12,
163-
}}
164-
heading="IPv4"
165-
onClick={() => {
166-
remove(0);
167-
}}
168-
renderIcon={() => <Radio checked={!isDualStackSelected} />}
169-
renderVariant={() => (
170-
<TooltipIcon
171-
status="info"
172-
sxTooltipIcon={{
173-
padding: '8px',
149+
<Controller
150+
control={control}
151+
name="ipv6"
152+
render={({ field }) => (
153+
<RadioGroup sx={{ display: 'block' }}>
154+
<Grid container spacing={2}>
155+
<SelectionCard
156+
checked={!isDualStackSelected}
157+
gridSize={{
158+
md: isDrawer ? 12 : 3,
159+
sm: 12,
160+
xs: 12,
174161
}}
175-
text={
176-
<Typography>
177-
The VPC uses IPv4 addresses only. The VPC can use the
178-
entire RFC 1918 specified range for subnetting.
179-
</Typography>
180-
}
181-
width={250}
162+
heading="IPv4"
163+
onClick={() => {
164+
field.onChange([]);
165+
subnets?.forEach((subnet, idx) =>
166+
update(idx, {
167+
...subnet,
168+
ipv6: undefined,
169+
})
170+
);
171+
}}
172+
renderIcon={() => <Radio checked={!isDualStackSelected} />}
173+
renderVariant={() => (
174+
<TooltipIcon
175+
status="info"
176+
sxTooltipIcon={{
177+
padding: '8px',
178+
}}
179+
text={
180+
<Typography>
181+
The VPC uses IPv4 addresses only. The VPC can use
182+
the entire RFC 1918 specified range for subnetting.
183+
</Typography>
184+
}
185+
width={250}
186+
/>
187+
)}
188+
subheadings={[]}
189+
sxCardBase={{ gap: 0 }}
190+
sxCardBaseIcon={{ svg: { fontSize: '20px' } }}
182191
/>
183-
)}
184-
subheadings={[]}
185-
sxCardBase={{ gap: 0 }}
186-
sxCardBaseIcon={{ svg: { fontSize: '20px' } }}
187-
/>
188-
<SelectionCard
189-
checked={isDualStackSelected}
190-
gridSize={{
191-
md: isDrawer ? 12 : 3,
192-
sm: 12,
193-
xs: 12,
194-
}}
195-
heading="IPv4 + IPv6 (Dual Stack)"
196-
onClick={() => {
197-
if (fields.length === 0) {
198-
append({
199-
range: '/52',
200-
});
201-
}
202-
}}
203-
renderIcon={() => <Radio checked={isDualStackSelected} />}
204-
renderVariant={() => (
205-
<TooltipIcon
206-
status="info"
207-
sxTooltipIcon={{
208-
padding: '8px',
192+
<SelectionCard
193+
checked={isDualStackSelected}
194+
gridSize={{
195+
md: isDrawer ? 12 : 3,
196+
sm: 12,
197+
xs: 12,
198+
}}
199+
heading="IPv4 + IPv6 (Dual Stack)"
200+
onClick={() => {
201+
field.onChange([
202+
{
203+
range: '/52',
204+
},
205+
]);
206+
subnets?.forEach((subnet, idx) =>
207+
update(idx, {
208+
...subnet,
209+
ipv6: subnet.ipv6 ?? [{ range: '/56' }],
210+
})
211+
);
209212
}}
210-
text={
211-
<Stack spacing={2}>
212-
<Typography>
213-
The VPC supports both IPv4 and IPv6 addresses.
214-
</Typography>
215-
<Typography>
216-
For IPv4, the VPC can use the entire RFC 1918
217-
specified range for subnetting.
218-
</Typography>
219-
<Typography>
220-
For IPv6, the VPC is assigned an IPv6 prefix length of{' '}
221-
<Code>/52</Code> by default.
222-
</Typography>
223-
</Stack>
224-
}
225-
width={250}
213+
renderIcon={() => <Radio checked={isDualStackSelected} />}
214+
renderVariant={() => (
215+
<TooltipIcon
216+
status="info"
217+
sxTooltipIcon={{
218+
padding: '8px',
219+
}}
220+
text={
221+
<Stack spacing={2}>
222+
<Typography>
223+
The VPC supports both IPv4 and IPv6 addresses.
224+
</Typography>
225+
<Typography>
226+
For IPv4, the VPC can use the entire RFC 1918
227+
specified range for subnetting.
228+
</Typography>
229+
<Typography>
230+
For IPv6, the VPC is assigned an IPv6 prefix
231+
length of <Code>/52</Code> by default.
232+
</Typography>
233+
</Stack>
234+
}
235+
width={250}
236+
/>
237+
)}
238+
subheadings={[]}
239+
sxCardBase={{ gap: 0 }}
240+
sxCardBaseIcon={{ svg: { fontSize: '20px' } }}
226241
/>
227-
)}
228-
subheadings={[]}
229-
sxCardBase={{ gap: 0 }}
230-
sxCardBaseIcon={{ svg: { fontSize: '20px' } }}
231-
/>
232-
</Grid>
233-
</RadioGroup>
242+
</Grid>
243+
</RadioGroup>
244+
)}
245+
/>
234246
</Box>
235247
)}
236248
{isDualStackSelected && isEnterpriseCustomer && (
237249
<Controller
238250
control={control}
239251
name="ipv6"
240-
render={() => (
252+
render={({ field, fieldState }) => (
241253
<RadioGroup
242-
onChange={(_, value) => {
243-
remove(0);
244-
append({
245-
range: value,
246-
});
247-
}}
248-
value={fields[0].range}
254+
onChange={(_, value) => field.onChange([{ range: value }])}
255+
value={field.value}
249256
>
250257
<StyledFormLabel sx={{ marginTop: 1, marginBottom: 0 }}>
251258
VPC IPv6 Prefix Length
252259
</StyledFormLabel>
253260
{errors.ipv6 && (
254261
<Notice
255262
sx={{ marginTop: 1 }}
256-
text={errors.ipv6[0]?.range?.message}
263+
text={fieldState.error?.message}
257264
variant="error"
258265
/>
259266
)}
260267
<>
261-
<FormControlLabel control={<Radio />} label="/52" value="/52" />
262-
<FormControlLabel control={<Radio />} label="/48" value="/48" />
268+
<FormControlLabel
269+
checked={vpcIPv6 && vpcIPv6[0].range === '/52'}
270+
control={<Radio />}
271+
label="/52"
272+
value="/52"
273+
/>
274+
<FormControlLabel
275+
checked={vpcIPv6 && vpcIPv6[0].range === '/48'}
276+
control={<Radio />}
277+
label="/48"
278+
value="/48"
279+
/>
263280
</>
264281
</RadioGroup>
265282
)}

packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Button, Divider } from '@linode/ui';
22
import Grid from '@mui/material/Grid';
33
import * as React from 'react';
4-
import { useFieldArray, useFormContext } from 'react-hook-form';
4+
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
55

6+
import { useVPCDualStack } from 'src/hooks/useVPCDualStack';
67
import {
78
DEFAULT_SUBNET_IPV4_VALUE,
89
getRecommendedSubnetIPv4,
@@ -27,10 +28,14 @@ export const MultipleSubnetInput = (props: Props) => {
2728
name: 'subnets',
2829
});
2930

31+
const vpcIPv6 = useWatch({ control, name: 'ipv6' });
32+
3033
const [lastRecommendedIPv4, setLastRecommendedIPv4] = React.useState(
3134
DEFAULT_SUBNET_IPV4_VALUE
3235
);
3336

37+
const { shouldDisplayIPv6, recommendedIPv6 } = useVPCDualStack(vpcIPv6);
38+
3439
const handleAddSubnet = () => {
3540
const recommendedIPv4 = getRecommendedSubnetIPv4(
3641
lastRecommendedIPv4,
@@ -39,6 +44,7 @@ export const MultipleSubnetInput = (props: Props) => {
3944
setLastRecommendedIPv4(recommendedIPv4);
4045
append({
4146
ipv4: recommendedIPv4,
47+
ipv6: recommendedIPv6,
4248
label: '',
4349
});
4450
};
@@ -56,6 +62,7 @@ export const MultipleSubnetInput = (props: Props) => {
5662
idx={subnetIdx}
5763
isCreateVPCDrawer={isDrawer}
5864
remove={remove}
65+
shouldDisplayIPv6={shouldDisplayIPv6}
5966
/>
6067
</Grid>
6168
);

0 commit comments

Comments
 (0)