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

refactor: [M3-8814] - Clean up SubnetCreateDrawer and fix animation for VPC subnet drawers #11195

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
83 changes: 83 additions & 0 deletions packages/manager/src/features/VPCs/VPCCreate/NewSubnetNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { FormHelperText } from '@linode/ui';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';

import { TextField } from 'src/components/TextField';
import {
RESERVED_IP_NUMBER,
calculateAvailableIPv4sRFC1918,
} from 'src/utilities/subnets';

import type { CreateSubnetPayload } from '@linode/api-v4';

/**
TODO: replace the current SubnetNode when VPC Create is refactored
I'm currently thinking about making a RemovableSubnetNode to wrap around
this for VPCCreate instead of having a bunch of optional props (current
state of affairs for SubnetNode) or just using this as is inside MultipleSubnetInput
(but the thinking might change when I start refactoring VPCCreate
and VPCCreateDrawer)
*/
export interface NewSubnetNodeProps {
disabled?: boolean;
ipv4Error: string | undefined;
labelError: string | undefined;
onChange: (subnet: CreateSubnetPayload) => void;
subnet: CreateSubnetPayload;
}

// @TODO VPC: currently only supports IPv4, must update when/if IPv6 is also supported
export const NewSubnetNode = (props: NewSubnetNodeProps) => {
const { disabled, ipv4Error, labelError, onChange, subnet } = props;

const availIPs = calculateAvailableIPv4sRFC1918(subnet.ipv4 ?? '');

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

const onIpv4Change = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSubnet = {
...subnet,
ipv4: e.target.value,
};
onChange(newSubnet);
};

return (
<Grid sx={{ flexGrow: 1, maxWidth: 460 }}>
<Stack>
<TextField
aria-label="Enter a subnet label"
disabled={disabled}
errorText={labelError}
label="Subnet Label"
onChange={onLabelChange}
placeholder="Enter a subnet label"
value={subnet.label}
/>
<TextField
aria-label="Enter an IPv4"
disabled={disabled}
errorText={ipv4Error}
label="Subnet IP Address Range"
onChange={onIpv4Change}
value={subnet.ipv4}
/>
{availIPs && (
<FormHelperText>
Number of Available IP Addresses:{' '}
{availIPs > RESERVED_IP_NUMBER
? (availIPs - RESERVED_IP_NUMBER).toLocaleString()
: 0}
</FormHelperText>
)}
</Stack>
</Grid>
);
};
109 changes: 55 additions & 54 deletions packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { createSubnetSchema } from '@linode/validation';
import { useFormik } from 'formik';
import * as React from 'react';
import { 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 { 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,
getRecommendedSubnetIPv4,
} from 'src/utilities/subnets';

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

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

interface Props {
onClose: () => void;
Expand All @@ -37,86 +37,87 @@ 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 {
formState: { errors, isDirty, isSubmitting },
handleSubmit,
reset,
setError,
setValue,
watch,
} = useForm<CreateSubnetPayload>({
defaultValues: {
ipv4: recommendedIPv4,
label: '',
},
mode: 'onBlur',
resolver: yupResolver(createSubnetSchema),
});

const values = watch();

const onCreateSubnet = async () => {
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={() => {
reset();
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);
<form onSubmit={handleSubmit(onCreateSubnet)}>
<NewSubnetNode
onChange={(subnet) => {
setValue('label', subnet.label, {
shouldDirty: true,
shouldValidate: true,
});
setValue('ipv4', subnet.ipv4, {
shouldDirty: true,
shouldValidate: true,
});
coliu-akamai marked this conversation as resolved.
Show resolved Hide resolved
}}
disabled={userCannotAddSubnet}
ipv4Error={errors.ipv4?.message}
labelError={errors.label?.message}
subnet={values}
/>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'create-subnet-drawer-button',
disabled: !dirty || userCannotAddSubnet,
disabled: !isDirty || userCannotAddSubnet,
label: 'Create Subnet',
loading: isPending,
onClick: onCreateSubnet,
loading: isSubmitting,
type: 'submit',
}}
secondaryButtonProps={{ label: 'Cancel', onClick: onClose }}
Expand Down