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-7029] - Add AGLB Certificate Create Drawer #9616

Merged
3 changes: 2 additions & 1 deletion packages/api-v4/src/aglb/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Request, {
import { BETA_API_ROOT } from 'src/constants';
import { Filter, Params, ResourcePage } from '../types';
import { Certificate, CreateCertificatePayload } from './types';
import { CreateCertificateSchema } from '@linode/validation';

/**
* getLoadbalancerCertificates
Expand Down Expand Up @@ -60,7 +61,7 @@ export const createLoadbalancerCertificate = (
`${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/certificates`
),
setMethod('POST'),
setData(data)
setData(data, CreateCertificateSchema)
);

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

Add AGLB Certificate Create Drawer ([#9616](https://github.com/linode/manager/pull/9616))
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { CreateCertificateDrawer } from './CreateCertificateDrawer';

describe('CreateCertificateDrawer', () => {
it('should be submittable when form is filled out correctly', async () => {
const onClose = jest.fn();

const { getByLabelText, getByTestId } = renderWithTheme(
<CreateCertificateDrawer loadbalancerId={0} onClose={onClose} open />
);

const labelInput = getByLabelText('Label');
const certInput = getByLabelText('TLS Certificate');
const keyInput = getByLabelText('Private Key');

act(() => {
userEvent.type(labelInput, 'my-cert-0');
userEvent.type(certInput, 'massive cert');
userEvent.type(keyInput, 'massive key');

userEvent.click(getByTestId('submit'));
});

await waitFor(() => expect(onClose).toBeCalled());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { CreateCertificatePayload } from '@linode/api-v4';
import { Stack } from '@mui/material';
import { useFormik } from 'formik';
import React from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { Notice } from 'src/components/Notice/Notice';
import { Radio } from 'src/components/Radio/Radio';
import { RadioGroup } from 'src/components/RadioGroup';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { useLoadBalancerCertificateCreateMutation } from 'src/queries/aglb/certificates';
import { getErrorMap } from 'src/utilities/errorUtils';

interface Props {
loadbalancerId: number;
onClose: () => void;
open: boolean;
}

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

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

const {
error,
mutateAsync: createCertificate,
reset,
} = useLoadBalancerCertificateCreateMutation(loadbalancerId);

const formik = useFormik<CreateCertificatePayload>({
initialValues: {
certificate: '',
key: '',
label: '',
type: 'downstream',
},
async onSubmit(values) {
await createCertificate(values);
onClose();
},
});

const errorMap = getErrorMap(['label', 'key', 'certificate'], error);

return (
<Drawer onClose={onClose} open={open} title="Upload Certificate">
<form onSubmit={formik.handleSubmit}>
{errorMap.none && <Notice text={errorMap.none} variant="error" />}
<Typography mb={2}>
Upload the certificates for Load Balancer authentication.
</Typography>
<RadioGroup
name="type"
onChange={formik.handleChange}
value={formik.values.type}
>
<FormControlLabel
label={
<Stack mt={1.5} spacing={1}>
<Typography>TLS Certificate</Typography>
<Typography>
Used by your load balancer to terminate the connection and
decrypt request from client prior to sending the request to
the endpoints in your Service Targets. You can specify a Host
Header. Also referred to as SSL Certificate.
</Typography>
</Stack>
}
control={<Radio />}
sx={{ alignItems: 'flex-start' }}
value="downstream"
/>
<FormControlLabel
label={
<Stack mt={1.5} spacing={1}>
<Typography>Service Target Certificate</Typography>
<Typography>
Used by the load balancer to accept responses from your
endpoints in your Service Target. This is the certificate
installed on your Endpoints.
</Typography>
</Stack>
}
control={<Radio />}
sx={{ alignItems: 'flex-start', mt: 2 }}
value="ca"
/>
</RadioGroup>
<TextField
errorText={errorMap.label}
label="Label"
name="label"
onChange={formik.handleChange}
value={formik.values.label}
/>
<TextField
errorText={errorMap.certificate}
label="TLS Certificate"
labelTooltipText="TODO"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure we have this tracked to complete in the future?

multiline
name="certificate"
onChange={formik.handleChange}
trimmed
value={formik.values.certificate}
/>
<TextField
errorText={errorMap.key}
label="Private Key"
labelTooltipText="TODO"
multiline
name="key"
onChange={formik.handleChange}
trimmed
value={formik.values.key}
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
/>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
label: 'Upload Certificate',
type: 'submit',
}}
/>
</form>
</Drawer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { usePagination } from 'src/hooks/usePagination';
import { useLoadBalancerCertificatesQuery } from 'src/queries/aglb/certificates';

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

const PREFERENCE_KEY = 'loadbalancer-certificates';

Expand All @@ -38,6 +39,7 @@ type CertificateTypeFilter = 'all' | Certificate['type'];
export const LoadBalancerCertificates = () => {
const { loadbalancerId } = useParams<{ loadbalancerId: string }>();

const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false);
const [type, setType] = useState<CertificateTypeFilter>('all');
const [query, setQuery] = useState<string>();

Expand Down Expand Up @@ -134,7 +136,12 @@ export const LoadBalancerCertificates = () => {
value={query}
/>
<Box flexGrow={1} />
<Button buttonType="primary">Upload Certificate</Button>
<Button
buttonType="primary"
onClick={() => setIsCreateDrawerOpen(true)}
>
Upload Certificate
</Button>
</Stack>
<Table>
<TableHead>
Expand Down Expand Up @@ -185,6 +192,11 @@ export const LoadBalancerCertificates = () => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
<CreateCertificateDrawer
loadbalancerId={Number(loadbalancerId)}
onClose={() => setIsCreateDrawerOpen(false)}
open={isCreateDrawerOpen}
/>
</>
);
};
25 changes: 23 additions & 2 deletions packages/manager/src/queries/aglb/certificates.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { getLoadbalancerCertificates } from '@linode/api-v4';
import { useQuery } from 'react-query';
import {
createLoadbalancerCertificate,
getLoadbalancerCertificates,
} from '@linode/api-v4';
import { useQuery, useQueryClient, useMutation } from 'react-query';

import { QUERY_KEY } from './loadbalancers';

import type {
APIError,
Certificate,
CreateCertificatePayload,
Filter,
Params,
ResourcePage,
Expand All @@ -22,3 +26,20 @@ export const useLoadBalancerCertificatesQuery = (
{ keepPreviousData: true }
);
};

export const useLoadBalancerCertificateCreateMutation = (id: number) => {
const queryClient = useQueryClient();
return useMutation<Certificate, APIError[], CreateCertificatePayload>(
(data) => createLoadbalancerCertificate(id, data),
{
onSuccess() {
queryClient.invalidateQueries([
QUERY_KEY,
'loadbalancer',
id,
'certificates',
]);
},
}
);
};
1 change: 1 addition & 0 deletions packages/validation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './firewalls.schema';
export * from './images.schema';
export * from './kubernetes.schema';
export * from './linodes.schema';
export * from './loadbalancers.schema';
export * from './longview.schema';
export * from './managed.schema';
export * from './networking.schema';
Expand Down
11 changes: 11 additions & 0 deletions packages/validation/src/loadbalancers.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { object, string } from 'yup';

export const CreateCertificateSchema = object({
certificate: string().required('Certificate is required.'),
key: string().when('type', {
is: 'downstream',
then: string().required('Private Key is required.'),
}),
label: string().required('Label is required.'),
type: string().oneOf(['downstream', 'ca']).required('Type is required.'),
});