Skip to content
Merged
2 changes: 2 additions & 0 deletions packages/manager/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Cypress tests for Premium Plans and Horizontal Resizing ([#12854](https://github.com/linode/manager/pull/12854))
- Cypress test flake in "events-fetching.spec.ts" ([#12875](https://github.com/linode/manager/pull/12875))
- Update Cypress tests following LKE-E postLa feature flag enablement ([#12883](https://github.com/linode/manager/pull/12883))
- Reflect 'Allow public IPv4 access (1:1 NAT)' copy change in E2E and unit tests related to Linode Create and networking flows ([#12885](https://github.com/linode/manager/pull/12885))

### Upcoming Features:

Expand All @@ -81,6 +82,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- ACLP: Add `GlobalFilterGroupByRendererComponent` and `WidgetFilterGroupByRendererComponent` ([#12865](https://github.com/linode/manager/pull/12865))
- Account scope support for ACLP-Alerting firewall dimension filters ([#12879](https://github.com/linode/manager/pull/12879))
- Ability to edit tags on volume details page ([#12800](https://github.com/linode/manager/pull/12800))
- Add VPC IPv6 support in Linode Create flow ([#12885](https://github.com/linode/manager/pull/12885))

## [2025-09-11] - v1.150.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@
.click();

// Check box to assign public IPv4.
cy.findByText('Assign a public IPv4 address for this Linode')
cy.findByText('Allow public IPv4 access (1:1 NAT)')

Check warning on line 314 in packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":314,"column":19,"nodeType":"Literal","endLine":314,"endColumn":55}
.should('be.visible')
.click();

Expand Down Expand Up @@ -548,7 +548,7 @@
* - Confirms that outgoing API request contains expected VPC interface data.
* - Confirms newly assigned Linode does not have an unrecommended config notice inside VPC
*/
it('can assign existing VPCs during Linode Create flow (Linode Inteface)', () => {
it('can assign existing VPCs during Linode Create flow (Linode Interface)', () => {
const mockSubnet = subnetFactory.build({
id: randomNumber(),
ipv4: `${randomIp()}/0`,
Expand Down Expand Up @@ -820,7 +820,7 @@
.click();

// Check box to assign public IPv4.
cy.findByText('Assign a public IPv4 address for this Linode')
cy.findByText('Allow public IPv4 access (1:1 NAT)')
.should('be.visible')
.click();

Expand Down Expand Up @@ -1005,7 +1005,7 @@
.click();

// Check box to assign public IPv4.
cy.findByText('Assign a public IPv4 address for this Linode')
cy.findByText('Allow public IPv4 access (1:1 NAT)')
.should('be.visible')
.click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ describe('Linode Config management', () => {
/*
* - Tests Linode config edit and VPC interface assignment UI flows using mock API data.
* - When the user sets primary interface to eth0, sets eth0 to "Public Internet", and sets eth1 to "VPC", confirm that correct notice appears.
* - When the user sets primary interface to eth0, sets eth0 to "Public Internet", sets eth1 to "VPC", and checks "Assign a public IPv4 address for this Linode", confirm that correct notice appears.
* - When the user sets primary interface to eth0, sets eth0 to "Public Internet", sets eth1 to "VPC", and checks "Allow public IPv4 access (1:1 NAT)", confirm that correct notice appears.
* - Confirms that "REBOOT NEEDED" status indicator appears upon creating VPC config.
*/
it('Creates a new config using non-recommended settings and confirm the informational notices', () => {
Expand Down Expand Up @@ -823,7 +823,7 @@ describe('Linode Config management', () => {
cy.findByText(LINODE_UNREACHABLE_HELPER_TEXT).should('be.visible');

// Sets eth0 to "Public Internet", and sets eth1 to "VPC",
// and checks "Assign a public IPv4 address for this Linode"
// and checks "Allow public IPv4 access (1:1 NAT)"
cy.get('[data-qa-textfield-label="VPC"]').scrollIntoView();
cy.get('[data-qa-textfield-label="VPC"]').click();
cy.focused().type(`${mockVPC.label}`);
Expand All @@ -840,7 +840,7 @@ describe('Linode Config management', () => {
.findByTitle(`${mockSubnet.label} (${mockSubnet.ipv4})`)
.should('be.visible')
.click();
cy.findByText('Assign a public IPv4 address for this Linode')
cy.findByText('Allow public IPv4 access (1:1 NAT)')
.should('be.visible')
.click();
// Confirm that internet access warning is displayed.
Expand Down
150 changes: 126 additions & 24 deletions packages/manager/src/features/Linodes/LinodeCreate/Networking/VPC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Autocomplete,
Box,
Checkbox,
Divider,
FormControlLabel,
Notice,
Stack,
Expand All @@ -14,17 +15,25 @@ import { LinkButton } from '@linode/ui';
import React, { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

import { VPCPublicIPLabel } from 'src/features/VPCs/components/VPCPublicIPLabel';
import {
VPCIPv6PublicIPLabel,
VPCPublicIPLabel,
} from 'src/features/VPCs/components/VPCPublicIPLabel';
import {
REGION_CAVEAT_HELPER_TEXT,
VPC_AUTO_ASSIGN_IPV4_TOOLTIP,
VPC_AUTO_ASSIGN_IPV6_TOOLTIP,
} from 'src/features/VPCs/constants';
import { generateVPCIPv6InputHelperText } from 'src/features/VPCs/utils';
import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer';
import { useVPCDualStack } from 'src/hooks/useVPCDualStack';

import { VPCAvailabilityNotice } from './VPCAvailabilityNotice';
import { VPCIPv6Ranges } from './VPCIPv6Ranges';
import { VPCRanges } from './VPCRanges';

import type { LinodeCreateFormValues } from '../utilities';
import type { Theme } from '@linode/ui';

interface Props {
index: number;
Expand All @@ -33,6 +42,7 @@ interface Props {
export const VPC = ({ index }: Props) => {
const {
control,
getValues,
resetField,
setValue,
formState: { errors },
Expand All @@ -58,8 +68,20 @@ export const VPC = ({ index }: Props) => {
filter: { region: regionId },
});

const { isDualStackEnabled } = useVPCDualStack();

const selectedVPC = vpcs?.find((vpc) => vpc.id === selectedVPCId);

// Check that selected subnet supports IPv6
const selectedSubnet = selectedVPC?.subnets.find(
(subnet) =>
subnet.id === getValues(`linodeInterfaces.${index}.vpc.subnet_id`)
);

const showIPv6Fields =
isDualStackEnabled &&
Boolean(selectedSubnet?.ipv6?.length && selectedSubnet?.ipv6?.length > 0);

return (
<Box>
<Stack spacing={1.5}>
Expand All @@ -83,7 +105,7 @@ export const VPC = ({ index }: Props) => {
field.onChange(vpc?.id ?? null);

if (vpc && vpc.subnets.length === 1) {
// If the user selectes a VPC and the VPC only has one subnet,
// If the user selects a VPC and the VPC only has one subnet,
// preselect that subnet for the user.
setValue(
`linodeInterfaces.${index}.vpc.subnet_id`,
Expand Down Expand Up @@ -145,10 +167,7 @@ export const VPC = ({ index }: Props) => {
disabled={!regionSupportsVPCs}
label={
<Stack alignItems="center" direction="row">
<Typography>
Auto-assign a VPC IPv4 address for this Linode in the
VPC
</Typography>
<Typography>Auto-assign VPC IPv4 address</Typography>
<TooltipIcon
status="info"
text={VPC_AUTO_ASSIGN_IPV4_TOOLTIP}
Expand Down Expand Up @@ -178,28 +197,111 @@ export const VPC = ({ index }: Props) => {
</Box>
)}
/>
<Controller
control={control}
name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`}
render={({ field, fieldState }) => (
<Box>
{fieldState.error?.message && (
<Notice text={fieldState.error.message} variant="error" />
{showIPv6Fields && (
<Controller
control={control}
name={`linodeInterfaces.${index}.vpc.ipv6.slaac.0.range`}
render={({ field, fieldState }) => (
<Box>
<FormControlLabel
checked={field.value === 'auto'}
control={<Checkbox sx={{ ml: 0.4 }} />}
disabled={!regionSupportsVPCs}
label={
<Stack alignItems="center" direction="row">
<Typography>Auto-assign VPC IPv6 address</Typography>
<TooltipIcon
status="info"
text={VPC_AUTO_ASSIGN_IPV6_TOOLTIP}
/>
</Stack>
}
onChange={(e, checked) =>
field.onChange(checked ? 'auto' : '')
}
/>
{field.value !== 'auto' && (
<TextField
containerProps={{ sx: { mb: 1.5, mt: 1 } }}
errorText={
fieldState.error?.message ??
errors.linodeInterfaces?.[index]?.vpc?.ipv6?.slaac?.[0]
?.range?.message
}
helperText={generateVPCIPv6InputHelperText(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper text doesn't seem to be updating when switching selected subnets

Screen.Recording.2025-09-22.at.4.13.01.PM.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, I've made a note of it -- the UI for that field and helper text will be changing with a forthcoming PR in the next few days, so we can bundle a fix with that (if the helper text is retained in the updated design)

selectedSubnet?.ipv6?.[0].range ?? ''
)}
label="VPC IPv6"
noMarginTop
onBlur={field.onBlur}
onChange={field.onChange}
required
value={field.value}
/>
)}
</Box>
)}
/>
)}
<Box>
<Divider
sx={(theme) => ({ marginBottom: theme.spacingFunction(16) })}
/>
<Typography sx={(theme: Theme) => ({ font: theme.font.bold })}>
Public access
</Typography>
<Controller
control={control}
name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`}
render={({ field, fieldState }) => (
<Box>
{fieldState.error?.message && (
<Notice text={fieldState.error.message} variant="error" />
)}
<FormControlLabel
checked={field.value === 'auto'}
control={<Checkbox sx={{ ml: 0.4 }} />}
disabled={!regionSupportsVPCs}
label={<VPCPublicIPLabel />}
onChange={(e, checked) =>
field.onChange(checked ? 'auto' : null)
}
/>
</Box>
)}
/>
{showIPv6Fields && (
<Controller
control={control}
name={`linodeInterfaces.${index}.vpc.ipv6.is_public`}
render={({ field, fieldState }) => (
<Box>
{fieldState.error?.message && (
<Notice text={fieldState.error.message} variant="error" />
)}
<FormControlLabel
checked={field.value === true}
control={<Checkbox sx={{ ml: 0.4 }} />}
disabled={!regionSupportsVPCs}
label={<VPCIPv6PublicIPLabel />}
onChange={() => field.onChange(!field.value)}
/>
</Box>
)}
<FormControlLabel
checked={field.value === 'auto'}
control={<Checkbox sx={{ ml: 0.4 }} />}
disabled={!regionSupportsVPCs}
label={<VPCPublicIPLabel />}
onChange={(e, checked) =>
field.onChange(checked ? 'auto' : null)
}
/>
</Box>
/>
)}
/>
<Divider
sx={(theme) => ({ marginTop: theme.spacingFunction(16) })}
/>
</Box>
</Stack>
<VPCRanges disabled={!regionSupportsVPCs} interfaceIndex={index} />
{showIPv6Fields && (
<VPCIPv6Ranges
disabled={!regionSupportsVPCs}
interfaceIndex={index}
/>
)}
</Stack>
<VPCCreateDrawer
onClose={() => setIsCreateDrawerOpen(false)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
CloseIcon,
IconButton,
LinkButton,
Notice,
Stack,
TextField,
TooltipIcon,
} from '@linode/ui';
import React from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';

import { VPCIPv6RangesDescription } from 'src/features/VPCs/components/VPCRangesDescription';

import type { LinodeCreateFormValues } from '../utilities';

interface Props {
disabled: boolean;
interfaceIndex: number;
}

export const VPCIPv6Ranges = ({ disabled, interfaceIndex }: Props) => {
const {
control,
formState: { errors },
} = useFormContext<LinodeCreateFormValues>();

const { append, fields, remove } = useFieldArray({
control,
name: `linodeInterfaces.${interfaceIndex}.vpc.ipv6.ranges`,
});

return (
<Stack>
<Stack spacing={1}>
{errors?.linodeInterfaces?.[interfaceIndex]?.vpc?.ipv6?.ranges
?.message && (
<Notice variant="error">
{
errors?.linodeInterfaces?.[interfaceIndex]?.vpc?.ipv6?.ranges
?.message
}
</Notice>
)}
{fields.map((field, index) => (
<Stack
alignItems="flex-start"
direction="row"
key={field.id}
spacing={0.5}
>
<Controller
control={control}
name={`linodeInterfaces.${interfaceIndex}.vpc.ipv6.ranges.${index}.range`}
render={({ field, fieldState }) => (
<TextField
errorText={fieldState.error?.message}
hideLabel
inputRef={field.ref}
label={`IP Range ${index}`}
onBlur={field.onBlur}
onChange={field.onChange}
sx={{ minWidth: 290 }}
value={field.value}
/>
)}
/>
<IconButton
aria-label={`Remove IP Range ${index}`}
onClick={() => remove(index)}
sx={{ padding: 1 }}
>
<CloseIcon />
</IconButton>
</Stack>
))}
</Stack>
<Stack alignItems="center" direction="row" spacing={1}>
<LinkButton disabled={disabled} onClick={() => append({ range: '' })}>
Add IPv6 Range
</LinkButton>
<TooltipIcon
status="info"
sxTooltipIcon={{ p: 0.5 }}
text={<VPCIPv6RangesDescription />}
/>
</Stack>
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ export const VPCRanges = ({ disabled, interfaceIndex }: Props) => {
label={`IP Range ${index}`}
onBlur={field.onBlur}
onChange={field.onChange}
// eslint-disable-next-line sonarjs/no-hardcoded-ip
placeholder="10.0.0.0/24"
sx={{ minWidth: 290 }}
value={field.value}
/>
Expand Down
Loading