Skip to content

Commit

Permalink
upcoming: [M3-7602] - Improve AGLB Configuration - Add Certificate Dr…
Browse files Browse the repository at this point in the history
…awer (revision 2) (#10066)

* implement new UX

* small fixes

* update e2e test

* add certifiate drawer loading state and cancel button

* Added changeset: Improve AGLB Configuration - Add Certificate Drawer

* move components to their own files and add error handling

* sort props

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Jan 18, 2024
1 parent b9ce55d commit 6fe4278
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 147 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Improve AGLB Configuration - Add Certificate Drawer ([#10066](https://github.com/linode/manager/pull/10066))
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ describe('Akamai Global Load Balancer configurations page', () => {
.should('be.visible')
.type(configuration.label);

ui.button.findByTitle('Apply Certificates').should('be.visible').click();
ui.button.findByTitle('Add Certificate').should('be.visible').click();

ui.drawer.findByTitle('Add Certificate').within(() => {
cy.findByLabelText('Add Existing Certificate').click();

ui.drawer.findByTitle('Apply Certificates').within(() => {
cy.findByLabelText('Host Header').should('be.visible').type('*');

cy.findByLabelText('Certificate').should('be.visible').click();
Expand All @@ -98,7 +100,7 @@ describe('Akamai Global Load Balancer configurations page', () => {
.click();

ui.button
.findByTitle('Save')
.findByTitle('Add')
.should('be.visible')
.should('be.enabled')
.click();
Expand Down Expand Up @@ -312,9 +314,11 @@ describe('Akamai Global Load Balancer configurations page', () => {
.should('be.visible')
.type('test');

ui.button.findByTitle('Apply Certificates').should('be.visible').click();
ui.button.findByTitle('Add Certificate').should('be.visible').click();

ui.drawer.findByTitle('Add Certificate').within(() => {
cy.findByLabelText('Add Existing Certificate').click();

ui.drawer.findByTitle('Apply Certificates').within(() => {
cy.findByLabelText('Host Header').should('be.visible').type('*');

cy.findByLabelText('Certificate').should('be.visible').click();
Expand All @@ -325,7 +329,7 @@ describe('Akamai Global Load Balancer configurations page', () => {
.click();

ui.button
.findByTitle('Save')
.findByTitle('Add')
.should('be.visible')
.should('be.enabled')
.click();
Expand Down Expand Up @@ -412,20 +416,22 @@ describe('Akamai Global Load Balancer configurations page', () => {
cy.findByLabelText('Port').should('be.visible').clear().type('444');

ui.button
.findByTitle('Apply More Certificates')
.findByTitle('Add Certificate')
.should('be.visible')
.should('be.enabled')
.click();

ui.drawer.findByTitle('Apply Certificates').within(() => {
ui.drawer.findByTitle('Add Certificate').within(() => {
cy.findByLabelText('Add Existing Certificate').click();

cy.findByLabelText('Host Header').type('example-1.com');

cy.findByLabelText('Certificate').click();

ui.autocompletePopper.findByTitle(certificates[1].label).click();

ui.button
.findByTitle('Save')
.findByTitle('Add')
.should('be.visible')
.should('be.enabled')
.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@ export const CreateCertificateDrawer = (props: Props) => {
primaryButtonProps={{
'data-testid': 'submit',
label: 'Upload Certificate',
loading: formik.isSubmitting,
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react';

import { Code } from 'src/components/Code/Code';
import { Drawer } from 'src/components/Drawer';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { Link } from 'src/components/Link';
import { Radio } from 'src/components/Radio/Radio';
import { RadioGroup } from 'src/components/RadioGroup';
import { Typography } from 'src/components/Typography';

import { AddExistingCertificateForm } from './AddExistingCertificateForm';
import { AddNewCertificateForm } from './AddNewCertificateForm';

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

export interface AddCertificateDrawerProps {
loadbalancerId: number;
onAdd: (certificates: CertificateConfig) => void;
onClose: () => void;
open: boolean;
}

type Mode = 'existing' | 'new';

export const AddCertificateDrawer = (props: AddCertificateDrawerProps) => {
const { onClose, open } = props;

const [mode, setMode] = useState<Mode>('new');

return (
<Drawer onClose={onClose} open={open} title="Add Certificate">
{/* @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>
<RadioGroup onChange={(_, value) => setMode(value as Mode)} value={mode}>
<FormControlLabel
control={<Radio />}
label="Create New Certificate"
value="new"
/>
<FormControlLabel
control={<Radio />}
label="Add Existing Certificate"
value="existing"
/>
</RadioGroup>
{mode === 'existing' ? (
<AddExistingCertificateForm {...props} />
) : (
<AddNewCertificateForm {...props} />
)}
</Drawer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CertificateEntrySchema } from '@linode/validation';
import { useFormik } from 'formik';
import React, { useEffect } from 'react';

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

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

import type { AddCertificateDrawerProps } from './AddCertificateDrawer';
import type { CertificateConfig } from '@linode/api-v4';

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

const formik = useFormik<CertificateConfig>({
initialValues: {
hostname: '',
id: -1,
},
onSubmit(values) {
onAdd(values);
onClose();
},
validationSchema: CertificateEntrySchema,
});

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

return (
<form onSubmit={formik.handleSubmit}>
<CertificateSelect
onChange={(certificate) =>
formik.setFieldValue('id', certificate?.id ?? null)
}
textFieldProps={{
noMarginTop: true,
onBlur: () => formik.setFieldTouched('id'),
}}
errorText={formik.touched.id ? formik.errors.id : undefined}
filter={{ type: 'downstream' }}
loadbalancerId={loadbalancerId}
value={formik.values.id}
/>
<TextField
errorText={formik.touched.hostname ? formik.errors.hostname : undefined}
label="Host Header"
name="hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.hostname}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Add',
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
CertificateEntrySchema,
CreateCertificateSchema,
} from '@linode/validation';
import { useFormik } from 'formik';
import React, { useEffect } from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Notice } from 'src/components/Notice/Notice';
import { TextField } from 'src/components/TextField';
import { useLoadBalancerCertificateCreateMutation } from 'src/queries/aglb/certificates';
import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils';

import {
CERTIFICATES_COPY,
exampleCert,
exampleKey,
} from '../Certificates/constants';

import type { AddCertificateDrawerProps } from './AddCertificateDrawer';
import type {
CertificateConfig,
CreateCertificatePayload,
} from '@linode/api-v4';

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

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

const formik = useFormik<
CreateCertificatePayload & Omit<CertificateConfig, 'id'>
>({
initialValues: {
certificate: '',
hostname: '',
key: '',
label: '',
type: 'downstream',
},
async onSubmit({ certificate, hostname, key, label, type }, helpers) {
try {
const cert = await createCertificate({
certificate,
key,
label,
type,
});

onAdd({ hostname, id: cert.id });
onClose();
} catch (error) {
helpers.setErrors(getFormikErrorsFromAPIErrors(error));
}
},
validationSchema: CertificateEntrySchema.omit(['id']).concat(
CreateCertificateSchema
),
});

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

const generalError = error?.[0].reason;

return (
<form onSubmit={formik.handleSubmit}>
{generalError && <Notice text={generalError} variant="error" />}
<TextField
errorText={formik.touched.label ? formik.errors.label : undefined}
label="Certificate Label"
name="label"
noMarginTop
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.label}
/>
<TextField
errorText={
formik.touched.certificate ? formik.errors.certificate : undefined
}
expand
label="Certificate"
labelTooltipText={CERTIFICATES_COPY.Tooltips.Certificate}
multiline
name="certificate"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder={exampleCert}
trimmed
value={formik.values.certificate}
/>
<TextField
errorText={formik.touched.key ? formik.errors.key : undefined}
expand
label="Private Key"
labelTooltipText={CERTIFICATES_COPY.Tooltips.Key}
multiline
name="key"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder={exampleKey}
trimmed
value={formik.values.key}
/>
<TextField
errorText={formik.touched.hostname ? formik.errors.hostname : undefined}
label="Host Header"
name="hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={formik.values.hostname}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Create and Add',
loading: formik.isSubmitting,
type: 'submit',
}}
secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
);
};
Loading

0 comments on commit 6fe4278

Please sign in to comment.