Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-6757] - Create VPC page #9537

Merged
merged 28 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3d8c50e
add landing header for vpc create
coliu-akamai Aug 7, 2023
ccde6c1
initial page layout, still need all the logic + more styling
coliu-akamai Aug 8, 2023
8fe3003
the form persists :o
coliu-akamai Aug 9, 2023
3ba5beb
tiny bit of styling (honestly should wait til the end to do this)
coliu-akamai Aug 9, 2023
195f582
form logic
coliu-akamai Aug 10, 2023
fb867ba
mock service worker
coliu-akamai Aug 11, 2023
382c289
some unit tests, calculating subnet
coliu-akamai Aug 11, 2023
266943e
test updates
coliu-akamai Aug 14, 2023
862ef9e
add multipleSubnetInput wrapper (may potentially undo)
coliu-akamai Aug 14, 2023
5b634d2
unnecessary divider in v2 of subnet inputs
coliu-akamai Aug 14, 2023
4d1ddcd
there's a lot of issues with validation
coliu-akamai Aug 16, 2023
c9b2ea8
Merge branch 'develop' into feat-m3-6757
coliu-akamai Aug 16, 2023
800370d
updated validation
coliu-akamai Aug 16, 2023
82b52c6
update diabled factors
coliu-akamai Aug 16, 2023
3019395
Added changeset: VPC Create page
coliu-akamai Aug 16, 2023
2ebdafd
fix failing vpc create tests
coliu-akamai Aug 16, 2023
35e922d
address feedback
coliu-akamai Aug 16, 2023
4444772
address feedback (+ dev account working now :D)
coliu-akamai Aug 18, 2023
e11c4ba
fix failing tests after scrollErrorIntoView
coliu-akamai Aug 18, 2023
1f076a4
address feedback, update tests
coliu-akamai Aug 21, 2023
908d492
subnet ip default value
coliu-akamai Aug 21, 2023
384b81b
default subnet ip value when adding new subnets too
coliu-akamai Aug 21, 2023
df3b960
eslint and move constant to utility file
coliu-akamai Aug 21, 2023
7c479f5
Update packages/manager/src/features/VPC/VPCCreate/VPCCreate.tsx
coliu-akamai Aug 22, 2023
dc12bc1
Update packages/validation/.changeset/pr-9537-changed-1692640036055.md
coliu-akamai Aug 22, 2023
330c062
feedback + starting some tests
coliu-akamai Aug 22, 2023
0c0bbd5
finish tests + update availIPv4 state location due to react key entry
coliu-akamai Aug 23, 2023
13c58ce
make subnet errors show up @hana-linode
coliu-akamai Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export type AccountCapability =
| 'Vlans'
| 'Machine Images'
| 'LKE HA Control Planes'
| 'Managed Databases';
| 'Managed Databases'
| 'VPCs';

