Skip to content

Commit 793d1a8

Browse files
upcoming: [M3-9535] - Support VPC interfaces on updated Linode Create Networking flow (#11847)
* add vpc field * add subnet field * attempt to improve validation layre * save progress * hopefully fix all typesafety issues * things seems to be working * make sure legacy interfaces can be created safely * improve UX a bit * continue working on validation * clean up tansformer function * add nicer validation errors * Added changeset: Improved accuracy of schemas related to Linode creation * Added changeset: Initial support for VPCs using Linode Interfaces on the Linode create flow * Added changeset: Improved type-safety of Linode Create flow form * validate when values are populated from drawer * use notices instead of helper text * fix spelling * surface another error --------- Co-authored-by: Banks Nussman <banks@nussman.us>
1 parent 74a074b commit 793d1a8

18 files changed

Lines changed: 462 additions & 127 deletions

File tree

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,23 @@ export interface Interface {
201201
ip_ranges?: string[];
202202
}
203203

204-
export type InterfacePayload = Omit<Interface, 'id' | 'active'>;
204+
export interface InterfacePayload {
205+
/**
206+
* Required to specify a VLAN
207+
*/
208+
label?: string | null;
209+
purpose: InterfacePurpose;
210+
/**
211+
* Used for VLAN, but is optional
212+
*/
213+
ipam_address?: string | null;
214+
primary?: boolean;
215+
subnet_id?: number | null;
216+
vpc_id?: number | null;
217+
ipv4?: ConfigInterfaceIPv4;
218+
ipv6?: ConfigInterfaceIPv6;
219+
ip_ranges?: string[] | null;
220+
}
205221

206222
export interface ConfigInterfaceOrderPayload {
207223
ids: number[];
@@ -527,7 +543,7 @@ export interface CreateLinodeRequest {
527543
* This is used to set the swap disk size for the newly-created Linode.
528544
* @default 512
529545
*/
530-
swap_size?: number;
546+
swap_size?: number | null;
531547
/**
532548
* An Image ID to deploy the Linode Disk from.
533549
*/
@@ -540,7 +556,7 @@ export interface CreateLinodeRequest {
540556
* A list of public SSH keys that will be automatically appended to the root user’s
541557
* `~/.ssh/authorized_keys`file when deploying from an Image.
542558
*/
543-
authorized_keys?: string[];
559+
authorized_keys?: string[] | null;
544560
/**
545561
* If this field is set to true, the created Linode will automatically be enrolled in the Linode Backup service.
546562
* This will incur an additional charge. The cost for the Backup service is dependent on the Type of Linode deployed.
@@ -549,7 +565,7 @@ export interface CreateLinodeRequest {
549565
*
550566
* @default false
551567
*/
552-
backups_enabled?: boolean;
568+
backups_enabled?: boolean | null;
553569
/**
554570
* This field is required only if the StackScript being deployed requires input data from the User for successful completion
555571
*/
@@ -560,29 +576,29 @@ export interface CreateLinodeRequest {
560576
* @default true if the Linode is created with an Image or from a Backup.
561577
* @default false if using new Linode Interfaces and no interfaces are defined
562578
*/
563-
booted?: boolean;
579+
booted?: boolean | null;
564580
/**
565581
* The Linode’s label is for display purposes only.
566582
* If no label is provided for a Linode, a default will be assigned.
567583
*/
568-
label?: string;
584+
label?: string | null;
569585
/**
570586
* An array of tags applied to this object.
571587
*
572588
* Tags are for organizational purposes only.
573589
*/
574-
tags?: string[];
590+
tags?: string[] | null;
575591
/**
576592
* If true, the created Linode will have private networking enabled and assigned a private IPv4 address.
577593
* @default false
578594
*/
579-
private_ip?: boolean;
595+
private_ip?: boolean | null;
580596
/**
581597
* A list of usernames. If the usernames have associated SSH keys,
582598
* the keys will be appended to the root users `~/.ssh/authorized_keys`
583599
* file automatically when deploying from an Image.
584600
*/
585-
authorized_users?: string[];
601+
authorized_users?: string[] | null;
586602
/**
587603
* An array of Network Interfaces to add to this Linode’s Configuration Profile.
588604
*/
@@ -598,7 +614,7 @@ export interface CreateLinodeRequest {
598614
*
599615
* Default value on depends on interfaces_for_new_linodes field in AccountSettings object.
600616
*/
601-
interface_generation?: InterfaceGenerationType;
617+
interface_generation?: InterfaceGenerationType | null;
602618
/**
603619
* Default value mirrors network_helper in AccountSettings object. Should only be
604620
* present when using Linode Interfaces.
@@ -612,20 +628,20 @@ export interface CreateLinodeRequest {
612628
/**
613629
* An object containing user-defined data relevant to the creation of Linodes.
614630
*/
615-
metadata?: UserData;
631+
metadata?: UserData | null;
616632
/**
617633
* The `id` of the Firewall to attach this Linode to upon creation.
618634
*/
619635
firewall_id?: number | null;
620636
/**
621637
* An object that assigns this the Linode to a placement group upon creation.
622638
*/
623-
placement_group?: CreateLinodePlacementGroupPayload;
639+
placement_group?: CreateLinodePlacementGroupPayload | null;
624640
/**
625641
* A property with a string literal type indicating whether the Linode is encrypted or unencrypted.
626642
* @default 'enabled' (if the region supports LDE)
627643
*/
628-
disk_encryption?: EncryptionStatus;
644+
disk_encryption?: EncryptionStatus | null;
629645
}
630646

631647
export interface MigrateLinodeRequest {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Tech Stories
3+
---
4+
5+
Improved type-safety of Linode Create flow form ([#11847](https://github.com/linode/manager/pull/11847))
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+
Initial support for VPCs using Linode Interfaces on the Linode create flow ([#11847](https://github.com/linode/manager/pull/11847))

packages/manager/src/components/VLANSelect.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ interface Props {
2020
* Default API filter
2121
*/
2222
filter?: Filter;
23+
/**
24+
* Helper text that will show below the select
25+
*/
26+
helperText?: string;
2327
/**
2428
* Called when the field is blurred
2529
*/
@@ -45,7 +49,16 @@ interface Props {
4549
* - Allows VLAN creation
4650
*/
4751
export const VLANSelect = (props: Props) => {
48-
const { disabled, errorText, filter, onBlur, onChange, sx, value } = props;
52+
const {
53+
disabled,
54+
errorText,
55+
filter,
56+
helperText,
57+
onBlur,
58+
onChange,
59+
sx,
60+
value,
61+
} = props;
4962

5063
const [open, setOpen] = React.useState(false);
5164
const [inputValue, setInputValue] = useState<string>('');
@@ -133,6 +146,7 @@ export const VLANSelect = (props: Props) => {
133146
}}
134147
disabled={disabled}
135148
errorText={errorText ?? error?.[0].reason}
149+
helperText={helperText}
136150
inputValue={selectedVLAN ? selectedVLAN.label : inputValue}
137151
label="VLAN"
138152
loading={isFetching}

packages/manager/src/features/Linodes/LinodeCreate/Addons/utilities.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import type { LinodeCreateFormValues } from '../utilities';
2+
13
interface BackupsEnabledOptions {
24
accountBackupsEnabled: boolean | undefined;
35
isDistributedRegion: boolean;
4-
value: boolean | undefined;
6+
value: LinodeCreateFormValues['backups_enabled'];
57
}
68

79
export const getBackupsEnabledValue = (options: BackupsEnabledOptions) => {
@@ -13,7 +15,7 @@ export const getBackupsEnabledValue = (options: BackupsEnabledOptions) => {
1315
return true;
1416
}
1517

16-
if (options.value === undefined) {
18+
if (options.value === undefined || options.value === null) {
1719
return false;
1820
}
1921

packages/manager/src/features/Linodes/LinodeCreate/Networking/LinodeInterface.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
55
import { InterfaceFirewall } from './InterfaceFirewall';
66
import { InterfaceType } from './InterfaceType';
77
import { VLAN } from './VLAN';
8+
import { VPC } from './VPC';
89

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

@@ -40,14 +41,21 @@ export const LinodeInterface = ({ index, onRemove }: Props) => {
4041
<Typography variant="h3">Interface eth{index}</Typography>
4142
{index !== 0 && <Button onClick={onRemove}>Remove Interface</Button>}
4243
</Stack>
43-
{errors.interfaces?.[index]?.purpose?.message && (
44+
{errors.linodeInterfaces?.[index]?.message && (
4445
<Notice
45-
text={errors.interfaces?.[index]?.purpose?.message}
46+
text={errors.linodeInterfaces?.[index]?.message}
47+
variant="error"
48+
/>
49+
)}
50+
{errors.linodeInterfaces?.[index]?.purpose?.message && (
51+
<Notice
52+
text={errors.linodeInterfaces?.[index]?.purpose?.message}
4653
variant="error"
4754
/>
4855
)}
4956
<InterfaceType index={index} />
5057
{interfaceType === 'vlan' && <VLAN index={index} />}
58+
{interfaceType === 'vpc' && <VPC index={index} />}
5159
{interfaceGeneration === 'linode' && <InterfaceFirewall index={index} />}
5260
</Stack>
5361
);

packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
22
Button,
33
Divider,
4+
Notice,
45
Paper,
56
PlusSignIcon,
67
Stack,
78
Typography,
89
} from '@linode/ui';
910
import React from 'react';
10-
import { useFieldArray, useWatch } from 'react-hook-form';
11+
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
1112

1213
import { Firewall } from './Firewall';
1314
import { InterfaceGeneration } from './InterfaceGeneration';
@@ -16,17 +17,20 @@ import { LinodeInterface } from './LinodeInterface';
1617
import type { LinodeCreateFormValues } from '../utilities';
1718

1819
export const Networking = () => {
19-
const { append, fields, remove } = useFieldArray<
20-
LinodeCreateFormValues,
21-
'linodeInterfaces'
22-
>({
20+
const {
21+
control,
22+
formState: { errors },
23+
} = useFormContext<LinodeCreateFormValues>();
24+
25+
const { append, fields, remove } = useFieldArray({
26+
control,
2327
name: 'linodeInterfaces',
2428
});
2529

26-
const interfaceGeneration = useWatch<
27-
LinodeCreateFormValues,
28-
'interface_generation'
29-
>({ name: 'interface_generation' });
30+
const interfaceGeneration = useWatch({
31+
control,
32+
name: 'interface_generation',
33+
});
3034

3135
return (
3236
<Paper>
@@ -54,6 +58,9 @@ export const Networking = () => {
5458
Add Another Interface
5559
</Button>
5660
</Stack>
61+
{errors.linodeInterfaces?.message && (
62+
<Notice text={errors.linodeInterfaces.message} variant="error" />
63+
)}
5764
<InterfaceGeneration />
5865
{fields.map((field, index) => (
5966
<LinodeInterface
Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Stack, TextField } from '@linode/ui';
1+
import { useRegionQuery } from '@linode/queries';
2+
import { Notice, Stack, TextField } from '@linode/ui';
23
import React from 'react';
34
import { Controller, useFormContext, useWatch } from 'react-hook-form';
45

@@ -20,44 +21,63 @@ export const VLAN = ({ index }: Props) => {
2021

2122
const regionId = useWatch({ control, name: 'region' });
2223

24+
const { data: selectedRegion } = useRegionQuery(regionId);
25+
26+
const regionSupportsVLANs =
27+
selectedRegion?.capabilities.includes('Vlans') ?? false;
28+
2329
return (
24-
<Stack columnGap={2} direction="row" flexWrap="wrap">
25-
<Controller
26-
render={({ field, fieldState }) => (
27-
<VLANSelect
28-
disabled={isLinodeCreateRestricted}
29-
errorText={fieldState.error?.message}
30-
filter={{ region: regionId }}
31-
onBlur={field.onBlur}
32-
onChange={field.onChange}
33-
sx={{ width: 300 }}
34-
value={field.value ?? null}
35-
/>
36-
)}
37-
control={control}
38-
name={`linodeInterfaces.${index}.vlan.vlan_label`}
39-
/>
40-
<Controller
41-
render={({ field, fieldState }) => (
42-
<TextField
43-
tooltipText={
44-
'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.'
45-
}
46-
containerProps={{ maxWidth: 335 }}
47-
disabled={isLinodeCreateRestricted}
48-
errorText={fieldState.error?.message}
49-
label="IPAM Address"
50-
noMarginTop
51-
onBlur={field.onBlur}
52-
onChange={field.onChange}
53-
optional
54-
placeholder="192.0.2.0/24"
55-
value={field.value ?? ''}
56-
/>
57-
)}
58-
control={control}
59-
name={`linodeInterfaces.${index}.vlan.ipam_address`}
60-
/>
30+
<Stack spacing={1.5}>
31+
{!regionId && (
32+
<Notice
33+
text="Select a region to see available VLANs."
34+
variant="warning"
35+
/>
36+
)}
37+
{selectedRegion && !regionSupportsVLANs && (
38+
<Notice
39+
text="VLAN is not available in the selected region."
40+
variant="warning"
41+
/>
42+
)}
43+
<Stack alignItems="flex-start" direction="row" flexWrap="wrap" gap={2}>
44+
<Controller
45+
render={({ field, fieldState }) => (
46+
<VLANSelect
47+
disabled={isLinodeCreateRestricted || !regionSupportsVLANs}
48+
errorText={fieldState.error?.message}
49+
filter={{ region: regionId }}
50+
onBlur={field.onBlur}
51+
onChange={field.onChange}
52+
sx={{ width: 300 }}
53+
value={field.value ?? null}
54+
/>
55+
)}
56+
control={control}
57+
name={`linodeInterfaces.${index}.vlan.vlan_label`}
58+
/>
59+
<Controller
60+
render={({ field, fieldState }) => (
61+
<TextField
62+
tooltipText={
63+
'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.'
64+
}
65+
containerProps={{ maxWidth: 335 }}
66+
disabled={isLinodeCreateRestricted || !regionSupportsVLANs}
67+
errorText={fieldState.error?.message}
68+
label="IPAM Address"
69+
noMarginTop
70+
onBlur={field.onBlur}
71+
onChange={field.onChange}
72+
optional
73+
placeholder="192.0.2.0/24"
74+
value={field.value ?? ''}
75+
/>
76+
)}
77+
control={control}
78+
name={`linodeInterfaces.${index}.vlan.ipam_address`}
79+
/>
80+
</Stack>
6181
</Stack>
6282
);
6383
};

0 commit comments

Comments
 (0)