diff --git a/packages/manager/.changeset/pr-11195-fixed-1730745532480.md b/packages/manager/.changeset/pr-11195-fixed-1730745532480.md new file mode 100644 index 00000000000..9a7e1e0ad9f --- /dev/null +++ b/packages/manager/.changeset/pr-11195-fixed-1730745532480.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Animation for VPC subnet drawers ([#11195](https://github.com/linode/manager/pull/11195)) diff --git a/packages/manager/.changeset/pr-11195-tech-stories-1730487840259.md b/packages/manager/.changeset/pr-11195-tech-stories-1730487840259.md new file mode 100644 index 00000000000..6f78202aaf5 --- /dev/null +++ b/packages/manager/.changeset/pr-11195-tech-stories-1730487840259.md @@ -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)) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index a4ac2977d21..aeada2e14d3 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -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']); @@ -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'); @@ -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)`) @@ -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'); @@ -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( diff --git a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx index 23c3f1b42eb..7c5dc9d88e5 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx @@ -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; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx index 0eaf06dca68..d3d1326a69a 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx @@ -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; @@ -37,86 +40,114 @@ export const SubnetCreateDrawer = (props: Props) => { vpc?.subnets?.map((subnet) => subnet.ipv4 ?? '') ?? [] ); - const [errorMap, setErrorMap] = React.useState< - Record - >({}); - const { isPending, mutateAsync: createSubnet, - reset, + reset: resetRequest, } = useCreateSubnetMutation(vpcId); - const onCreateSubnet = async () => { + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + reset: resetForm, + setError, + watch, + } = useForm({ + 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 ( - - {errorMap.none && } + { + resetForm(); + resetRequest(); + }} + onClose={onClose} + open={open} + title={'Create Subnet'} + > + {errors.root?.message && ( + + )} {userCannotAddSubnet && ( )} -
- { - setValues(subnetState); - }} - disabled={userCannotAddSubnet} - subnet={values} - /> + + + ( + + )} + control={control} + name="label" + /> + ( + + )} + control={control} + name="ipv4" + /> + {numberOfAvailableIPs && ( + + Number of Available IP Addresses:{' '} + {numberOfAvailableIPs > RESERVED_IP_NUMBER + ? (numberOfAvailableIPs - RESERVED_IP_NUMBER).toLocaleString() + : 0} + + )} + { Create Subnet - {subnetCreateDrawerOpen && ( - setSubnetCreateDrawerOpen(false)} - open={subnetCreateDrawerOpen} - vpcId={vpcId} - /> - )} + setSubnetCreateDrawerOpen(false)} + open={subnetCreateDrawerOpen} + vpcId={vpcId} + /> @@ -318,52 +316,42 @@ export const VPCSubnetsTable = (props: Props) => { page={pagination.page} pageSize={pagination.pageSize} /> - {subnetUnassignLinodesDrawerOpen && ( - { - setSubnetUnassignLinodesDrawerOpen(false); - setSelectedLinode(undefined); - }} - open={subnetUnassignLinodesDrawerOpen} - singleLinodeToBeUnassigned={selectedLinode} - subnet={selectedSubnet} - vpcId={vpcId} - /> - )} - {subnetAssignLinodesDrawerOpen && ( - setSubnetAssignLinodesDrawerOpen(false)} - open={subnetAssignLinodesDrawerOpen} - subnet={selectedSubnet} - vpcId={vpcId} - vpcRegion={vpcRegion} - /> - )} - {deleteSubnetDialogOpen && ( - setDeleteSubnetDialogOpen(false)} - open={deleteSubnetDialogOpen} - subnet={selectedSubnet} - vpcId={vpcId} - /> - )} - {editSubnetsDrawerOpen && ( - setEditSubnetsDrawerOpen(false)} - open={editSubnetsDrawerOpen} - subnet={selectedSubnet} - vpcId={vpcId} - /> - )} - {powerActionDialogOpen && ( - setPowerActionDialogOpen(false)} - /> - )} + { + setSubnetUnassignLinodesDrawerOpen(false); + setSelectedLinode(undefined); + }} + open={subnetUnassignLinodesDrawerOpen} + singleLinodeToBeUnassigned={selectedLinode} + subnet={selectedSubnet} + vpcId={vpcId} + /> + setSubnetAssignLinodesDrawerOpen(false)} + open={subnetAssignLinodesDrawerOpen} + subnet={selectedSubnet} + vpcId={vpcId} + vpcRegion={vpcRegion} + /> + setDeleteSubnetDialogOpen(false)} + open={deleteSubnetDialogOpen} + subnet={selectedSubnet} + vpcId={vpcId} + /> + setEditSubnetsDrawerOpen(false)} + open={editSubnetsDrawerOpen} + subnet={selectedSubnet} + vpcId={vpcId} + /> + setPowerActionDialogOpen(false)} + /> ); };