Skip to content

Commit

Permalink
initial routes features on AGLB create
Browse files Browse the repository at this point in the history
  • Loading branch information
bnussman committed Dec 13, 2023
1 parent 1c21e09 commit d099bcc
Show file tree
Hide file tree
Showing 6 changed files with 480 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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 { 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 existingRoutesInThisConfiguration = values.configurations![
configurationIndex
].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={existingRoutesInThisConfiguration}
onAdd={onAdd}
onClose={onClose}
/>
) : (
<AddNewRouteForm
existingRoutes={existingRoutesInThisConfiguration}
onAdd={onAdd}
onClose={onClose}
protocol={routeProtocol}
/>
)}
</Drawer>
);
};

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

interface RouteWithConfig extends RoutePayload {
configurationIndex: number;
}

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

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

const allRoutesAcrossConfurations = values.configurations!.reduce<
RouteWithConfig[]
>((acc, configuration, index) => {
return [
...acc,
...configuration.routes!.map((r) => ({
...r,
configurationIndex: index,
})),
];
}, []);

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

return (
<form onSubmit={formik.handleSubmit}>
<Autocomplete
getOptionLabel={(option) =>
`${option.label} (Configuration ${
values.configurations![option.configurationIndex].label
? values.configurations![option.configurationIndex].label
: option.configurationIndex
})`
}
errorText={formik.errors.route}
label="Route"
noMarginTop
onChange={(_, route) => formik.setFieldValue('route', route)}
options={allRoutesAcrossConfurations}
/>
<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 within each configuration.',
};
}

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
@@ -0,0 +1,97 @@
import { RoutePayload } from '@linode/api-v4';
import { UpdateRouteSchema } from '@linode/validation';
import { useFormik, useFormikContext, yupToFormErrors } from 'formik';
import React from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import { TextField } from 'src/components/TextField';

import { LoadBalancerCreateFormData } from './LoadBalancerCreate';

interface Props {
configurationIndex: number;
onClose: () => void;
open: boolean;
routeIndex: number | undefined;
}

export const EditRouteDrawer = (props: Props) => {
const { configurationIndex, onClose: _onClose, open, routeIndex } = props;

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

const existingRoutesInThisConfiguration = values.configurations![
configurationIndex
].routes!;

const formik = useFormik<RoutePayload>({
enableReinitialize: true,
initialValues:
routeIndex !== undefined &&
routeIndex >= 0 &&
routeIndex < values.configurations![configurationIndex].routes!.length
? values.configurations![configurationIndex].routes![routeIndex]
: { label: '', protocol: 'http', rules: [] },
onSubmit(route) {
setFieldValue(
`configurations[${configurationIndex}].routes[${routeIndex}]`,
route
);
onClose();
},
validate(values) {
if (
existingRoutesInThisConfiguration.some(
(route) => route.label === values.label
)
) {
return {
label: 'Routes must have unique labels within each configuration.',
};
}
// We must use `validate` insted of validationSchema because Formik decided to convert
// "" to undefined before passing the values to yup. This makes it hard to validate `label`.
// See https://github.com/jaredpalmer/formik/issues/805
try {
UpdateRouteSchema.validateSync(values, { abortEarly: false });
return {};
} catch (error) {
return yupToFormErrors(error);
}
},
});

const onClose = () => {
_onClose();
formik.resetForm();
};

return (
<Drawer onClose={onClose} open={open} title="Edit Route">
<form onSubmit={formik.handleSubmit}>
<TextField
errorText={formik.errors.label}
label="Route Label"
name="label"
onChange={formik.handleChange}
value={formik.values.label}
/>
<ActionsPanel
primaryButtonProps={{
disabled: !formik.dirty,
label: 'Save',
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
</Drawer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Typography } from 'src/components/Typography';
import { VerticalLinearStepper } from 'src/components/VerticalLinearStepper/VerticalLinearStepper';

import { ConfigurationDetails } from './ConfigurationDetails';
import { Routes } from './Routes';
import { ServiceTargets } from './ServiceTargets';

import type { LoadBalancerCreateFormData } from './LoadBalancerCreate';
Expand All @@ -19,17 +20,14 @@ export const LoadBalancerConfiguration = ({ index }: Props) => {
const configurationSteps = [
{
content: <ConfigurationDetails index={index} />,
handler: () => null,
label: 'Details',
},
{
content: <ServiceTargets />,
handler: () => null,
label: 'Service Targets',
},
{
content: <div>TODO: AGLB - Implement Routes Configuration.</div>,
handler: () => null,
content: <Routes configurationIndex={index} />,
label: 'Routes',
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface LoadBalancerCreateFormData extends CreateLoadbalancerPayload {

export const initialValues: LoadBalancerCreateFormData = {
configurations: [
{ certificates: [], label: '', port: 443, protocol: 'https' },
{ certificates: [], label: '', port: 443, protocol: 'https', routes: [] },
],
label: '',
regions: [],
Expand Down
Loading

0 comments on commit d099bcc

Please sign in to comment.