Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e65871b
initial commit
hasyed-akamai Jan 29, 2025
5f3f86f
populating IpV4 and ipV6
hasyed-akamai Jan 30, 2025
9b81d82
ACL Enable and IPv4 and IPv6 are added correctly
hasyed-akamai Feb 3, 2025
7388c0c
fix unit test and add IPv4 and IPv6 input for default case
hasyed-akamai Feb 3, 2025
c5b792d
cleanup
hasyed-akamai Feb 3, 2025
d96d617
Fix Cypress Test `Part-1`
hasyed-akamai Feb 4, 2025
e863e1e
Fix e2e Test `Part-2`
hasyed-akamai Feb 5, 2025
83be6c2
Remove Loading Bug
hasyed-akamai Feb 5, 2025
dfa1223
Fix k8_version initial value
hasyed-akamai Feb 6, 2025
f6e7795
Add Validation to the IPv4 and IPv6
hasyed-akamai Feb 6, 2025
f902c94
Set Validation errors and k8_version
hasyed-akamai Feb 6, 2025
5cdebc0
Add Error Scrollable Behaviour
hasyed-akamai Feb 6, 2025
e841372
Added changeset: Optimize CreateCluster component: Use React Hook Form
hasyed-akamai Feb 6, 2025
8768c89
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Feb 6, 2025
795d6ea
removed unused props and added updateFor prop
hasyed-akamai Feb 12, 2025
c1f15bb
Fixed issue of Multiple call of `ControlPlaneACLPane` component
hasyed-akamai Feb 12, 2025
7fb587b
disabled remove button for the first input
hasyed-akamai Feb 13, 2025
31a5987
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Feb 19, 2025
608bea6
Replaced useForm with useFormContext and add initialvalues for ipv4 a…
hasyed-akamai Feb 21, 2025
b2f5908
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Feb 21, 2025
935379d
Remove useState and useEffect
hasyed-akamai Feb 24, 2025
4ae1357
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Feb 24, 2025
a76ff57
fix unit test
hasyed-akamai Feb 24, 2025
d22b54f
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Feb 25, 2025
09fd694
remove isSubmitting useState
hasyed-akamai Feb 26, 2025
932bcf1
refactor `ControlPlaneACLPane`
hasyed-akamai Feb 28, 2025
1baaaa5
refactor `ControlPlaneACLPane` Part-2
hasyed-akamai Mar 4, 2025
a0676b5
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Mar 4, 2025
1e35fba
fix unit test
hasyed-akamai Mar 4, 2025
b6f7398
cleanup
hasyed-akamai Mar 4, 2025
847325a
Added default values in `ControlPlaneACLPane` test file
hasyed-akamai Mar 5, 2025
c9c9457
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
hasyed-akamai Mar 7, 2025
f599847
populate k8_versions for enterprise tier
hasyed-akamai Mar 7, 2025
84676ad
cleanup
hasyed-akamai Mar 10, 2025
955ed0a
Merge branch 'develop' into M3-8777-refactor-CreateCluster-component-…
Apr 9, 2025
2916d98
Revert "Merge branch 'develop' into M3-8777-refactor-CreateCluster-co…
Apr 9, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Optimize CreateCluster component: Use React Hook Form ([#11581](https://github.com/linode/manager/pull/11581))
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ describe('LKE Cluster Creation with ACL', () => {
.should('be.visible')
.should('be.enabled')
.click();
cy.get('[id="domain-transfer-ip-1"]')
cy.get('[id="ipv4-addresses-or-cidrs-ip-address-1"]')
.should('be.visible')
.click()
.type('10.0.1.0/24');
Expand Down
18 changes: 4 additions & 14 deletions packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,13 @@ describe('displays kubernetes plans panel based on availability', () => {

// Dedicated CPU tab
// Should be selected/open by default
// Should have the limited availability notice
// Should contain 5 plans (6 rows including the header row)
// Should have 3 plans disabled
// Should have no tooltips for the disabled plans (more than half disabled plans in the panel)
// All inputs for a row should be enabled if row is enabled (only testing one row in suite)
// All inputs for a disabled row should be disabled (only testing one row in suite)
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.limitedAvailability).should('be.visible');
cy.findAllByRole('alert').should('have.length', 0);
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 6);
Expand Down Expand Up @@ -319,14 +317,12 @@ describe('displays kubernetes plans panel based on availability', () => {
});

// High Memory tab
// Should have the limited availability notice
// Should contain 1 plan (2 rows including the header row)
// Should have one disabled plan
// Should have no tooltip for the disabled plan (more than half disabled plans in the panel)
cy.findByText('High Memory').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.limitedAvailability).should('be.visible');
cy.findAllByRole('alert').should('have.length', 0);

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
Expand All @@ -339,15 +335,13 @@ describe('displays kubernetes plans panel based on availability', () => {
});

// Premium CPU
// Should have the unavailable notice
// Only present since we manually inject the 512 plan for it
// Should contain 1 plan (2 rows including the header row)
// Should have its whole panel disabled
// Should not have tooltip for the disabled plan (not needed on disabled panels)
cy.findByText('Premium CPU').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
Expand Down Expand Up @@ -436,25 +430,21 @@ describe('displays specific kubernetes plans for GPU', () => {
cy.findByText('GPU').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 2);
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', {
name: 'List of NVIDIA RTX 4000 Ada Plans',
}).within(() => {
cy.findByText('NVIDIA RTX 4000 Ada').should('be.visible');
cy.findAllByRole('row').should('have.length', 2);
cy.get('[data-qa-plan-row="gpu-2 Ada"]').should(
'have.attr',
'disabled'
);
cy.get('[data-qa-plan-row="gpu-2 Ada"]').should('be.visible');
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
});

cy.findByRole('table', {
name: 'List of NVIDIA Quadro RTX 6000 Plans',
}).within(() => {
cy.findByText('NVIDIA Quadro RTX 6000').should('be.visible');
cy.findAllByRole('row').should('have.length', 2);
cy.get('[data-qa-plan-row="gpu-1"]').should('have.attr', 'disabled');
cy.get('[data-qa-plan-row="gpu-1"]').should('be.visible');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const props: ControlPlaneACLProps = {
errorText: undefined,
handleIPv4Change: vi.fn(),
handleIPv6Change: vi.fn(),
ipV4Addr: [{ address: '' }],
ipV6Addr: [{ address: '' }],
ipV4Addr: [''],
ipV6Addr: [''],
setControlPlaneACL: vi.fn(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@ import {
Box,
FormControl,
FormControlLabel,
IconButton,
Notice,
Stack,
TextField,
Toggle,
Typography,
} from '@linode/ui';
import CloseIcon from '@mui/icons-material/Close';
import { FormLabel } from '@mui/material';
import * as React from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';

import { ErrorMessage } from 'src/components/ErrorMessage';
import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput';
import { validateIPs } from 'src/utilities/ipUtils';

import type { ExtendedIP } from 'src/utilities/ipUtils';
import { LinkButton } from 'src/components/LinkButton';
import { stringToExtendedIP, validateIPs } from 'src/utilities/ipUtils';

export interface ControlPlaneACLProps {
enableControlPlaneACL: boolean;
errorText: string | undefined;
handleIPv4Change: (ips: ExtendedIP[]) => void;
handleIPv6Change: (ips: ExtendedIP[]) => void;
ipV4Addr: ExtendedIP[];
ipV6Addr: ExtendedIP[];
handleIPv4Change: (ips: string[]) => void;
handleIPv6Change: (ips: string[]) => void;
ipV4Addr: null | string[] | undefined;
ipV6Addr: null | string[] | undefined;
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
setControlPlaneACL: (enabled: boolean) => void;
}

Expand All @@ -31,11 +34,54 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => {
errorText,
handleIPv4Change,
handleIPv6Change,
ipV4Addr,
ipV6Addr,
setControlPlaneACL,
} = props;

const { clearErrors, control, setError, watch } = useForm();
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
const {
append: appendIPv4,
fields: ipv4Fields,
remove: removeIPv4,
} = useFieldArray({
control,
name: 'ipv4Addresses',
});
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated

const {
append: appendIPv6,
fields: ipv6Fields,
remove: removeIPv6,
} = useFieldArray({
control,
name: 'ipv6Addresses',
});

const ipv4Addresses = watch('ipv4Addresses');
const ipv6Addresses = watch('ipv6Addresses');

React.useEffect(() => {
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
if (enableControlPlaneACL) {
if (!ipv4Addresses || ipv4Addresses.length == 0) {
appendIPv4('');
}
if (!ipv6Addresses || ipv6Addresses.length == 0) {
appendIPv6('');
}
}
});

React.useEffect(() => {
if (ipv4Addresses && ipv4Addresses.length) {
handleIPv4Change(ipv4Addresses);
}
}, [ipv4Addresses, handleIPv4Change]);

React.useEffect(() => {
if (ipv6Addresses && ipv6Addresses.length) {
handleIPv6Change(ipv6Addresses);
}
}, [ipv6Addresses, handleIPv6Change]);
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated

return (
<>
<FormControl data-testid="control-plane-ipacl-form">
Expand Down Expand Up @@ -63,37 +109,121 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => {
label="Enable Control Plane ACL"
/>
</FormControl>

{enableControlPlaneACL && (
<Box sx={{ marginBottom: 3, maxWidth: 450 }}>
<MultipleIPInput
onBlur={(_ips: ExtendedIP[]) => {
const validatedIPs = validateIPs(_ips, {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv4 address.',
});
handleIPv4Change(validatedIPs);
}}
buttonText="Add IPv4 Address"
ips={ipV4Addr}
isLinkStyled
onChange={handleIPv4Change}
title="IPv4 Addresses or CIDRs"
/>
{/* IPv4 Addresses */}
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
<Box marginTop={2}>
<MultipleIPInput
onBlur={(_ips: ExtendedIP[]) => {
const validatedIPs = validateIPs(_ips, {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv6 address.',
});
handleIPv6Change(validatedIPs);
}}
buttonText="Add IPv6 Address"
ips={ipV6Addr}
isLinkStyled
onChange={handleIPv6Change}
title="IPv6 Addresses or CIDRs"
/>
<Typography mb={1} variant="inherit">
IPv4 Addresses or CIDRs
</Typography>
{ipv4Fields.map((field, index) => (
<Stack
alignItems="flex-start"
direction="row"
key={field.id}
spacing={0.5}
sx={{ marginBottom: 1 }}
>
<Controller
render={({ field: controllerField, fieldState }) => (
<TextField
{...controllerField}
onBlur={() => {
const _ips = stringToExtendedIP(controllerField.value);
const validatedIPs = validateIPs([_ips], {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv4 address.',
});
if (validatedIPs[0].error) {
setError(controllerField.name, {
message: validatedIPs[0].error,
type: 'manual',
});
} else {
clearErrors(controllerField.name);
}
}}
error={!!fieldState.error}
errorText={fieldState.error?.message}
hideLabel
label={`IPv4 Addresses or CIDRs ip-address-${index}`}
ref={null}
sx={{ minWidth: 350 }}
/>
)}
control={control}
name={`ipv4Addresses[${index}]`}
/>
<IconButton
aria-label={`Remove IPv4 Address ${index}`}
onClick={() => removeIPv4(index)}
sx={{ padding: 0.75 }}
>
<CloseIcon />
</IconButton>
Comment thread
hasyed-akamai marked this conversation as resolved.
Outdated
</Stack>
))}
<LinkButton onClick={() => appendIPv4('')}>
Add IPv4 Address
</LinkButton>
Comment thread
hana-akamai marked this conversation as resolved.
Outdated
</Box>

{/* IPv6 Addresses */}
<Box marginTop={2}>
<Typography mb={1} variant="inherit">
IPv6 Addresses or CIDRs
</Typography>
{ipv6Fields.map((field, index) => (
<Stack
alignItems="flex-start"
direction="row"
key={field.id}
spacing={0.5}
sx={{ marginBottom: 1 }}
>
<Controller
render={({ field: controllerField, fieldState }) => (
<TextField
{...controllerField}
onBlur={() => {
const _ips = stringToExtendedIP(controllerField.value);
const validatedIPs = validateIPs([_ips], {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv6 address.',
});
if (validatedIPs[0].error) {
setError(controllerField.name, {
message: validatedIPs[0].error,
type: 'manual',
});
} else {
clearErrors(controllerField.name);
}
}}
error={!!fieldState.error}
errorText={fieldState.error?.message}
hideLabel
label={`IPv6 Addresses or CIDRs ip-address-${index}`}
ref={null}
sx={{ minWidth: 350 }}
/>
)}
control={control}
name={`ipv6Addresses[${index}]`}
/>
<IconButton
aria-label={`Remove IPv6 Address ${index}`}
onClick={() => removeIPv6(index)}
sx={{ padding: 0.75 }}
>
<CloseIcon />
</IconButton>
</Stack>
))}
<LinkButton onClick={() => appendIPv6('')}>
Add IPv6 Address
</LinkButton>
</Box>
</Box>
)}
Expand Down
Loading