Skip to content

Commit

Permalink
feat: [M3-7292] - Stepper component - Details content (#9849)
Browse files Browse the repository at this point in the history
* feat: [M3-7311] - Create Load Balancer flow - Mange state.

* Code cleanup

* Added changeset: Create Load Balancer flow - Mange state.

* Update pr-9848-upcoming-features-1698675578462.md

* feat: [M3-7292] - Stepper component - Details content

* Added changeset: Stepper component - Details content

* Update update-database.spec.ts

* Update packages/manager/.changeset/pr-9848-upcoming-features-1698675578462.md

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

* Fix typos

* Update LoadBalancerConfiguration.test.tsx

* use useFormikContext instead of custom state management hook

* PR feedback

* Code cleanup

* Update loadbalancers.schema.ts

* Update packages/validation/src/loadbalancers.schema.ts

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

* Update packages/validation/src/loadbalancers.schema.ts

Co-authored-by: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com>

* Update loadbalancers.schema.ts

* schema dependency issue.

* Resolve type issues

* Fix Formik issues

* update tests

* Delete pr-9848-upcoming-features-1698675578462.md

* PR - feedback use CreateLoadbalancerPayload

* PR - feedback error handling

* PR - Feedback use full type

* PR - feedback

* Code cleanup, PR feedback

* Code cleanup

* Create global test helper to wrap with formik and theme - renderWithThemeAndFormik

* code cleanup

* Update packages/manager/src/utilities/testHelpers.tsx

Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>

* Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate/LoadBalancerLabel.tsx

Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>

* PR - feedback

* Added latest copy

* Update LoadBalancerConfiguration.test.tsx

* Add learn more link

* Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate/ConfigurationDetails.tsx

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

* Update packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Configurations/constants.tsx

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

---------

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>
Co-authored-by: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com>
Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 1, 2023
1 parent 6dd45ff commit acf3f2e
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 94 deletions.
4 changes: 3 additions & 1 deletion packages/api-v4/src/aglb/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface UpdateLoadbalancerPayload {
configuration_ids?: number[];
}

type Protocol = 'tcp' | 'http' | 'https';
export type Protocol = 'tcp' | 'http' | 'https';

type RouteProtocol = 'tcp' | 'http';

Expand All @@ -46,6 +46,7 @@ export type MatchField = 'path_prefix' | 'query' | 'host' | 'header' | 'method';

export interface RoutePayload {
label: string;
protocol: Protocol;
rules: RuleCreatePayload[];
}

Expand Down Expand Up @@ -145,6 +146,7 @@ export interface RouteServiceTargetPayload {
export interface ServiceTargetPayload {
label: string;
protocol: Protocol;
percentage: number;
endpoints: Endpoint[];
certificate_id: number | null;
load_balancing_policy: Policy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Stepper component - Details content ([#9849](https://github.com/linode/manager/pull/9849))
5 changes: 5 additions & 0 deletions packages/manager/src/factories/aglb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
UpdateLoadbalancerPayload,
} from '@linode/api-v4/lib/aglb/types';
import * as Factory from 'factory.ts';

import { pickRandom } from 'src/utilities/random';

export const mockCertificate = `
Expand Down Expand Up @@ -119,6 +120,7 @@ export const createLoadbalancerWithAllChildrenFactory = Factory.Sync.makeFactory
routes: [
{
label: 'my-route',
protocol: 'tcp',
rules: [
{
match_condition: {
Expand Down Expand Up @@ -149,6 +151,7 @@ export const createLoadbalancerWithAllChildrenFactory = Factory.Sync.makeFactory
},
label: 'my-service-target',
load_balancing_policy: 'round_robin',
percentage: 0,
protocol: 'https',
},
],
Expand Down Expand Up @@ -279,6 +282,7 @@ export const serviceTargetFactory = Factory.Sync.makeFactory<ServiceTarget>({
id: Factory.each((i) => i),
label: Factory.each((i) => `service-target-${i}`),
load_balancing_policy: 'round_robin',
percentage: 0,
protocol: 'https',
});

Expand All @@ -303,6 +307,7 @@ export const createServiceTargetFactory = Factory.Sync.makeFactory<ServiceTarget
},
label: 'my-service-target',
load_balancing_policy: 'least_request',
percentage: 0,
protocol: 'https',
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Stack from '@mui/material/Stack';
import { useFormikContext } from 'formik';
import * as React from 'react';

import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
import { BetaChip } from 'src/components/BetaChip/BetaChip';
import { Box } from 'src/components/Box';
import { Link } from 'src/components/Link';
import { TextField } from 'src/components/TextField';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { AGLB_DOCS_TLS_CERTIFICATE } from 'src/features/LoadBalancers/constants';

import {
CONFIGURATION_COPY,
protocolOptions,
} from '../LoadBalancerDetail/Configurations/constants';

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

interface Props {
index: number;
name: string;
}

export const ConfigurationDetails = ({ index, name }: Props) => {
const {
errors,
handleBlur,
handleChange,
setFieldValue,
touched,
values,
} = useFormikContext<CreateLoadbalancerPayload>();

return (
<Box padding={1}>
<Typography variant="h2">Details</Typography>
<Typography sx={(theme) => ({ marginRight: theme.spacing(1) })}>
The port the load balancer listens on, and the protocol for routing
incoming traffic to the targets.
</Typography>
<Stack spacing={2}>
<Stack direction="row" spacing={2}>
<Autocomplete
errorText={
touched[name]?.[index]?.protocol
? errors[name]?.[index]?.protocol
: ''
}
onChange={(_, { value }) =>
setFieldValue(`${name}.${index}.protocol`, value)
}
textFieldProps={{
labelTooltipText: CONFIGURATION_COPY.Protocol,
}}
value={protocolOptions.find(
(option) => option.value === values[name]?.[index]?.protocol
)}
disableClearable
label="Protocol"
options={protocolOptions}
/>
<TextField
errorText={
touched[name]?.[index]?.port ? errors[name]?.[index]?.port : ''
}
label="Port"
labelTooltipText={CONFIGURATION_COPY.Port}
name={`${name}.${index}.port`}
onBlur={handleBlur}
onChange={handleChange}
placeholder="Enter Port"
type="number"
value={values[name]?.[index]?.port}
/>
</Stack>
<Stack>
<Typography variant="h3">
TLS Certificates
<TooltipIcon status="help" text={CONFIGURATION_COPY.Certificates} />
</Typography>
<Box sx={{ alignItems: 'center', display: 'flex' }}>
<BetaChip
sx={(theme) => ({
marginLeft: '0 !important',
marginRight: `${theme.spacing(1 / 2)} !important`,
})}
/>
<Typography>
After the load balancer is created, and if the protocol is HTTPS,
upload TLS termination certificates.
<Link to={AGLB_DOCS_TLS_CERTIFICATE}>Learn more.</Link>
</Typography>
</Box>
</Stack>
</Stack>
<TextField
errorText={
touched[name]?.[index]?.label ? errors[name]?.[index]?.label : ''
}
label="Configuration Label"
labelTooltipText={CONFIGURATION_COPY.configuration}
name={`${name}.${index}.label`}
onBlur={handleBlur}
onChange={handleChange}
placeholder="Enter Configuration Label"
value={values[name]?.[index]?.label}
/>
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const LoadBalancerActionPanel = () => {
buttonType="primary"
onClick={submitForm}
sx={{ marginLeft: 'auto' }}
type="submit"
>
Review Load Balancer
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';
import { renderWithThemeAndFormik } from 'src/utilities/testHelpers';

import { LoadBalancerConfiguration } from './LoadBalancerConfiguration';

// Define your initial values based on your form structure
const initialValues = {
configurations: [{ label: '', port: 80, protocol: 'https' }],
label: '',
};

describe('LoadBalancerConfiguration', () => {
test('Should render Details content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
expect(
screen.getByText('TODO: AGLB - Implement Details step content.')
).toBeInTheDocument();
renderWithThemeAndFormik(
<LoadBalancerConfiguration index={0} name="configurations" />,
{ initialValues, onSubmit: vi.fn() }
);

const ConfigurationInputLabel = screen.getByPlaceholderText(
'Enter Configuration Label'
);
const ConfigurationPort = screen.getByPlaceholderText('Enter Port');

userEvent.type(ConfigurationInputLabel, 'Test Label');
// Clear the input field before typing
userEvent.clear(ConfigurationPort);
userEvent.type(ConfigurationPort, '90');

expect(ConfigurationInputLabel).toHaveValue('Test Label');
expect(ConfigurationPort).toHaveValue(90);

expect(screen.getByText('Protocol')).toBeInTheDocument();
expect(
screen.queryByText(
'TODO: AGLB - Implement Service Targets Configuration.'
Expand All @@ -24,7 +45,10 @@ describe('LoadBalancerConfiguration', () => {
expect(screen.queryByText('Previous: Details')).toBeNull();
});
test('Should navigate to Service Targets content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
renderWithThemeAndFormik(
<LoadBalancerConfiguration index={0} name="configurations" />,
{ initialValues, onSubmit: vi.fn() }
);
userEvent.click(screen.getByTestId('service-targets'));
expect(
screen.getByText('TODO: AGLB - Implement Service Targets Configuration.')
Expand All @@ -40,7 +64,10 @@ describe('LoadBalancerConfiguration', () => {
expect(screen.queryByText('Previous: Service Targets')).toBeNull();
});
test('Should navigate to Routes content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
renderWithThemeAndFormik(
<LoadBalancerConfiguration index={0} name="configurations" />,
{ initialValues, onSubmit: vi.fn() }
);
userEvent.click(screen.getByTestId('service-targets'));
userEvent.click(screen.getByTestId('routes'));
expect(
Expand All @@ -57,11 +84,12 @@ describe('LoadBalancerConfiguration', () => {
expect(screen.getByText('Previous: Service Targets')).toBeInTheDocument();
});
test('Should be able to go previous step', () => {
renderWithTheme(<LoadBalancerConfiguration />);
renderWithThemeAndFormik(
<LoadBalancerConfiguration index={0} name="configurations" />,
{ initialValues, onSubmit: vi.fn() }
);
userEvent.click(screen.getByTestId('service-targets'));
userEvent.click(screen.getByText('Previous: Details'));
expect(
screen.getByText('TODO: AGLB - Implement Details step content.')
).toBeInTheDocument();
expect(screen.getByText('Protocol')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
import Stack from '@mui/material/Stack';
import { useFormikContext } from 'formik';
import * as React from 'react';

import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';
import { VerticalLinearStepper } from 'src/components/VerticalLinearStepper/VerticalLinearStepper';

export const configurationSteps = [
{
content: <div>TODO: AGLB - Implement Details step content.</div>,
handler: () => null,
label: 'Details',
},
{
content: <div>TODO: AGLB - Implement Service Targets Configuration.</div>,
handler: () => null,
label: 'Service Targets',
},
{
content: <div>TODO: AGLB - Implement Routes Configuration.</div>,
handler: () => null,
label: 'Routes',
},
];
import { ConfigurationDetails } from './ConfigurationDetails';

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

interface Props {
index: number;
name: string;
}

export const LoadBalancerConfiguration = ({ index, name }: Props) => {
const configurationSteps = [
{
content: <ConfigurationDetails index={index} name={name} />,
handler: () => null,
label: 'Details',
},
{
content: <div>TODO: AGLB - Implement Service Targets Configuration.</div>,
handler: () => null,
label: 'Service Targets',
},
{
content: <div>TODO: AGLB - Implement Routes Configuration.</div>,
handler: () => null,
label: 'Routes',
},
];

const { values } = useFormikContext<CreateLoadbalancerPayload>();

export const LoadBalancerConfiguration = () => {
return (
<Paper>
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
>
Configuration -{' '}
Configuration -{values[name]?.[index]?.label}
</Typography>
<Stack spacing={1}>
<Typography>
A Configuration listens on a port and uses Route Rules to forward
request to Service Target Endpoints
The load balancer configuration for processing incoming requests, the
service targets it directs requests to and routing rules.
</Typography>
<VerticalLinearStepper steps={configurationSteps} />
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FieldArray, useFormikContext } from 'formik';
import * as React from 'react';

import { LoadBalancerConfiguration } from './LoadBalancerConfiguration';

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

export const LoadBalancerConfigurations = () => {
const { values } = useFormikContext<CreateLoadbalancerPayload>();
return (
<FieldArray name="configurations">
{({ insert, push, remove }) => (
<div>
{values.configurations?.map((configuration, index) => (
<LoadBalancerConfiguration
index={index}
key={index}
name="configurations"
/>
))}
</div>
)}
</FieldArray>
);
};
Loading

0 comments on commit acf3f2e

Please sign in to comment.