-
Notifications
You must be signed in to change notification settings - Fork 367
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [M3-6819] - AGLB Details - Configurations Tab (#9591)
* 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
1 parent
fc4ad08
commit 5b87bf8
Showing
14 changed files
with
607 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
packages/manager/.changeset/pr-9591-upcoming-features-1692984602898.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
.../manager/src/features/LoadBalancers/LoadBalancerDetail/Certificates/CertificateSelect.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
); | ||
}; |
110 changes: 110 additions & 0 deletions
110
.../src/features/LoadBalancers/LoadBalancerDetail/Configurations/ApplyCertificatesDrawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
63 changes: 63 additions & 0 deletions
63
...manager/src/features/LoadBalancers/LoadBalancerDetail/Configurations/CertificateTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.