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

feat: [M3-7288] - AGLB create page with Actions buttons. #9825

Merged
merged 28 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d120b11
feat: [M3-7288] - AGLB create page with Actions buttons.
cpathipa Oct 23, 2023
e2a78e3
Merge remote-tracking branch 'origin/develop' into M3-7288
cpathipa Oct 23, 2023
7f56fb9
Add configuration header section
cpathipa Oct 23, 2023
0d77241
Add Actions buttons
cpathipa Oct 23, 2023
c767907
Render Regions as per the mockup
cpathipa Oct 23, 2023
3d2dc73
Added changeset: AGLB create page with Actions buttons.
cpathipa Oct 23, 2023
aa2d981
Unit test coverage for LoadBalancerLabel
cpathipa Oct 23, 2023
a9ceab7
Unit test for LoadBalancerConfiguration
cpathipa Oct 23, 2023
078df04
Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate…
cpathipa Oct 24, 2023
f8b765e
Update packages/manager/.changeset/pr-9825-upcoming-features-16980932…
cpathipa Oct 24, 2023
af618d6
PR - Feedback
cpathipa Oct 24, 2023
4d4e84c
Merge branch 'M3-7288' of github.com:cpathipa/manager into M3-7288
cpathipa Oct 24, 2023
47fea16
PR - feedback
cpathipa Oct 24, 2023
19ccdaf
Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate…
cpathipa Oct 24, 2023
f529242
Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate…
cpathipa Oct 24, 2023
f5dd31f
PR - Feedback
cpathipa Oct 24, 2023
e42026a
Fix alignment for small screen
cpathipa Oct 24, 2023
fb9122c
Override breadcrumb label
cpathipa Oct 24, 2023
ea2f25b
Fix tests
cpathipa Oct 24, 2023
949349c
Adjust actions buttons order for small screens.
cpathipa Oct 25, 2023
6fd6386
Update packages/manager/src/features/LoadBalancers/LoadBalancerCreate…
cpathipa Oct 25, 2023
413b325
Rename file name - LoadBalancerLabel
cpathipa Oct 25, 2023
c50c029
Merge branch 'M3-7288' of github.com:cpathipa/manager into M3-7288
cpathipa Oct 25, 2023
f5930a6
simplify styles and use less grid
bnussman Oct 25, 2023
d1f4b08
Wrap stepper component for mobile view
cpathipa Oct 26, 2023
a7344f2
add real regions
bnussman Oct 26, 2023
71e8ff1
Use util - convertToKebabCase
cpathipa Oct 26, 2023
8aba807
Merge branch 'M3-7288' of github.com:cpathipa/manager into M3-7288
cpathipa Oct 26, 2023
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": Upcoming Features
---

