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
*/