diff --git a/packages/manager/.changeset/pr-9848-upcoming-features-1698675578462.md b/packages/manager/.changeset/pr-9848-upcoming-features-1698675578462.md new file mode 100644 index 00000000000..027f79ae372 --- /dev/null +++ b/packages/manager/.changeset/pr-9848-upcoming-features-1698675578462.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Create Load Balancer flow - manage state ([#9848](https://github.com/linode/manager/pull/9848)) diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index 5c7e492b8a5..a2b15006d23 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -186,8 +186,12 @@ describe('Update database clusters', () => { cy.get('[data-qa-cluster-config]').within(() => { cy.findByText(configuration.region.label).should('be.visible'); - cy.findByText(database.used_disk_size_gb + " GB").should('be.visible'); - cy.findByText(database.total_disk_size_gb + " GB").should('be.visible'); + cy.findByText(database.used_disk_size_gb + ' GB').should( + 'be.visible' + ); + cy.findByText(database.total_disk_size_gb + ' GB').should( + 'be.visible' + ); }); cy.get('[data-qa-connection-details]').within(() => { diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerActionPanel.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerActionPanel.tsx new file mode 100644 index 00000000000..8a6c43caa6a --- /dev/null +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerActionPanel.tsx @@ -0,0 +1,27 @@ +import { useFormikContext } from 'formik'; +import * as React from 'react'; + +import { Box } from 'src/components/Box'; +import { Button } from 'src/components/Button/Button'; + +export const LoadBalancerActionPanel = () => { + const { submitForm } = useFormikContext(); + return ( + + + + + ); +}; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx index 014364fbcca..a7f01c1eed9 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.test.tsx @@ -18,7 +18,7 @@ describe('LoadBalancerConfiguration', () => { ) ).toBeNull(); expect( - screen.queryByText('TODO: AGLB - Implement Routes Confiugataion.') + screen.queryByText('TODO: AGLB - Implement Routes Configuration.') ).toBeNull(); expect(screen.getByText('Next: Service Targets')).toBeInTheDocument(); expect(screen.queryByText('Previous: Details')).toBeNull(); @@ -33,7 +33,7 @@ describe('LoadBalancerConfiguration', () => { screen.queryByText('TODO: AGLB - Implement Details step content.') ).toBeNull(); expect( - screen.queryByText('TODO: AGLB - Implement Routes Confiugataion.') + screen.queryByText('TODO: AGLB - Implement Routes Configuration.') ).toBeNull(); expect(screen.getByText('Next: Routes')).toBeInTheDocument(); expect(screen.getByText('Previous: Details')).toBeInTheDocument(); @@ -52,7 +52,7 @@ describe('LoadBalancerConfiguration', () => { ) ).toBeNull(); expect( - screen.getByText('TODO: AGLB - Implement Routes Confiugataion.') + screen.getByText('TODO: AGLB - Implement Routes Configuration.') ).toBeInTheDocument(); expect(screen.getByText('Previous: Service Targets')).toBeInTheDocument(); }); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.tsx index a9eadc0b2b2..3d6d3cccd9d 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerConfiguration.tsx @@ -17,7 +17,7 @@ export const configurationSteps = [ label: 'Service Targets', }, { - content:
TODO: AGLB - Implement Routes Confiugataion.
, + content:
TODO: AGLB - Implement Routes Configuration.
, handler: () => null, label: 'Routes', }, diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerCreate.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerCreate.tsx index c7b90a87917..0308f977e0a 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerCreate.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerCreate.tsx @@ -1,15 +1,23 @@ +import { CreateLoadBalancerSchema } from '@linode/validation'; import Stack from '@mui/material/Stack'; +import { Form, Formik } from 'formik'; import * as React from 'react'; -import { Box } from 'src/components/Box'; -import { Button } from 'src/components/Button/Button'; import { DocumentTitleSegment } from 'src/components/DocumentTitle/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; +import { LoadBalancerActionPanel } from './LoadBalancerActionPanel'; import { LoadBalancerConfiguration } from './LoadBalancerConfiguration'; import { LoadBalancerLabel } from './LoadBalancerLabel'; import { LoadBalancerRegions } from './LoadBalancerRegions'; +import type { CreateLoadbalancerPayload } from '@linode/api-v4'; + +const initialValues = { + label: '', + regions: [], +}; + const LoadBalancerCreate = () => { return ( <> @@ -26,35 +34,23 @@ const LoadBalancerCreate = () => { }} title="Create" /> - - null, - value: '', - }} - /> - - - {/* TODO: AGLB - - * Implement Review Load Balancer Action Behavior - * Implement Add Another Configuration Behavior - */} - - - - - + + onSubmit={(values, actions) => { + // TODO: AGLB - Implement form submit + // console.log('Values ', values); + }} + initialValues={initialValues} + validationSchema={CreateLoadBalancerSchema} + > +
+ + + + + + +
+ ); }; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.test.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.test.tsx index c99ba36bfe2..29c52f12122 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.test.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.test.tsx @@ -1,49 +1,74 @@ +import { fireEvent } from '@testing-library/react'; +import { Formik } from 'formik'; import React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { LoadBalancerLabel } from './LoadBalancerLabel'; +const loadBalancerLabelValue = 'Test Label'; +const loadBalancerTestId = 'textfield-input'; + +import type { CreateLoadbalancerPayload } from '@linode/api-v4'; + +type MockFormikContext = { + initialErrors?: {}; + initialTouched?: {}; + initialValues: CreateLoadbalancerPayload; +}; + +const initialValues = { + label: loadBalancerLabelValue, + regions: [], +}; + +const renderWithFormikWrapper = (mockFormikContext: MockFormikContext) => + renderWithTheme( + + + + ); + describe('LoadBalancerLabel', () => { it('should render the component with a label and no error', () => { - const labelFieldProps = { - disabled: false, - errorText: '', - label: 'Load Balancer Label', - onChange: jest.fn(), - value: 'Test Label', - }; - - const { getByTestId, queryByText } = renderWithTheme( - - ); - - const labelInput = getByTestId('textfield-input'); + const { getByTestId, queryByText } = renderWithFormikWrapper({ + initialValues, + }); + + const labelInput = getByTestId(loadBalancerTestId); const errorNotice = queryByText('Error Text'); expect(labelInput).toBeInTheDocument(); expect(labelInput).toHaveAttribute('placeholder', 'Enter a label'); - expect(labelInput).toHaveValue('Test Label'); + expect(labelInput).toHaveValue(loadBalancerLabelValue); expect(errorNotice).toBeNull(); }); it('should render the component with an error message', () => { - const labelFieldProps = { - disabled: false, - errorText: 'This is an error', - label: 'Load Balancer Label', - onChange: jest.fn(), - value: 'Test Label', - }; - - const { getByTestId, getByText } = renderWithTheme( - - ); - - const labelInput = getByTestId('textfield-input'); + const { getByTestId, getByText } = renderWithFormikWrapper({ + initialErrors: { label: 'This is an error' }, + initialTouched: { label: true }, + initialValues, + }); + + const labelInput = getByTestId(loadBalancerTestId); const errorNotice = getByText('This is an error'); expect(labelInput).toBeInTheDocument(); expect(errorNotice).toBeInTheDocument(); }); + + it('should update formik values on input change', () => { + const { getByTestId } = renderWithFormikWrapper({ + initialValues, + }); + + const labelInput = getByTestId(loadBalancerTestId); + + // Simulate typing 'New Label' in the input field + fireEvent.change(labelInput, { target: { value: 'New Label' } }); + + // Expect the input to have the new value + expect(labelInput).toHaveValue('New Label'); + }); }); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.tsx index 1bff990ab83..f385010e44d 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.tsx @@ -1,16 +1,18 @@ +import { useFormikContext } from 'formik'; import * as React from 'react'; -import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; -import { TextField, TextFieldProps } from 'src/components/TextField'; +import { TextField } from 'src/components/TextField'; -interface LabelProps { - error?: string; - labelFieldProps: TextFieldProps; -} +import type { CreateLoadbalancerPayload } from '@linode/api-v4'; -export const LoadBalancerLabel = (props: LabelProps) => { - const { error, labelFieldProps } = props; +export const LoadBalancerLabel = () => { + const { + errors, + handleChange, + touched, + values, + } = useFormikContext(); return ( { }} data-qa-label-header > - {error && } labelFieldProps.onChange} + onChange={handleChange} placeholder="Enter a label" - value={labelFieldProps.value} + value={values?.label} /> ); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerRegions.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerRegions.tsx index e1242d97fda..e5d999d8ed4 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerRegions.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerRegions.tsx @@ -7,7 +7,7 @@ import { Flag } from 'src/components/Flag'; import { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; -const regions = [ +const loadBalancerRegions = [ { country: 'us', id: 'us-iad', label: 'Washington, DC' }, { country: 'us', id: 'us-lax', label: 'Los Angeles, CA' }, { country: 'fr', id: 'fr-par', label: 'Paris, FR' }, @@ -31,7 +31,7 @@ export const LoadBalancerRegions = () => { - {regions.map((region) => ( + {loadBalancerRegions.map((region) => ( val !== 'http' && val !== 'tcp', + then: array().of(certificateConfigSchema).required(), + otherwise: array().strip(), + }), + routes: string().when('protocol', { + is: 'tcp', + then: array() + .of( + object({ + label: string().required(), + protocol: string().oneOf(['tcp']).required(), + rules: array().of(CreateLoadBalancerRuleSchema).required(), + }) + ) + .required(), + otherwise: array() + .of( + object().shape({ + label: string().required(), + protocol: string().oneOf(['http']).required(), + rules: array().of(CreateLoadBalancerRuleSchema).required(), + }) + ) + .required(), + }), +}); +export const CreateLoadBalancerSchema = object({ + label: string() + .matches( + /^[a-zA-Z0-9.\-_]+$/, + 'Label may only contain letters, numbers, periods, dashes, and underscores.' + ) + .required(LABEL_REQUIRED), + tags: array().of(string()), // TODO: AGLB - Should confirm on this with API team. Assuming this will be out of scope for Beta. + regions: array().of(string()).required(), + configurations: array().of(ConfigurationSchema), +}); + /** * TODO: AGLB - remove this create schema */