Skip to content

Commit 4adcb87

Browse files
upcoming: [M3-9637] - Support more VPC features and default Firewalls on the Linode Create flow (#11915)
* save progress * handle complex error case * more fixes * dial in UI * default firewalls work on Linode Create flow * clean up a bit * unit test a few things * Added changeset: Support more VPC features when using Linode Interfaces on the Linode Create page * Added changeset: Pre-select default firewalls on the Linode Create flow * not sure what to do here * not sure what to do here * some fixes and better error handling * fix clickable areas in VPC section * improve default firewall ux * hacky fix to improve error handling for duplicate purpose field * Revert "hacky fix to improve error handling for duplicate purpose field" This reverts commit 7ce8c8d. --------- Co-authored-by: Banks Nussman <banks@nussman.us>
1 parent ae9e628 commit 4adcb87

File tree

11 files changed

+448
-99
lines changed

11 files changed

+448
-99
lines changed
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+
Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915))
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+
Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915))

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { useAllFirewallsQuery } from '@linode/queries';
2-
import { Autocomplete, Box, Stack } from '@linode/ui';
1+
import { Box, Stack } from '@linode/ui';
32
import React, { useState } from 'react';
43
import { useController, useFormContext, useWatch } from 'react-hook-form';
54

65
import { LinkButton } from 'src/components/LinkButton';
6+
import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect';
77
import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/CreateFirewallDrawer';
88
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
99

@@ -26,31 +26,23 @@ export const InterfaceFirewall = ({ index }: Props) => {
2626
name: `linodeInterfaces.${index}.firewall_id`,
2727
});
2828

29-
const { data: firewalls, error, isLoading } = useAllFirewallsQuery();
30-
3129
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
3230

3331
const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
3432
globalGrantType: 'add_linodes',
3533
});
3634

