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-6819] - AGLB Details - Configurations Tab #9591

Merged
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,
}}
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
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>
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
</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