Skip to content

Commit 64060c2

Browse files
change: [UIE-9859] - Improvements in Add Network interface drawer (linode#13264)
1 parent c2a9115 commit 64060c2

File tree

4 files changed

+152
-12
lines changed

4 files changed

+152
-12
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
Improvements in Add Network interface drawer ([#13264](https://github.com/linode/manager/pull/13264))

packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.test.tsx

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { linodeInterfaceFactoryPublic } from '@linode/utilities';
12
import userEvent from '@testing-library/user-event';
23
import React from 'react';
34

@@ -11,39 +12,60 @@ import { AddInterfaceForm } from './AddInterfaceForm';
1112
const props = { linodeId: 0, onClose: vi.fn(), regionId: '' };
1213

1314
describe('AddInterfaceForm', () => {
14-
it('renders radios for the interface types (Public, VPC, VLAN)', () => {
15-
const { getByRole } = renderWithTheme(<AddInterfaceForm {...props} />);
15+
beforeEach(() => {
16+
server.use(
17+
http.get('*/linode/instances/:linodeId/interfaces', () => {
18+
return HttpResponse.json({
19+
interfaces: [],
20+
});
21+
})
22+
);
23+
});
24+
25+
it('renders radios for the interface types (Public, VPC, VLAN)', async () => {
26+
const { getByRole, findByRole } = renderWithTheme(
27+
<AddInterfaceForm {...props} />
28+
);
29+
30+
// Wait for the loading to complete and form to render
31+
await findByRole('radio', { name: 'VPC' });
1632

1733
expect(getByRole('radio', { name: 'VPC' })).toBeInTheDocument();
1834
expect(getByRole('radio', { name: 'Public' })).toBeInTheDocument();
1935
expect(getByRole('radio', { name: 'VLAN' })).toBeInTheDocument();
2036
});
2137

2238
it('renders a Firewall select if "VPC" is selected', async () => {
23-
const { getByRole, getByLabelText } = renderWithTheme(
39+
const { getByRole, getByLabelText, findByRole } = renderWithTheme(
2440
<AddInterfaceForm {...props} />
2541
);
2642

43+
// Wait for the loading to complete and form to render
44+
await findByRole('radio', { name: 'VPC' });
2745
await userEvent.click(getByRole('radio', { name: 'VPC' }));
2846

2947
expect(getByLabelText('Firewall')).toBeVisible();
3048
});
3149

3250
it('renders a Firewall select if "Public" is selected', async () => {
33-
const { getByRole, getByLabelText } = renderWithTheme(
51+
const { getByRole, getByLabelText, findByRole } = renderWithTheme(
3452
<AddInterfaceForm {...props} />
3553
);
3654

55+
// Wait for the loading to complete and form to render
56+
await findByRole('radio', { name: 'Public' });
3757
await userEvent.click(getByRole('radio', { name: 'Public' }));
3858

3959
expect(getByLabelText('Firewall')).toBeVisible();
4060
});
4161

4262
it('renders does not render a Firewall select if "VLAN" is selected', async () => {
43-
const { getByRole, queryByLabelText } = renderWithTheme(
63+
const { getByRole, queryByLabelText, findByRole } = renderWithTheme(
4464
<AddInterfaceForm {...props} />
4565
);
4666

67+
// Wait for the loading to complete and form to render
68+
await findByRole('radio', { name: 'VLAN' });
4769
await userEvent.click(getByRole('radio', { name: 'VLAN' }));
4870

4971
expect(queryByLabelText('Firewall')).toBeNull();
@@ -67,12 +89,58 @@ describe('AddInterfaceForm', () => {
6789
})
6890
);
6991

70-
const { getByRole, findByDisplayValue } = renderWithTheme(
92+
const { getByRole, findByDisplayValue, findByRole } = renderWithTheme(
7193
<AddInterfaceForm {...props} />
7294
);
7395

96+
// Wait for the loading to complete and form to render
97+
await findByRole('radio', { name: 'VPC' });
7498
await userEvent.click(getByRole('radio', { name: 'VPC' }));
7599

76100
await findByDisplayValue(firewall.label);
77101
});
102+
103+
it('should show a warning notice on selection of VPC option if a Public interface already exists', async () => {
104+
const mockPublicInterface = linodeInterfaceFactoryPublic.build();
105+
106+
server.use(
107+
http.get('*/linode/instances/:linodeId/interfaces', () => {
108+
return HttpResponse.json({
109+
interfaces: [mockPublicInterface],
110+
});
111+
})
112+
);
113+
114+
const { getByRole, findByRole, getByText } = renderWithTheme(
115+
<AddInterfaceForm {...props} />
116+
);
117+
118+
// Wait for the loading to complete and form to render
119+
await findByRole('radio', { name: 'VPC' });
120+
await userEvent.click(getByRole('radio', { name: 'VPC' }));
121+
expect(
122+
getByText(/This Linode already has a public interface/)
123+
).toBeVisible();
124+
});
125+
126+
it('should disable Public interface radio button if a Public interface already exists', async () => {
127+
const mockPublicInterface = linodeInterfaceFactoryPublic.build();
128+
129+
server.use(
130+
http.get('*/linode/instances/:linodeId/interfaces', () => {
131+
return HttpResponse.json({
132+
interfaces: [mockPublicInterface],
133+
});
134+
})
135+
);
136+
137+
const { getByRole, findByRole } = renderWithTheme(
138+
<AddInterfaceForm {...props} />
139+
);
140+
141+
// Wait for the loading to complete and form to render
142+
await findByRole('radio', { name: 'Public' });
143+
144+
expect(getByRole('radio', { name: 'Public' })).toBeDisabled();
145+
});
78146
});

packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { yupResolver } from '@hookform/resolvers/yup';
2-
import { useCreateLinodeInterfaceMutation } from '@linode/queries';
3-
import { Notice, Stack } from '@linode/ui';
2+
import {
3+
useCreateLinodeInterfaceMutation,
4+
useLinodeInterfacesQuery,
5+
} from '@linode/queries';
6+
import { Box, CircleProgress, Notice, Stack, Typography } from '@linode/ui';
47
import { useSnackbar } from 'notistack';
58
import React from 'react';
69
import { FormProvider, useForm } from 'react-hook-form';
@@ -10,6 +13,7 @@ import {
1013
getLinodeInterfacePayload,
1114
} from 'src/features/Linodes/LinodeCreate/Networking/utilities';
1215

16+
import { getLinodeInterfaceType } from '../utilities';
1317
import { Actions } from './Actions';
1418
import { InterfaceFirewall } from './InterfaceFirewall';
1519
import { InterfaceType } from './InterfaceType';
@@ -32,6 +36,14 @@ export const AddInterfaceForm = (props: Props) => {
3236

3337
const { mutateAsync } = useCreateLinodeInterfaceMutation(linodeId);
3438

39+
const { data: interfacesData, isPending: isLoadingInterfaces } =
40+
useLinodeInterfacesQuery(linodeId);
41+
42+
const existingInterfaces =
43+
interfacesData?.interfaces.map((networkInterface) =>
44+
getLinodeInterfaceType(networkInterface)
45+
) ?? [];
46+
const isPublicInterfacePresent = existingInterfaces.includes('Public');
3547
const form = useForm<CreateInterfaceFormValues>({
3648
defaultValues: {
3749
firewall_id: null,
@@ -78,6 +90,21 @@ export const AddInterfaceForm = (props: Props) => {
7890

7991
const selectedInterfacePurpose = form.watch('purpose');
8092

93+
if (isLoadingInterfaces) {
94+
return (
95+
<Box
96+
sx={{
97+
display: 'flex',
98+
alignItems: 'center',
99+
justifyContent: 'center',
100+
height: 'calc(100% - 100px)',
101+
}}
102+
>
103+
<CircleProgress size="md" />
104+
</Box>
105+
);
106+
}
107+
81108
return (
82109
<FormProvider {...form}>
83110
<form onSubmit={form.handleSubmit(onSubmit)}>
@@ -94,13 +121,30 @@ export const AddInterfaceForm = (props: Props) => {
94121
variant="error"
95122
/>
96123
)}
97-
<InterfaceType />
124+
<InterfaceType existingInterfaces={existingInterfaces} />
98125
{selectedInterfacePurpose === 'public' && <PublicInterface />}
99126
{selectedInterfacePurpose === 'vlan' && (
100127
<VLANInterface regionId={regionId} />
101128
)}
102129
{selectedInterfacePurpose === 'vpc' && (
103-
<VPCInterface regionId={regionId} />
130+
<Box>
131+
{isPublicInterfacePresent && (
132+
<Notice variant="warning">
133+
<Typography>
134+
This Linode already has a public interface. Having both a
135+
VPC interface and a public interface is not recommended. If
136+
you need public internet access, consider using the VPC’s
137+
<strong> Public access</strong> option instead.
138+
</Typography>
139+
<Typography paddingTop={2}>
140+
Each Linode includes one public IP address. To request
141+
additional public IPs, please note that they incur a monthly
142+
charge.
143+
</Typography>
144+
</Notice>
145+
)}
146+
<VPCInterface regionId={regionId} />
147+
</Box>
104148
)}
105149
{selectedInterfacePurpose !== 'vlan' && <InterfaceFirewall />}
106150
<Actions onClose={onClose} />

packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
FormHelperText,
66
Radio,
77
RadioGroup,
8+
Stack,
9+
TooltipIcon,
810
} from '@linode/ui';
911
import { useQueryClient } from '@tanstack/react-query';
1012
import { useSnackbar } from 'notistack';
@@ -13,10 +15,15 @@ import { useController, useFormContext } from 'react-hook-form';
1315

1416
import { getDefaultFirewallForInterfacePurpose } from 'src/features/Linodes/LinodeCreate/Networking/utilities';
1517

18+
import type { LinodeInterfaceType } from '../utilities';
1619
import type { CreateInterfaceFormValues } from './utilities';
1720
import type { InterfacePurpose } from '@linode/api-v4';
1821

19-
export const InterfaceType = () => {
22+
interface Props {
23+
existingInterfaces?: LinodeInterfaceType[] | null;
24+
}
25+
export const InterfaceType = (props: Props) => {
26+
const { existingInterfaces } = props;
2027
const queryClient = useQueryClient();
2128
const { enqueueSnackbar } = useSnackbar();
2229

@@ -72,7 +79,23 @@ export const InterfaceType = () => {
7279
sx={{ my: `0 !important` }}
7380
value={field.value ?? null}
7481
>
75-
<FormControlLabel control={<Radio />} label="Public" value="public" />
82+
<FormControlLabel
83+
control={<Radio />}
84+
disabled={existingInterfaces?.includes('Public')}
85+
label={
86+
<Stack alignItems="center" direction="row" spacing={0.5}>
87+
Public
88+
{existingInterfaces?.includes('Public') && (
89+
<TooltipIcon
90+
status="info"
91+
sxTooltipIcon={{ p: 0, ml: '8px !important' }}
92+
text="Each Linode can only have one public interface"
93+
/>
94+
)}
95+
</Stack>
96+
}
97+
value="public"
98+
/>
7699
<FormControlLabel control={<Radio />} label="VPC" value="vpc" />
77100
<FormControlLabel control={<Radio />} label="VLAN" value="vlan" />
78101
</RadioGroup>

0 commit comments

Comments
 (0)