Skip to content

Commit

Permalink
refactor: [M3-8814] - Clean up SubnetCreateDrawer and fix animation f…
Browse files Browse the repository at this point in the history
…or VPC subnet drawers (#11195)

* new subnet node?

* subnet node - i think I like this version more?

* update spacing of bottom margins for notices

* switch to remove two setValues

* Added changeset: Convert from `formik` to `react-hook-form` for `SubnetCreateDrawer`

* Update packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx

Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>

* remove unnecessary grid + update variable name

* fix animation for VPC subnet drawers

* Added changeset: Animation for VPC subnet drawers

* fix cypress tests

---------

Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>
  • Loading branch information
coliu-akamai and bnussman-akamai authored Nov 4, 2024
1 parent 6de4781 commit 8c3c7fd
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 119 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11195-fixed-1730745532480.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Animation for VPC subnet drawers ([#11195](https://github.com/linode/manager/pull/11195))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Convert from `formik` to `react-hook-form` for `SubnetCreateDrawer` ([#11195](https://github.com/linode/manager/pull/11195))
10 changes: 4 additions & 6 deletions packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('VPC assign/unassign flows', () => {
mockGetVPC(mockVPC).as('getVPC');
mockGetSubnets(mockVPC.id, []).as('getSubnets');
mockCreateSubnet(mockVPC.id).as('createSubnet');
mockGetLinodes([mockLinode]).as('getLinodes');

cy.visitWithLogin(`/vpcs/${mockVPC.id}`);
cy.wait(['@getVPC', '@getSubnets']);
Expand Down Expand Up @@ -110,7 +111,7 @@ describe('VPC assign/unassign flows', () => {
.click();
});

cy.wait(['@createSubnet', '@getVPC', '@getSubnets']);
cy.wait(['@createSubnet', '@getVPC', '@getSubnets', '@getLinodes']);

// confirm that newly created subnet should now appear on VPC's detail page
cy.findByText(mockVPC.label).should('be.visible');
Expand All @@ -123,12 +124,10 @@ describe('VPC assign/unassign flows', () => {
.should('be.visible')
.click();

mockGetLinodes([mockLinode]).as('getLinodes');
ui.actionMenuItem
.findByTitle('Assign Linodes')
.should('be.visible')
.click();
cy.wait('@getLinodes');

ui.drawer
.findByTitle(`Assign Linodes to subnet: ${mockSubnet.label} (0.0.0.0/0)`)
Expand Down Expand Up @@ -224,9 +223,10 @@ describe('VPC assign/unassign flows', () => {
mockGetVPCs(mockVPCs).as('getVPCs');
mockGetVPC(mockVPC).as('getVPC');
mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets');
mockGetLinodes([mockLinode, mockSecondLinode]).as('getLinodes');

cy.visitWithLogin(`/vpcs/${mockVPC.id}`);
cy.wait(['@getVPC', '@getSubnets']);
cy.wait(['@getVPC', '@getSubnets', '@getLinodes']);

// confirm that subnet should get displayed on VPC's detail page
cy.findByText(mockVPC.label).should('be.visible');
Expand All @@ -239,12 +239,10 @@ describe('VPC assign/unassign flows', () => {
.should('be.visible')
.click();

mockGetLinodes([mockLinode, mockSecondLinode]).as('getLinodes');
ui.actionMenuItem
.findByTitle('Unassign Linodes')
.should('be.visible')
.click();
cy.wait('@getLinodes');

ui.drawer
.findByTitle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { Button } from 'src/components/Button/Button';
import { Divider } from 'src/components/Divider';
import {
DEFAULT_SUBNET_IPV4_VALUE,
SubnetFieldState,
getRecommendedSubnetIPv4,
} from 'src/utilities/subnets';

import { SubnetNode } from './SubnetNode';

import type { SubnetFieldState } from 'src/utilities/subnets';

interface Props {
disabled?: boolean;
isDrawer?: boolean;
Expand Down
149 changes: 90 additions & 59 deletions packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { FormHelperText } from '@linode/ui';
import { createSubnetSchema } from '@linode/validation';
import { useFormik } from 'formik';
import * as React from 'react';
import { Controller, useForm } from 'react-hook-form';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import { Notice } from 'src/components/Notice/Notice';
import { Stack } from 'src/components/Stack';
import { TextField } from 'src/components/TextField';
import { useGrants, useProfile } from 'src/queries/profile/profile';
import { useCreateSubnetMutation, useVPCQuery } from 'src/queries/vpcs/vpcs';
import { getErrorMap } from 'src/utilities/errorUtils';
import {
DEFAULT_SUBNET_IPV4_VALUE,
RESERVED_IP_NUMBER,
calculateAvailableIPv4sRFC1918,
getRecommendedSubnetIPv4,
} from 'src/utilities/subnets';

import { SubnetNode } from '../VPCCreate/SubnetNode';

import type { SubnetFieldState } from 'src/utilities/subnets';
import type { CreateSubnetPayload } from '@linode/api-v4';

interface Props {
onClose: () => void;
Expand All @@ -37,86 +40,114 @@ export const SubnetCreateDrawer = (props: Props) => {
vpc?.subnets?.map((subnet) => subnet.ipv4 ?? '') ?? []
);

const [errorMap, setErrorMap] = React.useState<
Record<string, string | undefined>
>({});

const {
isPending,
mutateAsync: createSubnet,
reset,
reset: resetRequest,
} = useCreateSubnetMutation(vpcId);

const onCreateSubnet = async () => {
const {
control,
formState: { errors, isDirty, isSubmitting },
handleSubmit,
reset: resetForm,
setError,
watch,
} = useForm<CreateSubnetPayload>({
defaultValues: {
ipv4: recommendedIPv4,
label: '',
},
mode: 'onBlur',
resolver: yupResolver(createSubnetSchema),
});

const ipv4 = watch('ipv4');
const numberOfAvailableIPs = calculateAvailableIPv4sRFC1918(ipv4 ?? '');

const onCreateSubnet = async (values: CreateSubnetPayload) => {
try {
await createSubnet({ ipv4: values.ip.ipv4, label: values.label });
await createSubnet(values);
onClose();
} catch (errors) {
const newErrors = getErrorMap(['label', 'ipv4'], errors);
setErrorMap(newErrors);
setValues({
ip: {
...values.ip,
ipv4Error: newErrors.ipv4,
},
label: values.label,
labelError: newErrors.label,
});
for (const error of errors) {
setError(error?.field ?? 'root', { message: error.reason });
}
}
};

const { dirty, handleSubmit, resetForm, setValues, values } = useFormik({
enableReinitialize: true,
initialValues: {
ip: {
availIPv4s: 256,
ipv4: recommendedIPv4,
},
// @TODO VPC: add IPv6 when that is supported
label: '',
} as SubnetFieldState,
onSubmit: onCreateSubnet,
validateOnBlur: false,
validateOnChange: false,
validationSchema: createSubnetSchema,
});

React.useEffect(() => {
if (open) {
resetForm();
reset();
setErrorMap({});
}
}, [open, reset, resetForm]);

return (
<Drawer onClose={onClose} open={open} title={'Create Subnet'}>
{errorMap.none && <Notice text={errorMap.none} variant="error" />}
<Drawer
onExited={() => {
resetForm();
resetRequest();
}}
onClose={onClose}
open={open}
title={'Create Subnet'}
>
{errors.root?.message && (
<Notice spacingBottom={8} text={errors.root.message} variant="error" />
)}
{userCannotAddSubnet && (
<Notice
text={
"You don't have permissions to create a new Subnet. Please contact an account administrator for details."
}
important
spacingBottom={8}
spacingTop={16}
variant="error"
/>
)}
<form onSubmit={handleSubmit}>
<SubnetNode
onChange={(subnetState) => {
setValues(subnetState);
}}
disabled={userCannotAddSubnet}
subnet={values}
/>
<form onSubmit={handleSubmit(onCreateSubnet)}>
<Stack>
<Controller
render={({ field, fieldState }) => (
<TextField
aria-label="Enter a subnet label"
disabled={userCannotAddSubnet}
errorText={fieldState.error?.message}
label="Subnet Label"
onBlur={field.onBlur}
onChange={field.onChange}
placeholder="Enter a subnet label"
value={field.value}
/>
)}
control={control}
name="label"
/>
<Controller
render={({ field, fieldState }) => (
<TextField
aria-label="Enter an IPv4"
disabled={userCannotAddSubnet}
errorText={fieldState.error?.message}
label="Subnet IP Address Range"
onBlur={field.onBlur}
onChange={field.onChange}
value={field.value}
/>
)}
control={control}
name="ipv4"
/>
{numberOfAvailableIPs && (
<FormHelperText>
Number of Available IP Addresses:{' '}
{numberOfAvailableIPs > RESERVED_IP_NUMBER
? (numberOfAvailableIPs - RESERVED_IP_NUMBER).toLocaleString()
: 0}
</FormHelperText>
)}
</Stack>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'create-subnet-drawer-button',
disabled: !dirty || userCannotAddSubnet,
disabled: !isDirty || userCannotAddSubnet,
label: 'Create Subnet',
loading: isPending,
onClick: onCreateSubnet,
loading: isPending || isSubmitting,
type: 'submit',
}}
secondaryButtonProps={{ label: 'Cancel', onClick: onClose }}
Expand Down
94 changes: 41 additions & 53 deletions packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,11 @@ export const VPCSubnetsTable = (props: Props) => {
Create Subnet
</Button>
</Box>
{subnetCreateDrawerOpen && (
<SubnetCreateDrawer
onClose={() => setSubnetCreateDrawerOpen(false)}
open={subnetCreateDrawerOpen}
vpcId={vpcId}
/>
)}
<SubnetCreateDrawer
onClose={() => setSubnetCreateDrawerOpen(false)}
open={subnetCreateDrawerOpen}
vpcId={vpcId}
/>
<CollapsibleTable
TableRowEmpty={
<TableRowEmpty colSpan={5} message={'No Subnets are assigned.'} />
Expand All @@ -318,52 +316,42 @@ export const VPCSubnetsTable = (props: Props) => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
{subnetUnassignLinodesDrawerOpen && (
<SubnetUnassignLinodesDrawer
onClose={() => {
setSubnetUnassignLinodesDrawerOpen(false);
setSelectedLinode(undefined);
}}
open={subnetUnassignLinodesDrawerOpen}
singleLinodeToBeUnassigned={selectedLinode}
subnet={selectedSubnet}
vpcId={vpcId}
/>
)}
{subnetAssignLinodesDrawerOpen && (
<SubnetAssignLinodesDrawer
onClose={() => setSubnetAssignLinodesDrawerOpen(false)}
open={subnetAssignLinodesDrawerOpen}
subnet={selectedSubnet}
vpcId={vpcId}
vpcRegion={vpcRegion}
/>
)}
{deleteSubnetDialogOpen && (
<SubnetDeleteDialog
onClose={() => setDeleteSubnetDialogOpen(false)}
open={deleteSubnetDialogOpen}
subnet={selectedSubnet}
vpcId={vpcId}
/>
)}
{editSubnetsDrawerOpen && (
<SubnetEditDrawer
onClose={() => setEditSubnetsDrawerOpen(false)}
open={editSubnetsDrawerOpen}
subnet={selectedSubnet}
vpcId={vpcId}
/>
)}
{powerActionDialogOpen && (
<PowerActionsDialog
action={linodePowerAction ?? 'Reboot'}
isOpen={powerActionDialogOpen}
linodeId={selectedLinode?.id}
linodeLabel={selectedLinode?.label}
onClose={() => setPowerActionDialogOpen(false)}
/>
)}
<SubnetUnassignLinodesDrawer
onClose={() => {
setSubnetUnassignLinodesDrawerOpen(false);
setSelectedLinode(undefined);
}}
open={subnetUnassignLinodesDrawerOpen}
singleLinodeToBeUnassigned={selectedLinode}
subnet={selectedSubnet}
vpcId={vpcId}
/>
<SubnetAssignLinodesDrawer
onClose={() => setSubnetAssignLinodesDrawerOpen(false)}
open={subnetAssignLinodesDrawerOpen}
subnet={selectedSubnet}
vpcId={vpcId}
vpcRegion={vpcRegion}
/>
<SubnetDeleteDialog
onClose={() => setDeleteSubnetDialogOpen(false)}
open={deleteSubnetDialogOpen}
subnet={selectedSubnet}
vpcId={vpcId}
/>
<SubnetEditDrawer
onClose={() => setEditSubnetsDrawerOpen(false)}
open={editSubnetsDrawerOpen}
subnet={selectedSubnet}
vpcId={vpcId}
/>
<PowerActionsDialog
action={linodePowerAction ?? 'Reboot'}
isOpen={powerActionDialogOpen}
linodeId={selectedLinode?.id}
linodeLabel={selectedLinode?.label}
onClose={() => setPowerActionDialogOpen(false)}
/>
</>
);
};
Expand Down

0 comments on commit 8c3c7fd

Please sign in to comment.