export interface AccountSettings {
managed: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

VPC Create page ([#9537](https://github.com/linode/manager/pull/9537))
2 changes: 1 addition & 1 deletion packages/manager/src/cachedData/regions.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import Grid from '@mui/material/Unstable_Grid2';
import { useTheme } from '@mui/material/styles';

import { Divider } from 'src/components/Divider';
import { Button } from 'src/components/Button/Button';
import { SubnetFieldState } from 'src/utilities/subnets';
import { SubnetNode } from './SubnetNode';

interface Props {
subnets: SubnetFieldState[];
onChange: (subnets: SubnetFieldState[]) => void;
disabled?: boolean;
}

export const MultipleSubnetInput = (props: Props) => {
const theme = useTheme();
const { subnets, onChange, disabled } = props;

const addSubnet = () => {
onChange([
...subnets,
{ label: '', labelError: '', ip: { ipv4: '', ipv4Error: '' } },
]);
};

const handleSubnetChange = (
subnet: SubnetFieldState,
subnetIdx: number,
removable: boolean
) => {
const newSubnets = [...subnets];
if (removable) {
newSubnets.splice(subnetIdx, 1);
} else {
newSubnets[subnetIdx] = subnet;
}
onChange(newSubnets);
};

return (
<Grid>
{subnets.map((subnet, subnetIdx) => (
<Grid key={`subnet-${subnetIdx}`}>
{subnetIdx !== 0 && <Divider sx={{ marginTop: theme.spacing(3) }} />}
<SubnetNode
idx={subnetIdx}
subnet={subnet}
disabled={disabled}
// janky solution to enable SubnetNode to work on its own or be part of MultipleSubnetInput
onChange={(subnet, subnetIdx, removable) =>
handleSubnetChange(subnet, subnetIdx ?? 0, !!removable)
}
Comment on lines +55 to +57
Copy link
Contributor Author

Choose a reason for hiding this comment

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

see comment at SubnetNode.tsx

removable={true}
/>
</Grid>
))}
<Button
buttonType="outlined"
disabled={disabled}
onClick={addSubnet}
sx={{ marginTop: theme.spacing(3) }}
>
Add a Subnet
</Button>
</Grid>
);
};
44 changes: 44 additions & 0 deletions packages/manager/src/features/VPC/VPCCreate/SubnetNode.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';

import { SubnetNode } from './SubnetNode';
import { renderWithTheme } from 'src/utilities/testHelpers';

describe('SubnetNode', () => {
it('should calculate the correct subnet mask', async () => {
renderWithTheme(
<SubnetNode
disabled={false}
idx={0}
onChange={() => {}}
subnet={{ label: '', ip: { ipv4: '' } }}
/>
);
const subnetAddress = screen.getAllByTestId('textfield-input');
expect(subnetAddress[1]).toBeInTheDocument();
await userEvent.type(subnetAddress[1], '192.0.0.0/24', { delay: 1 });

expect(subnetAddress[1]).toHaveValue('192.0.0.0/24');
const availIps = screen.getByText('Available IP Addresses: 252');
expect(availIps).toBeInTheDocument();
});

it('should not show a subnet mask for an ip without a mask', async () => {
renderWithTheme(
<SubnetNode
disabled={false}
idx={0}
onChange={() => {}}
subnet={{ label: '', ip: { ipv4: '' } }}
/>
);
const subnetAddress = screen.getAllByTestId('textfield-input');
expect(subnetAddress[1]).toBeInTheDocument();
await userEvent.type(subnetAddress[1], '192.0.0.0', { delay: 1 });

expect(subnetAddress[1]).toHaveValue('192.0.0.0');
const availIps = screen.queryByText('Available IP Addresses:');
expect(availIps).not.toBeInTheDocument();
});
});
112 changes: 112 additions & 0 deletions packages/manager/src/features/VPC/VPCCreate/SubnetNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from 'react';
import Grid from '@mui/material/Unstable_Grid2';
import { styled } from '@mui/material/styles';

import Close from '@mui/icons-material/Close';
import { Button } from 'src/components/Button/Button';
import { TextField } from 'src/components/TextField';
import { SubnetFieldState } from 'src/utilities/subnets';
import { FormHelperText } from 'src/components/FormHelperText';
import { determineIPType } from '@linode/validation';
import { calculateAvailableIpv4s } from 'src/utilities/subnets';

interface Props {
disabled?: boolean;
idx?: number;
// janky solution to enable SubnetNode to be an independent component or be part of MultipleSubnetInput
// (not the biggest fan tbh)
onChange: (
subnet: SubnetFieldState,
subnetIdx?: number,
remove?: boolean
) => void;
removable?: boolean;
Copy link
Contributor Author

@coliu-akamai coliu-akamai Aug 16, 2023

Choose a reason for hiding this comment

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

Trying to make SubnetNode its own component in order for it to have future use cases (like the edit/create Subnet drawers), but that led to packing the onChange method with some parameters that wouldn't be necessary (and some props, like removable) if this file was combined with MultiplleSubnetInput.tsx. The MultipleSubnetInput.tsx functionality is otherwise along the same idea as the MultipleIpInput component

subnet: SubnetFieldState;
}

const RESERVED_IP_NUMBER = 4;

// TODO: VPC - currently only supports IPv4, must update when/if IPv6 is also supported
export const SubnetNode = (props: Props) => {
const { disabled, idx, onChange, subnet, removable } = props;
const [availIps, setAvailIps] = React.useState<number | undefined>(undefined);

const onLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSubnet = {
...subnet,
label: e.target.value,
labelError: '',
};
onChange(newSubnet, idx);
};

const onIpv4Change = (e: React.ChangeEvent<HTMLInputElement>) => {
const ipType = determineIPType(e.target.value);
const newSubnet = {
...subnet,
ip: { ipv4: e.target.value },
};
setAvailIps(calculateAvailableIpv4s(e.target.value, ipType));
onChange(newSubnet, idx);
};

const removeSubnet = () => {
onChange(subnet, idx, removable);
};

return (
<Grid key={idx} sx={{ maxWidth: 460 }}>
<Grid
direction="row"
container
spacing={2}
>
<Grid xs={removable ? 11 : 12}>
<TextField
disabled={disabled}
label="Subnet label"
onChange={onLabelChange}
value={subnet.label}
errorText={subnet.labelError}
/>
</Grid>
{removable && !!idx && (
<Grid xs={1}>
<StyledButton onClick={removeSubnet}>
<Close data-testid={`delete-subnet-${idx}`} />
</StyledButton>
</Grid>
)}
</Grid>
<Grid xs={removable ? 11 : 12}>
<TextField
disabled={disabled}
label="Subnet IP Range Address"
onChange={onIpv4Change}
value={subnet.ip.ipv4}
errorText={subnet.ip.ipv4Error}
/>
{availIps && (
<FormHelperText>
Available IP Addresses:{' '}
{availIps > 4 ? availIps - RESERVED_IP_NUMBER : 0}
</FormHelperText>
)}
</Grid>
</Grid>
);
};

const StyledButton = styled(Button, { label: 'StyledButton' })(({ theme }) => ({
'& :hover, & :focus': {
backgroundColor: theme.color.grey2,
},
'& > span': {
padding: 2,
},
color: theme.textColors.tableHeader,
marginTop: theme.spacing(6),
minHeight: 'auto',
minWidth: 'auto',
padding: 0,
}));
107 changes: 107 additions & 0 deletions packages/manager/src/features/VPC/VPCCreate/VPCCreate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from 'react';
import { act } from 'react-dom/test-utils';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';

import VPCCreate from './VPCCreate';
import { renderWithTheme } from 'src/utilities/testHelpers';

beforeEach(() => {
// ignores the console errors in these tests as they're supposed to happen
jest.spyOn(console, 'error').mockImplementation(() => {});
});

describe('VPC create page', () => {
it('should render the vpc and subnet sections', () => {
const { getAllByText } = renderWithTheme(<VPCCreate />);

getAllByText('Region');
getAllByText('VPC label');
getAllByText('Select a Region');
getAllByText('Description');
getAllByText('Subnet');
getAllByText('Subnet label');
getAllByText('Subnet IP Range Address');
getAllByText('Add a Subnet');
getAllByText('Create VPC');
});

it('should require vpc labels and region and ignore subnets that are blank', async () => {
renderWithTheme(<VPCCreate />);
const createVPCButton = screen.getByText('Create VPC');
expect(createVPCButton).toBeInTheDocument();
await act(async () => {
userEvent.click(createVPCButton);
});

const regionError = screen.getByText('Region is required');
expect(regionError).toBeInTheDocument();
const labelError = screen.getByText('Label is required');
expect(labelError).toBeInTheDocument();
const badSubnetIP = screen.queryByText('The IPv4 range must be in CIDR format');
expect(badSubnetIP).not.toBeInTheDocument();
const badSubnetLabel = screen.queryByText('Label is required. Must only be ASCII letters, numbers, and dashes');
expect(badSubnetLabel).not.toBeInTheDocument();
});

it('should add and delete subnets correctly', async () => {
renderWithTheme(<VPCCreate />);
const addSubnet = screen.getByText('Add a Subnet');
expect(addSubnet).toBeInTheDocument();
await act(async () => {
userEvent.click(addSubnet);
});

const subnetLabels = screen.getAllByText('Subnet label');
const subnetIps = screen.getAllByText('Subnet IP Range Address');
expect(subnetLabels).toHaveLength(2);
expect(subnetIps).toHaveLength(2);

const deleteSubnet = screen.getByTestId('delete-subnet-1');
expect(deleteSubnet).toBeInTheDocument();
await act(async () => {
userEvent.click(deleteSubnet);
});

const subnetLabelAfter = screen.getAllByText('Subnet label');
const subnetIpsAfter = screen.getAllByText('Subnet IP Range Address');
expect(subnetLabelAfter).toHaveLength(1);
expect(subnetIpsAfter).toHaveLength(1);
});

it('should display that a subnet ip is invalid and require a subnet label if a user adds an invalid subnet ip', async () => {
renderWithTheme(<VPCCreate />);
const subnetLabel = screen.getByText('Subnet label');
expect(subnetLabel).toBeInTheDocument();
const subnetIp = screen.getByText('Subnet IP Range Address');
expect(subnetIp).toBeInTheDocument();
const createVPCButton = screen.getByText('Create VPC');
expect(createVPCButton).toBeInTheDocument();

await act(async () => {
await userEvent.type(subnetIp, 'bad ip', { delay: 1 });
userEvent.click(createVPCButton);
});
const badSubnetIP = screen.getByText('The IPv4 range must be in CIDR format');
expect(badSubnetIP).toBeInTheDocument();
const badSubnetLabel = screen.getByText('Label is required. Must only be ASCII letters, numbers, and dashes');
expect(badSubnetLabel).toBeInTheDocument();
});

it('should require a subnet ip if a subnet label has been changed', async () => {
renderWithTheme(<VPCCreate />);
const subnetLabel = screen.getByText('Subnet label');
expect(subnetLabel).toBeInTheDocument();
const createVPCButton = screen.getByText('Create VPC');
expect(createVPCButton).toBeInTheDocument();

await act(async () => {
await userEvent.type(subnetLabel, 'label', { delay: 1 });
userEvent.click(createVPCButton);
});
const badSubnetIP = screen.getByText('The IPv4 range must be in CIDR format');
expect(badSubnetIP).toBeInTheDocument();
const badSubnetLabel = screen.queryByText('Label is required. Must only be ASCII letters, numbers, and dashes');
expect(badSubnetLabel).not.toBeInTheDocument();
});
});
Loading