37-
const selectedFirewall =
38-
firewalls?.find((firewall) => firewall.id === field.value) ?? null;
39-
4035
return (
4136
<Stack spacing={2}>
4237
<Stack spacing={1.5}>
43-
<Autocomplete
38+
<FirewallSelect
4439
disabled={isLinodeCreateRestricted}
45-
errorText={fieldState.error?.message ?? error?.[0].reason}
40+
errorText={fieldState.error?.message}
4641
label={`${labelMap[interfaceType]} Interface Firewall`}
47-
loading={isLoading}
48-
noMarginTop
4942
onBlur={field.onBlur}
5043
onChange={(e, firewall) => field.onChange(firewall?.id ?? null)}
51-
options={firewalls ?? []}
5244
placeholder="None"
53-
value={selectedFirewall}
45+
value={field.value}
5446
/>
5547
<Box>
5648
<LinkButton

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1+
import { useFirewallSettingsQuery } from '@linode/queries';
12
import { FormControl, FormControlLabel, Radio, RadioGroup } from '@linode/ui';
23
import React from 'react';
34
import { useController, useFormContext } from 'react-hook-form';
45

56
import { FormLabel } from 'src/components/FormLabel';
67

8+
import { getDefaultFirewallForInterfacePurpose } from './utilities';
9+
710
import type { LinodeCreateFormValues } from '../utilities';
11+
import type { InterfacePurpose } from '@linode/api-v4';
812

913
interface Props {
1014
index: number;
1115
}
1216

1317
export const InterfaceType = ({ index }: Props) => {
14-
const { control } = useFormContext<LinodeCreateFormValues>();
18+
const {
19+
control,
20+
setValue,
21+
getFieldState,
22+
} = useFormContext<LinodeCreateFormValues>();
23+
24+
const { data: firewallSettings } = useFirewallSettingsQuery();
1525

1626
const { field } = useController({
1727
control,
@@ -24,8 +34,26 @@ export const InterfaceType = ({ index }: Props) => {
2434
Network Connection
2535
</FormLabel>
2636
<RadioGroup
37+
onChange={(e, value) => {
38+
// Change the interface purpose (Public, VPC, VLAN)
39+
field.onChange(value);
40+
41+
const defaultFirewall = getDefaultFirewallForInterfacePurpose(
42+
value as InterfacePurpose,
43+
firewallSettings
44+
);
45+
46+
// Set the Firewall based on defaults if:
47+
// - there is a default firewall for this interface type
48+
// - the user has not touched the Firewall field
49+
if (
50+
defaultFirewall &&
51+
!getFieldState(`linodeInterfaces.${index}.firewall_id`).isTouched
52+
) {
53+
setValue(`linodeInterfaces.${index}.firewall_id`, defaultFirewall);
54+
}
55+
}}
2756
aria-labelledby="network-interface"
28-
onChange={field.onChange}
2957
row
3058
sx={{ mb: '0px !important' }}
3159
value={field.value}

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useFirewallSettingsQuery } from '@linode/queries';
12
import {
23
Button,
34
Divider,
@@ -13,6 +14,7 @@ import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
1314
import { Firewall } from './Firewall';
1415
import { InterfaceGeneration } from './InterfaceGeneration';
1516
import { LinodeInterface } from './LinodeInterface';
17+
import { getDefaultInterfacePayload } from './utilities';
1618

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

@@ -22,6 +24,8 @@ export const Networking = () => {
2224
formState: { errors },
2325
} = useFormContext<LinodeCreateFormValues>();
2426

27+
const { data: firewallSettings } = useFirewallSettingsQuery();
28+
2529
const { append, fields, remove } = useFieldArray({
2630
control,
2731
name: 'linodeInterfaces',
@@ -42,16 +46,9 @@ export const Networking = () => {
4246
>
4347
<Typography variant="h2">Networking</Typography>
4448
<Button
45-
onClick={() =>
46-
append({
47-
default_route: null,
48-
firewall_id: null,
49-
public: {},
50-
purpose: 'public',
51-
vlan: null,
52-
vpc: null,
53-
})
54-
}
49+
onClick={() => {
50+
append(getDefaultInterfacePayload('public', firewallSettings));
51+
}}
5552
buttonType="outlined"
5653
endIcon={<PlusSignIcon height="12px" width="12px" />}
5754
>

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

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { useAllVPCsQuery, useRegionQuery } from '@linode/queries';
2-
import { Autocomplete, Box, Notice, Stack } from '@linode/ui';
2+
import {
3+
Autocomplete,
4+
Box,
5+
Checkbox,
6+
FormControlLabel,
7+
Notice,
8+
Stack,
9+
TextField,
10+
TooltipIcon,
11+
Typography,
12+
} from '@linode/ui';
313
import React, { useState } from 'react';
414
import { Controller, useFormContext, useWatch } from 'react-hook-form';
515

616
import { LinkButton } from 'src/components/LinkButton';
7-
import { REGION_CAVEAT_HELPER_TEXT } from 'src/features/VPCs/constants';
17+
import {
18+
REGION_CAVEAT_HELPER_TEXT,
19+
VPC_AUTO_ASSIGN_IPV4_TOOLTIP,
20+
} from 'src/features/VPCs/constants';
821
import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer';
922

23+
import { VPCRanges } from './VPCRanges';
24+
1025
import type { LinodeCreateFormValues } from '../utilities';
1126

1227
interface Props {
@@ -18,6 +33,7 @@ export const VPC = ({ index }: Props) => {
1833
control,
1934
resetField,
2035
setValue,
36+
formState: { errors },
2137
} = useFormContext<LinodeCreateFormValues>();
2238
const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false);
2339

@@ -118,6 +134,85 @@ export const VPC = ({ index }: Props) => {
118134
control={control}
119135
name={`linodeInterfaces.${index}.vpc.subnet_id`}
120136
/>
137+
<Stack>
138+
<Controller
139+
render={({ field, fieldState }) => (
140+
<Box>
141+
<FormControlLabel
142+
label={
143+
<Stack alignItems="center" direction="row">
144+
<Typography>
145+
Auto-assign a VPC IPv4 address for this Linode in the
146+
VPC
147+
</Typography>
148+
<TooltipIcon
149+
status="help"
150+
text={VPC_AUTO_ASSIGN_IPV4_TOOLTIP}
151+
/>
152+
</Stack>
153+
}
154+
onChange={(e, checked) =>
155+
field.onChange(checked ? 'auto' : '')
156+
}
157+
checked={field.value === 'auto'}
158+
control={<Checkbox sx={{ ml: 0.4 }} />}
159+
disabled={!regionSupportsVPCs}
160+
/>
161+
{field.value !== 'auto' && (
162+
<TextField
163+
errorText={
164+
fieldState.error?.message ??
165+
errors.linodeInterfaces?.[index]?.vpc?.ipv4
166+
?.addresses?.[0]?.message
167+
}
168+
containerProps={{ sx: { mb: 1.5, mt: 1 } }}
169+
label="VPC IPv4"
170+
noMarginTop
171+
onBlur={field.onBlur}
172+
onChange={field.onChange}
173+
required
174+
value={field.value}
175+
/>
176+
)}
177+
</Box>
178+
)}
179+
control={control}
180+
name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.address`}
181+
/>
182+
<Controller
183+
render={({ field, fieldState }) => (
184+
<Box>
185+
{fieldState.error?.message && (
186+
<Notice text={fieldState.error.message} variant="error" />
187+
)}
188+
<FormControlLabel
189+
label={
190+
<Stack alignItems="center" direction="row">
191+
<Typography>
192+
Assign a public IPv4 address for this Linode
193+
</Typography>
194+
<TooltipIcon
195+
text={
196+
'Access the internet through the public IPv4 address using static 1:1 NAT.'
197+
}
198+
status="help"
199+
/>
200+
</Stack>
201+
}
202+
onChange={(e, checked) =>
203+
field.onChange(checked ? 'auto' : null)
204+
}
205+
checked={field.value === 'auto'}
206+
control={<Checkbox sx={{ ml: 0.4 }} />}
207+
disabled={!regionSupportsVPCs}
208+
/>
209+
</Box>
210+
)}
211+
control={control}
212+
name={`linodeInterfaces.${index}.vpc.ipv4.addresses.0.nat_1_1_address`}
213+
/>
214+
</Stack>
215+
<VPCRanges disabled={!regionSupportsVPCs} interfaceIndex={index} />
121216
</Stack>
122217
<VPCCreateDrawer
123218
onSuccess={(vpc) => {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
IconButton,
3+
Stack,
4+
TextField,
5+
TooltipIcon,
6+
Typography,
7+
} from '@linode/ui';
8+
import CloseIcon from '@mui/icons-material/Close';
9+
import React from 'react';
10+
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
11+
12+
import { Link } from 'src/components/Link';
13+
import { LinkButton } from 'src/components/LinkButton';
14+
15+
import type { LinodeCreateFormValues } from '../utilities';
16+
17+
interface Props {
18+
disabled: boolean;
19+
interfaceIndex: number;
20+
}
21+
22+
export const VPCRanges = ({ disabled, interfaceIndex }: Props) => {
23+
const { control } = useFormContext<LinodeCreateFormValues>();
24+
25+
const { append, fields, remove } = useFieldArray({
26+
control,
27+
name: `linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges`,
28+
});
29+
30+
return (
31+
<Stack>
32+
<Stack spacing={1}>
33+
{fields.map((field, index) => (
34+
<Stack
35+
alignItems="flex-start"
36+
direction="row"
37+
key={field.id}
38+
spacing={0.5}
39+
>
40+
<Controller
41+
render={({ field, fieldState }) => (
42+
<TextField
43+
errorText={fieldState.error?.message}
44+
hideLabel
45+
label={`IP Range ${index}`}
46+
onBlur={field.onBlur}
47+
onChange={field.onChange}
48+
placeholder="10.0.0.0/24"
49+
sx={{ minWidth: 290 }}
50+
value={field.value}
51+
/>
52+
)}
53+
control={control}
54+
name={`linodeInterfaces.${interfaceIndex}.vpc.ipv4.ranges.${index}.range`}
55+
/>
56+
<IconButton
57+
aria-label={`Remove IP Range ${index}`}
58+
onClick={() => remove(index)}
59+
sx={{ padding: 0.75 }}
60+
>
61+
<CloseIcon />
62+
</IconButton>
63+
</Stack>
64+
))}
65+
</Stack>
66+
<Stack alignItems="center" direction="row" spacing={1}>
67+
<LinkButton isDisabled={disabled} onClick={() => append({ range: '' })}>
68+
Add IPv4 Range
69+
</LinkButton>
70+
<TooltipIcon
71+
text={
72+
<Typography>
73+
Assign additional IPv4 address ranges that the VPC can use to
74+
reach services running on this Linode.{' '}
75+
<Link to="https://techdocs.akamai.com/cloud-computing/docs/assign-a-compute-instance-to-a-vpc">
76+
Learn more
77+
</Link>
78+
.
79+
</Typography>
80+
}
81+
status="help"
82+
sxTooltipIcon={{ p: 0.5 }}
83+
/>
84+
</Stack>
85+
</Stack>
86+
);
87+
};

0 commit comments

Comments
 (0)