Skip to content

Commit

Permalink
feat: [M3-6819] - AGLB Details - Configurations Tab (#9591)
Browse files Browse the repository at this point in the history
* add aglb configs tab - basic layout

* improve factory

* add ability to delete a cert

* add orderable table

* remove routes ection for now

* adding and removing certs works

* add basic error handling and mutation

* add some validation

* Added changeset: AGLB Details - Configuration Tab

* feedback @abailly-akamai

* feedback @mjac0bs

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Sep 7, 2023
1 parent fc4ad08 commit 5b87bf8
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 23 deletions.
2 changes: 1 addition & 1 deletion packages/api-v4/src/aglb/configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const createLoadbalancerConfiguration = (
export const updateLoadbalancerConfiguration = (
loadbalancerId: number,
configurationId: number,
data: Partial<ConfigurationPayload>
data: Partial<Configuration>
) =>
Request<Configuration>(
setURL(
Expand Down
14 changes: 7 additions & 7 deletions packages/api-v4/src/aglb/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface UpdateLoadbalancerPayload {
configuration_ids?: number[];
}

type Protocol = 'TCP' | 'HTTP' | 'HTTPS';
type Protocol = 'tcp' | 'http' | 'https';

type Policy =
| 'round_robin'
Expand Down Expand Up @@ -69,7 +69,7 @@ export interface ConfigurationPayload {
label: string;
port: number;
protocol: Protocol;
certificate_table: CertificateTable[];
certificates: CertificateConfig[];
routes?: RoutePayload[];
route_ids?: number[];
}
Expand All @@ -79,13 +79,13 @@ export interface Configuration {
label: string;
port: number;
protocol: Protocol;
certificate_table: CertificateTable[];
routes: string[];
certificates: CertificateConfig[];
routes: { id: number; label: string }[];
}

export interface CertificateTable {
sni_hostname: string;
certificate_id: string;
export interface CertificateConfig {
hostname: string;
id: number;
}

export interface Rule {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

AGLB Details - Configuration Tab ([#9591](https://github.com/linode/manager/pull/9591))
24 changes: 14 additions & 10 deletions packages/manager/src/factories/aglb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,21 @@ LhIhYpJ8UsCVt5snWo2N+M+6ANh5tpWdQnEK6zILh4tRbuzaiHgb
// Configuration endpoints
// ********************
export const configurationFactory = Factory.Sync.makeFactory<Configuration>({
certificate_table: [
certificates: [
{
certificate_id: 'cert-12345',
sni_hostname: 'example.com',
hostname: 'example.com',
id: 0,
},
],
id: Factory.each((i) => i),
label: Factory.each((i) => `entrypoint${i}`),
label: Factory.each((i) => `configuration-${i}`),
port: 80,
protocol: 'HTTP',
routes: ['images-route'],
protocol: 'http',
routes: [
{ id: 0, label: 'route-0' },
{ id: 1, label: 'route-1' },
{ id: 2, label: 'route-2' },
],
});

// ***********************
Expand All @@ -101,15 +105,15 @@ export const createLoadbalancerWithAllChildrenFactory = Factory.Sync.makeFactory
{
configurations: [
{
certificate_table: [
certificates: [
{
certificate_id: 'cert-12345',
sni_hostname: 'example.com',
id: 1,
hostname: 'example.com',
},
],
label: 'myentrypoint1',
port: 80,
protocol: 'HTTP',
protocol: 'http',
routes: [
{
label: 'my-route',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import Autocomplete from '@mui/material/Autocomplete';
import React from 'react';

import { TextField } from 'src/components/TextField';
import { useLoadBalancerCertificatesInfiniteQuery } from 'src/queries/aglb/certificates';

import type { Certificate, Filter } from '@linode/api-v4';

interface Props {
/**
* Error text to display as helper text under the TextField. Useful for validation errors.
*/
errorText?: string;
/**
* The TextField label
* @default Certificate
*/
label?: string;
/**
* The id of the Load Balancer you want to show certificates for
*/
loadbalancerId: number;
/**
* Called when the value of the Select changes
*/
onChange: (certificate: Certificate | null) => void;
/**
* The id of the selected certificate
*/
value: number;
}

export const CertificateSelect = (props: Props) => {
const { errorText, label, loadbalancerId, onChange, value } = props;

const [inputValue, setInputValue] = React.useState<string>('');

const filter: Filter = {};

if (inputValue) {
filter['label'] = { '+contains': inputValue };
}

const {
data,
error,
fetchNextPage,
hasNextPage,
isLoading,
} = useLoadBalancerCertificatesInfiniteQuery(loadbalancerId, filter);

const certificates = data?.pages.flatMap((page) => page.data);

const selectedCertificate =
certificates?.find((cert) => cert.id === value) ?? null;

const onScroll = (event: React.SyntheticEvent) => {
const listboxNode = event.currentTarget;
if (
listboxNode.scrollTop + listboxNode.clientHeight >=
listboxNode.scrollHeight &&
hasNextPage
) {
fetchNextPage();
}
};

return (
<Autocomplete
ListboxProps={{
onScroll,
}}
onInputChange={(_, value, reason) => {
if (reason === 'input') {
setInputValue(value);
}
}}
renderInput={(params) => (
<TextField
label={label ?? 'Certificate'}
{...params}
errorText={error?.[0].reason ?? errorText}
/>
)}
inputValue={selectedCertificate ? selectedCertificate.label : inputValue}
loading={isLoading}
onChange={(e, value) => onChange(value)}
options={certificates ?? []}
value={selectedCertificate}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Configuration } from '@linode/api-v4';
import { certificateConfigSchema } from '@linode/validation';
import { useFormik } from 'formik';
import React, { useEffect } from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Box } from 'src/components/Box';
import { Button } from 'src/components/Button/Button';
import { Code } from 'src/components/Code/Code';
import { Divider } from 'src/components/Divider';
import { Drawer } from 'src/components/Drawer';
import { Link } from 'src/components/Link';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';

import { CertificateSelect } from '../Certificates/CertificateSelect';

interface Props {
loadbalancerId: number;
onAdd: (certificates: Configuration['certificates']) => void;
onClose: () => void;
open: boolean;
}

const defaultCertItem = {
hostname: '',
id: -1,
};

export const ApplyCertificatesDrawer = (props: Props) => {
const { loadbalancerId, onAdd, onClose, open } = props;

const formik = useFormik<{ certificates: Configuration['certificates'] }>({
initialValues: {
certificates: [defaultCertItem],
},
onSubmit(values) {
onAdd(values.certificates);
onClose();
},
validateOnChange: false,
validationSchema: certificateConfigSchema,
});

useEffect(() => {
if (open) {
formik.resetForm();
}
}, [open]);

const onAddAnother = () => {
formik.setFieldValue('certificates', [
...formik.values.certificates,
defaultCertItem,
]);
};

return (
<Drawer onClose={onClose} open={open} title="Apply Certificates">
{/* @TODO Add AGLB docs link - M3-7041 */}
<Typography>
Input the host header that the Load Balancer will repsond to and the
respective certificate to deliver. Use <Code>*</Code> as a wildcard
apply to any host. <Link to="#">Learn more.</Link>
</Typography>
<form onSubmit={formik.handleSubmit}>
{formik.values.certificates.map(({ hostname, id }, index) => (
<Box key={index}>
<TextField
onChange={(e) =>
formik.setFieldValue(
`certificates.${index}.hostname`,
e.target.value
)
}
errorText={formik.errors.certificates?.[index]?.['hostname']}
label="Host Header"
value={hostname}
/>
<CertificateSelect
onChange={(certificate) =>
formik.setFieldValue(
`certificates.${index}.id`,
certificate?.id ?? null
)
}
errorText={formik.errors.certificates?.[index]?.['id']}
loadbalancerId={loadbalancerId}
value={id}
/>
<Divider spacingTop={24} />
</Box>
))}
<Button
buttonType="outlined"
onClick={onAddAnother}
sx={{ marginTop: 2 }}
>
Add Another
</Button>
<ActionsPanel
primaryButtonProps={{
label: 'Save',
type: 'submit',
}}
/>
</form>
</Drawer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import CloseIcon from '@mui/icons-material/Close';
import React from 'react';

import { IconButton } from 'src/components/IconButton';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
import { useLoadBalancerCertificatesQuery } from 'src/queries/aglb/certificates';

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

interface Props {
certificates: Configuration['certificates'];
loadbalancerId: number;
onRemove: (index: number) => void;
}

export const CertificateTable = (props: Props) => {
const { certificates, loadbalancerId, onRemove } = props;

const { data } = useLoadBalancerCertificatesQuery(
loadbalancerId,
{},
{ '+or': certificates.map((cert) => ({ id: cert.id })) }
);

return (
<Table>
<TableHead>
<TableRow>
<TableCell>Certificate</TableCell>
<TableCell>Host Header</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{certificates.length === 0 && <TableRowEmpty colSpan={3} />}
{certificates.map((cert, idx) => {
const certificate = data?.data.find((c) => c.id === cert.id);
return (
<TableRow key={idx}>
<TableCell>{certificate?.label ?? cert.id}</TableCell>
<TableCell>{cert.hostname}</TableCell>
<TableCell actionCell>
<IconButton
aria-label={`Remove Certificate ${
certificate?.label ?? cert.id
}`}
onClick={() => onRemove(idx)}
>
<CloseIcon />
</IconButton>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
Loading

0 comments on commit 5b87bf8

Please sign in to comment.