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

upcoming: [M3-7294] - Add AGLB Routes Step to Full Create Flow #9997

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 Routes section of full create page ([#9997](https://github.com/linode/manager/pull/9997))
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { AddRouteDrawer } from './AddRouteDrawer';
import { initialValues } from './LoadBalancerCreate';

describe('AddRouteDrawer (AGLB full create flow)', () => {
it('renders a title', () => {
const { getByText } = renderWithThemeAndFormik(
<AddRouteDrawer
configurationIndex={0}
onClose={vi.fn()}
open={true}
protocol="tcp"
/>,
{ initialValues, onSubmit: vi.fn() }
);

expect(getByText('Add Route', { selector: 'h2' })).toBeVisible();
});
it('renders the options and default to creating a new route', () => {
const { getByText } = renderWithThemeAndFormik(
<AddRouteDrawer
configurationIndex={0}
onClose={vi.fn()}
open={true}
protocol="http"
/>,
{ initialValues, onSubmit: vi.fn() }
);

expect(getByText('Create New HTTP Route')).toBeVisible();
});
it('closes the drawer upon route creation', async () => {
const onClose = vi.fn();
const {
getByLabelText,
getByText,
} = renderWithThemeAndFormik(
<AddRouteDrawer
configurationIndex={0}
onClose={onClose}
open={true}
protocol="http"
/>,
{ initialValues, onSubmit: vi.fn() }
);

const labelTextField = getByLabelText('Route Label');
const addButton = getByText('Add Route', { selector: 'span' }).closest(
'button'
);

userEvent.type(labelTextField, 'my-route');

userEvent.click(addButton!);

await waitFor(() => expect(onClose).toBeCalled());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { CreateRouteSchema } from '@linode/validation';
import { useFormik, useFormikContext, yupToFormErrors } from 'formik';
import React, { useState } from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
import { Drawer } from 'src/components/Drawer';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { Radio } from 'src/components/Radio/Radio';
import { RadioGroup } from 'src/components/RadioGroup';
import { Stack } from 'src/components/Stack';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { getNextLabel } from 'src/utilities/stringUtils';

import { ROUTE_COPY } from '../LoadBalancerDetail/Routes/constants';
import { getRouteProtocolFromConfigurationProtocol } from '../LoadBalancerDetail/Routes/utils';
import { LoadBalancerCreateFormData } from './LoadBalancerCreate';

import type { Configuration, Route, RoutePayload } from '@linode/api-v4';

export interface Props {
configurationIndex: number;
onClose: () => void;
open: boolean;
protocol: Configuration['protocol'];
}

type Mode = 'existing' | 'new';

export const AddRouteDrawer = (props: Props) => {
const { configurationIndex, onClose, open, protocol } = props;

const {
setFieldValue,
values,
} = useFormikContext<LoadBalancerCreateFormData>();

const [mode, setMode] = useState<Mode>('new');

const routeProtocol = getRouteProtocolFromConfigurationProtocol(protocol);

const allRoutes = values.configurations.reduce<RoutePayload[]>(
(acc, configuration) => {
return [...acc, ...configuration.routes!];
},
[]
);

const onAdd = (route: RoutePayload) => {
setFieldValue(`configurations[${configurationIndex}].routes`, [
...values.configurations![configurationIndex].routes!,
route,
]);
onClose();
};

return (
<Drawer onClose={onClose} open={open} title="Add Route">
<Stack spacing={1}>
<Typography>{ROUTE_COPY.Description.main}</Typography>
<Typography>{ROUTE_COPY.Description[routeProtocol]}</Typography>
</Stack>
<RadioGroup onChange={(_, value) => setMode(value as Mode)} value={mode}>
<FormControlLabel
control={<Radio />}
label={`Create New ${routeProtocol.toUpperCase()} Route`}
value="new"
/>
<FormControlLabel
control={<Radio />}
label="Add Existing Route"
value="existing"
/>
</RadioGroup>
{mode === 'existing' ? (
<AddExistingRouteForm
existingRoutes={allRoutes}
onAdd={onAdd}
onClose={onClose}
/>
) : (
<AddNewRouteForm
existingRoutes={allRoutes}
onAdd={onAdd}
onClose={onClose}
protocol={routeProtocol}
/>
)}
</Drawer>
);
};

interface AddExistingRouteFormProps {
existingRoutes: RoutePayload[];
onAdd: (route: RoutePayload) => void;
onClose: () => void;
}

const AddExistingRouteForm = (props: AddExistingRouteFormProps) => {
const { existingRoutes, onAdd, onClose } = props;

const formik = useFormik<{ route: RoutePayload | null }>({
initialValues: {
route: null,
},
onSubmit({ route }) {
if (!route) {
throw new Error('No route selected');
}
onAdd({ ...route, label: getNextLabel(route, existingRoutes) });
onClose();
},
validate(values) {
if (!values.route) {
return { route: 'Please select an existing route.' };
}
return {};
},
});

return (
<form onSubmit={formik.handleSubmit}>
<Autocomplete
errorText={formik.errors.route}
label="Route"
noMarginTop
onChange={(_, route) => formik.setFieldValue('route', route)}
options={existingRoutes}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Add Route',
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
);
};

interface AddNewRouteFormProps {
existingRoutes: RoutePayload[];
onAdd: (route: RoutePayload) => void;
onClose: () => void;
protocol: Route['protocol'];
}

const AddNewRouteForm = (props: AddNewRouteFormProps) => {
const { existingRoutes, onAdd, onClose, protocol } = props;

const formik = useFormik<RoutePayload>({
initialValues: {
label: '',
protocol,
rules: [],
},
async onSubmit(route) {
onAdd(route);
},
validate(values) {
if (existingRoutes.some((route) => route.label === values.label)) {
return {
label: 'Routes must have unique labels.',
};
}

try {
CreateRouteSchema.validateSync(values, { abortEarly: false });
return {};
} catch (error) {
return yupToFormErrors(error);
}
},
});

return (
<form onSubmit={formik.handleSubmit}>
<TextField
errorText={formik.errors.label}
label="Route Label"
name="label"
noMarginTop
onChange={formik.handleChange}
value={formik.values.label}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Add Route',
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Stack from '@mui/material/Stack';
import { useFormikContext, getIn } from 'formik';
import { getIn, useFormikContext } from 'formik';
import * as React from 'react';

import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
Expand All @@ -16,9 +16,11 @@ import {
protocolOptions,
} from '../LoadBalancerDetail/Configurations/constants';

import type { Handlers } from './LoadBalancerConfigurations';
import type { LoadBalancerCreateFormData } from './LoadBalancerCreate';

interface Props {
handlers: Handlers;
index: number;
}

Expand Down Expand Up @@ -67,6 +69,7 @@ export const ConfigurationDetails = ({ index }: Props) => {
? getIn(errors, `configurations[${index}].port`)
: ''
}
inputId={`configuration-${index}-port`}
label="Port"
labelTooltipText={CONFIGURATION_COPY.Port}
name={`configurations[${index}].port`}
Expand Down Expand Up @@ -104,6 +107,7 @@ export const ConfigurationDetails = ({ index }: Props) => {
? getIn(errors, `configurations[${index}].label`)
: ''
}
inputId={`configuration-${index}-label`}
label="Configuration Label"
labelTooltipText={CONFIGURATION_COPY.configuration}
name={`configurations[${index}].label`}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { EditRouteDrawer } from './EditRouteDrawer';
import {
LoadBalancerCreateFormData,
initialValues,
} from './LoadBalancerCreate';

describe('EditRouteDrawer (AGLB full create flow)', () => {
it('renders a title', () => {
const { getByText } = renderWithThemeAndFormik(
<EditRouteDrawer
configurationIndex={0}
onClose={vi.fn()}
open={true}
routeIndex={0}
/>,
{ initialValues, onSubmit: vi.fn() }
);

expect(getByText('Edit Route', { selector: 'h2' })).toBeVisible();
});
it('prefills the label field with the route label and edits', async () => {
const values: LoadBalancerCreateFormData = {
...initialValues,
configurations: [
{
service_targets: [],
certificates: [],
label: 'test',
port: 8080,
protocol: 'http',
routes: [{ label: 'test-1', protocol: 'http', rules: [] }],
},
],
};

const {
getByLabelText,
getByText,
} = renderWithThemeAndFormik(
<EditRouteDrawer
configurationIndex={0}
onClose={vi.fn()}
open={true}
routeIndex={0}
/>,
{ initialValues: values, onSubmit: vi.fn() }
);

const routeLabelTextField = getByLabelText('Route Label');
const saveButton = getByText('Save').closest('button');

expect(routeLabelTextField).toHaveDisplayValue(
values.configurations![0].routes![0].label
);

expect(saveButton).toBeDisabled();

userEvent.type(routeLabelTextField, 'my-new-label');

expect(saveButton).toBeEnabled();

userEvent.click(saveButton!);
});
});
Loading