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-7020] - Update Create Firewall drawer #9630

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Update Create Firewall Drawer ([#9630](https://github.com/linode/manager/pull/9630))
Original file line number Diff line number Diff line change
Expand Up @@ -218,23 +218,16 @@ describe('Migrate Linode With Firewall', () => {
fbtClick('Create Firewall');
});

cy.get('[data-testid="textfield-input"]:first')
.should('be.visible')
.type(firewallLabel);
cy.findByLabelText('Label').should('be.visible').type(firewallLabel);

cy.get('[data-testid="textfield-input"]:last')
.should('be.visible')
.click()
.type(linode.label);
cy.findByLabelText('Linodes').should('be.visible').type(linode.label);

cy.get('[data-qa-autocomplete-popper]')
.findByText(linode.label)
ui.autocompletePopper
.findByTitle(linode.label)
.should('be.visible')
.click();

cy.get('[data-testid="textfield-input"]:last')
.should('be.visible')
.click();
cy.findByLabelText('Linodes').should('be.visible').click();

cy.findByText(linode.label).should('be.visible');

Expand Down Expand Up @@ -284,23 +277,16 @@ describe('Migrate Linode With Firewall', () => {
fbtClick('Create Firewall');
});

cy.get('[data-testid="textfield-input"]:first')
.should('be.visible')
.type(firewallLabel);
cy.findByLabelText('Label').should('be.visible').type(firewallLabel);

cy.get('[data-testid="textfield-input"]:last')
.should('be.visible')
.click()
.type(linodeLabel);
cy.findByLabelText('Linodes').should('be.visible').type(linodeLabel);

cy.get('[data-qa-autocomplete-popper]')
.findByText(linode.label)
.should('be.visible')
.click();

cy.get('[data-testid="textfield-input"]:last')
.should('be.visible')
.click();
cy.findByLabelText('Linodes').should('be.visible').click();

cy.findByText(linodeLabel).should('be.visible');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ const removeFirewallRules = (ruleLabel: string) => {
*/
const addLinodesToFirewall = (firewall: Firewall, linode: Linode) => {
// Go to Linodes tab
ui.tabList.findTabByTitle('Linodes').should('be.visible').click();
ui.tabList
.findTabByTitle('Linodes', { exact: false })
.should('be.visible')
.click();

ui.button.findByTitle('Add Linodes to Firewall').should('be.visible').click();

Expand Down
10 changes: 8 additions & 2 deletions packages/manager/cypress/support/ui/tab-list.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SelectorMatcherOptions } from '@testing-library/cypress';

/**
* Tab list UI element.
*/
Expand All @@ -15,10 +17,14 @@ export const tabList = {
* Finds a tab within a tab list by its title.
*
* @param tabTitle - Title of tab to find.
* @param options - Selector matcher options.
*
* @returns Cypress chainable.
*/
findTabByTitle: (tabTitle: string): Cypress.Chainable => {
return cy.get('[data-reach-tab-list]').findByText(tabTitle);
findTabByTitle: (
tabTitle: string,
options?: SelectorMatcherOptions
): Cypress.Chainable => {
return cy.get('[data-reach-tab-list]').findByText(tabTitle, options);
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen, within } from '@testing-library/react';
import { act, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

Expand All @@ -12,6 +12,12 @@ const props = {
};

describe('Create Firewall Drawer', () => {
it('should close the drawer on cancel', () => {
renderWithTheme(<CreateFirewallDrawer {...props} />);
userEvent.click(screen.getByTestId('cancel'));
expect(props.onClose).toHaveBeenCalledTimes(1);
});

it('should render a title', () => {
renderWithTheme(<CreateFirewallDrawer {...props} />);
const title = within(screen.getByTestId('drawer-title')).getByText(
Expand All @@ -29,4 +35,23 @@ describe('Create Firewall Drawer', () => {
);
expect(error).toBeInTheDocument();
});

it('should be able to submit when fields are filled out correctly', async () => {
const { getByTestId } = renderWithTheme(
<CreateFirewallDrawer {...props} />
);

act(() => {
userEvent.type(screen.getByLabelText('Label'), 'test label');
userEvent.type(screen.getByLabelText('Linodes'), 'test linode');
userEvent.type(
screen.getByLabelText('NodeBalancers'),
'test nodebalancer'
);

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

await waitFor(() => expect(props.onClose).toHaveBeenCalledTimes(1));
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {
CreateFirewallPayload,
FirewallDeviceEntityType,
} from '@linode/api-v4/lib/firewalls';
/* eslint-disable jsx-a11y/anchor-is-valid */
import { CreateFirewallPayload } from '@linode/api-v4/lib/firewalls';
import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers';
import { CreateFirewallSchema } from '@linode/validation/lib/firewalls.schema';
import { useFormik } from 'formik';
import * as React from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
import { Box } from 'src/components/Box';
import { Drawer } from 'src/components/Drawer';
import { Notice } from 'src/components/Notice/Notice';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect';
import { useAccountManagement } from 'src/hooks/useAccountManagement';
import { useCreateFirewall } from 'src/queries/firewalls';
import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers';
import { useGrants } from 'src/queries/profile';
import { getErrorMap } from 'src/utilities/errorUtils';
import {
Expand All @@ -21,12 +24,8 @@ import {
} from 'src/utilities/formikErrorUtils';
import { getEntityIdsByPermission } from 'src/utilities/grants';

import { formattedTypes } from '../FirewallDetail/Devices/FirewallDeviceLanding';

export const READ_ONLY_DEVICES_HIDDEN_MESSAGE = (
deviceType: FirewallDeviceEntityType
) =>
`Only ${formattedTypes[deviceType]}s you have permission to modify are shown.`;
export const READ_ONLY_DEVICES_HIDDEN_MESSAGE =
'Only Devices you have permission to modify are shown.';

export interface CreateFirewallDrawerProps {
onClose: () => void;
Expand All @@ -36,6 +35,7 @@ export interface CreateFirewallDrawerProps {
const initialValues: CreateFirewallPayload = {
devices: {
linodes: [],
nodebalancers: [],
},
label: '',
rules: {
Expand All @@ -44,17 +44,19 @@ const initialValues: CreateFirewallPayload = {
},
};

const firewallLabelText = `Assign devices to the Firewall.`;
const firewallHelperText = `Assign one or more devices to this firewall. You can add devices later if you want to customize your rules first.`;
const nodebalancerHelperText = `Only the Firewall's inbound rules apply to NodeBalancers.`;
coliu-akamai marked this conversation as resolved.
Show resolved Hide resolved

export const CreateFirewallDrawer = React.memo(
(props: CreateFirewallDrawerProps) => {
const { onClose, open } = props;

/**
* We'll eventually want to check the read_write firewall
* grant here too, but it doesn't exist yet.
coliu-akamai marked this conversation as resolved.
Show resolved Hide resolved
*/
const { _hasGrant, _isRestrictedUser } = useAccountManagement();
const { data: grants } = useGrants();

const { mutateAsync } = useCreateFirewall();

const {
Expand Down Expand Up @@ -119,11 +121,31 @@ export const CreateFirewallDrawer = React.memo(
validationSchema: CreateFirewallSchema,
});

const [selectedNodeBalancers, setSelectedNodeBalancers] = React.useState<
carrillo-erik marked this conversation as resolved.
Show resolved Hide resolved
NodeBalancer[]
>([]);

const handleNodeBalancerChange = (nodebalancers: NodeBalancer[]) => {
if (nodebalancers.length > 0) {
setSelectedNodeBalancers(
nodebalancers.map((nodebalancer) => nodebalancer)
);
setFieldValue(
'devices.nodebalancers',
nodebalancers.map((nodebalancer) => nodebalancer.id)
);
} else {
setSelectedNodeBalancers([]);
setFieldValue('devices.nodebalancers', selectedNodeBalancers);
}
};
coliu-akamai marked this conversation as resolved.
Show resolved Hide resolved

carrillo-erik marked this conversation as resolved.
Show resolved Hide resolved
React.useEffect(() => {
if (open) {
resetForm();
setSelectedNodeBalancers([]);
}
}, [open]);
}, [open, resetForm]);

const userCannotAddFirewall =
_isRestrictedUser && !_hasGrant('add_firewalls');
Expand All @@ -133,14 +155,32 @@ export const CreateFirewallDrawer = React.memo(
? getEntityIdsByPermission(grants, 'linode', 'read_only')
: [];

const linodeSelectGuidance =
readOnlyLinodeIds.length > 0
? READ_ONLY_DEVICES_HIDDEN_MESSAGE('linode')
: undefined;
// If a user is restricted, they can not add a read-only NodeBalancer to a firewall.
const readOnlyNodebalancerIds = _isRestrictedUser
? getEntityIdsByPermission(grants, 'nodebalancer', 'read_only')
: [];

const firewallHelperText = `Assign one or more Linodes to this firewall. You can add Linodes later if you want to customize your rules first. ${
linodeSelectGuidance ? linodeSelectGuidance : ''
}`;
const deviceSelectGuidance =
readOnlyLinodeIds.length > 0 || readOnlyNodebalancerIds.length > 0
? READ_ONLY_DEVICES_HIDDEN_MESSAGE
: null;

const optionsFilter = (nodebalancer: NodeBalancer) => {
return ![...readOnlyNodebalancerIds, ...selectedNodeBalancers].includes(
nodebalancer.id
);
};
coliu-akamai marked this conversation as resolved.
Show resolved Hide resolved

const {
data,
error: nodebalancerError,
isLoading: nodebalancerIsLoading,
} = useAllNodeBalancersQuery();

const nodebalancers = data?.filter(optionsFilter);

// TODO: NBFW - Placeholder until real link is available
const learnMoreLink = <a href="#">Learn more</a>;

const generalError =
status?.generalError ||
Expand Down Expand Up @@ -178,6 +218,29 @@ export const CreateFirewallDrawer = React.memo(
onChange={handleChange}
value={values.label}
/>
<Box>
<Typography
sx={(theme) => ({
margin: `${theme.spacing(2)} ${theme.spacing(0)}`,
})}
variant="h3"
>
{firewallLabelText}
</Typography>
<Typography>
{firewallHelperText}
{deviceSelectGuidance ? ` ${deviceSelectGuidance}` : null}
</Typography>
<Typography
sx={(theme) => ({
margin: `${theme.spacing(2)} ${theme.spacing(0)}`,
})}
>
{nodebalancerHelperText}
<br />
{learnMoreLink}
</Typography>
</Box>
<LinodeSelect
carrillo-erik marked this conversation as resolved.
Show resolved Hide resolved
onSelectionChange={(selected) =>
setFieldValue(
Expand All @@ -187,12 +250,28 @@ export const CreateFirewallDrawer = React.memo(
}
disabled={userCannotAddFirewall}
errorText={errors['devices.linodes']}
helperText={firewallHelperText}
multiple
onBlur={handleBlur}
optionsFilter={(linode) => !readOnlyLinodeIds.includes(linode.id)}
value={values.devices?.linodes ?? []}
/>
<Autocomplete
onChange={(_, nodebalancers) =>
handleNodeBalancerChange(nodebalancers)
}
sx={(theme) => ({
marginTop: theme.spacing(2),
})}
disabled={userCannotAddFirewall || !!nodebalancerError}
errorText={errors['devices.nodebalancers']}
label="NodeBalancers"
loading={nodebalancerIsLoading}
multiple
noMarginTop={false}
noOptionsText="No NodeBalancers available to add"
options={nodebalancers || []}
value={selectedNodeBalancers}
/>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const FirewallLanding = () => {
</TableSortCell>
<Hidden smDown>
<TableCell>Rules</TableCell>
<TableCell>Linodes</TableCell>
<TableCell>Devices</TableCell>
</Hidden>
<TableCell></TableCell>
</TableRow>
Expand Down