Add AGLB create page with Actions buttons ([#9825](https://github.com/linode/manager/pull/9825))
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export const CustomStepIcon = styled(StepIcon, { label: 'StyledCircleIcon' })(

export const StyledColorlibConnector = styled(StepConnector, {
label: 'StyledColorlibConnector',
})(() => ({
})(({ theme }) => ({
'& .MuiStepConnector-line': {
borderColor: '#eaeaf0',
borderLeftWidth: '3px',
minHeight: '28px',
minHeight: theme.spacing(2),
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import {
} from '@mui/material';
import Box from '@mui/material/Box';
import { Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/styles';
import React, { useState } from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { convertToKebabCase } from 'src/utilities/convertToKebobCase';

import {
CustomStepIcon,
Expand All @@ -23,14 +26,17 @@ type VerticalLinearStep = {
label: string;
};

interface VerticalLinearStepperProps {
export interface VerticalLinearStepperProps {
steps: VerticalLinearStep[];
}

export const VerticalLinearStepper = ({
steps,
}: VerticalLinearStepperProps) => {
const [activeStep, setActiveStep] = useState(0);
const theme = useTheme<Theme>();

const matchesSmDown = useMediaQuery(theme.breakpoints.down('md'));

const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
Expand All @@ -45,9 +51,8 @@ export const VerticalLinearStepper = ({
sx={(theme: Theme) => ({
backgroundColor: theme.bg.bgPaper,
display: 'flex',
margin: 'auto',
maxWidth: 800,
p: `${theme.spacing(2)}`,
flexDirection: matchesSmDown ? 'column' : 'row',
p: matchesSmDown ? `${theme.spacing(2)}px 0px` : `${theme.spacing(2)}`,
})}
>
{/* Left Column - Vertical Steps */}
Expand Down Expand Up @@ -101,7 +106,16 @@ export const VerticalLinearStepper = ({
{steps.map(({ content, handler, label }, index) => (
<Step key={label}>
{index === activeStep ? (
<StepContent sx={{ border: 'none' }}>
<StepContent
sx={{
border: 'none',
marginLeft: matchesSmDown ? '0px' : undefined,
paddingLeft: matchesSmDown ? '0px' : undefined,
paddingTop: matchesSmDown
? `${theme.spacing(2)}`
: undefined,
}}
>
<Box
sx={(theme) => ({
bgcolor: theme.bg.app,
Expand All @@ -116,9 +130,13 @@ export const VerticalLinearStepper = ({
primaryButtonProps={
index !== 2
? {
'data-testid': steps[
index + 1
]?.label.toLocaleLowerCase(),
/** Generate a 'data-testid' attribute value based on the label of the next step.
* 1. toLocaleLowerCase(): Converts the label to lowercase for consistency.
* 2. replace(/\s/g, ''): Removes spaces from the label to create a valid test ID.
*/
'data-testid': convertToKebabCase(
steps[index + 1]?.label
),
label: `Next: ${steps[index + 1]?.label}`,
onClick: () => {
handleNext();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { LoadBalancerConfiguration } from './LoadBalancerConfiguration';

describe('LoadBalancerConfiguration', () => {
test('Should render Details content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can clean this up a little by doing this instead and with the other tests

    const { getByText, queryByText } = renderWithTheme(
      <LoadBalancerConfiguration />
    );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, I don't see any significant benefits in choosing between using 'screen' methods
and destructuring the 'render' result. Both approaches are concise and easy to read, and I have
no strong opinions on which one to use. It largely depends on personal preference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While screen works great, I also have a deep fear that it one day will cause issues. It scares me that it's just some magic global that happens to have the correct scope when used in a test. I have no idea how it is implemented but I generally prefer destructuring the 'render' result because it seems more direct and less "magic" to me. I'm sure either method is fine but just wanted to provide another perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a quick look in into testing-library documentation, looks like they are recommending screen methods to find DOM elements (Didn't do in-depth research) and it also supports multiple frameworks.
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting, good find. πŸ”Ž

expect(
screen.getByText('TODO: AGLB - Implement Details step content.')
).toBeInTheDocument();
expect(
screen.queryByText(
'TODO: AGLB - Implement Service Targets Configuration.'
)
).toBeNull();
expect(
screen.queryByText('TODO: AGLB - Implement Routes Confiugataion.')
).toBeNull();
expect(screen.getByText('Next: Service Targets')).toBeInTheDocument();
expect(screen.queryByText('Previous: Details')).toBeNull();
});
test('Should navigate to Service Targets content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
userEvent.click(screen.getByTestId('service-targets'));
expect(
screen.getByText('TODO: AGLB - Implement Service Targets Configuration.')
).toBeInTheDocument();
expect(
screen.queryByText('TODO: AGLB - Implement Details step content.')
).toBeNull();
expect(
screen.queryByText('TODO: AGLB - Implement Routes Confiugataion.')
).toBeNull();
expect(screen.getByText('Next: Routes')).toBeInTheDocument();
expect(screen.getByText('Previous: Details')).toBeInTheDocument();
expect(screen.queryByText('Previous: Service Targets')).toBeNull();
});
test('Should navigate to Routes content', () => {
renderWithTheme(<LoadBalancerConfiguration />);
userEvent.click(screen.getByTestId('service-targets'));
userEvent.click(screen.getByTestId('routes'));
expect(
screen.queryByText('TODO: AGLB - Implement Details step content.')
).toBeNull();
expect(
screen.queryByText(
'TODO: AGLB - Implement Service Targets Configuration.'
)
).toBeNull();
expect(
screen.getByText('TODO: AGLB - Implement Routes Confiugataion.')
).toBeInTheDocument();
expect(screen.getByText('Previous: Service Targets')).toBeInTheDocument();
});
test('Should be able to go previous step', () => {
renderWithTheme(<LoadBalancerConfiguration />);
userEvent.click(screen.getByTestId('service-targets'));
userEvent.click(screen.getByText('Previous: Details'));
expect(
screen.getByText('TODO: AGLB - Implement Details step content.')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Stack from '@mui/material/Stack';
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 Confiugataion.</div>,
handler: () => null,
label: 'Routes',
},
];

export const LoadBalancerConfiguration = () => {
return (
<Paper>
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
>
Configuration -{' '}
</Typography>
<Stack spacing={1}>
<Typography>
A Configuration listens on a port and uses Route Rules to forward
request to Service Target Endpoints
</Typography>
<VerticalLinearStepper steps={configurationSteps} />
</Stack>
</Paper>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
import Stack from '@mui/material/Stack';
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 { LoadBalancerConfiguration } from './LoadBalancerConfiguration';
import { LoadBalancerLabel } from './LoadBalancerLabel';
import { LoadBalancerRegions } from './LoadBalancerRegions';

const LoadBalancerCreate = () => {
return (
<>
<DocumentTitleSegment segment="Load Balancers" />
TODO: AGLB M3-6815: Load Balancer Create
<DocumentTitleSegment segment="Create a Load Balancer" />
<LandingHeader
breadcrumbProps={{
crumbOverrides: [
{
label: 'Global Load Balancers',
position: 1,
},
],
pathname: location.pathname,
}}
title="Create"
/>
<Stack spacing={3}>
<LoadBalancerLabel
labelFieldProps={{
disabled: false,
errorText: '',
label: 'Linode Label',
onChange: () => null,
value: '',
}}
/>
<LoadBalancerRegions />
<LoadBalancerConfiguration />
{/* TODO: AGLB -
* Implement Review Load Balancer Action Behavior
* Implement Add Another Configuration Behavior
*/}
<Box
columnGap={1}
display="flex"
flexWrap="wrap"
justifyContent="space-between"
rowGap={3}
>
<Button buttonType="outlined">Add Another Configuration</Button>
<Button buttonType="primary" sx={{ marginLeft: 'auto' }}>
Review Load Balancer
</Button>
</Box>
</Stack>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';

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

import { LoadBalancerLabel } from './LoadBalancerLabel';

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(
<LoadBalancerLabel error="" labelFieldProps={labelFieldProps} />
);

const labelInput = getByTestId('textfield-input');
const errorNotice = queryByText('Error Text');

expect(labelInput).toBeInTheDocument();
expect(labelInput).toHaveAttribute('placeholder', 'Enter a label');
expect(labelInput).toHaveValue('Test Label');
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(
<LoadBalancerLabel error="Error Text" labelFieldProps={labelFieldProps} />
);

const labelInput = getByTestId('textfield-input');
const errorNotice = getByText('This is an error');

expect(labelInput).toBeInTheDocument();
expect(errorNotice).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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';

interface LabelProps {
error?: string;
labelFieldProps: TextFieldProps;
}

export const LoadBalancerLabel = (props: LabelProps) => {
const { error, labelFieldProps } = props;

return (
<Paper
sx={{
flexGrow: 1,
width: '100%',
}}
data-qa-label-header
>
{error && <Notice text={error} variant="error" />}
<TextField
data-qa-label-input
disabled={labelFieldProps.disabled}
errorText={labelFieldProps.errorText}
label="Load Balancer Label"
noMarginTop
onChange={() => labelFieldProps.onChange}
placeholder="Enter a label"
value={labelFieldProps.value}
/>
</Paper>
);
};
Loading