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 all 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 (0)').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,11 @@ 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.`;
const FIREWALL_LABEL_TEXT = `Assign devices to the Firewall.`;
const FIREWALL_HELPER_TEXT = `Assign one or more devices to this firewall. You can add devices later if you want to customize your rules first.`;
const NODEBALANCER_HELPER_TEXT = `Only the Firewall's inbound rules apply to NodeBalancers.`;
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 +38,7 @@ export interface CreateFirewallDrawerProps {
const initialValues: CreateFirewallPayload = {
devices: {
linodes: [],
nodebalancers: [],
},
label: '',
rules: {
Expand All @@ -46,15 +49,10 @@ const initialValues: CreateFirewallPayload = {

export const CreateFirewallDrawer = React.memo(
(props: CreateFirewallDrawerProps) => {
// TODO: NBFW - We'll eventually want to check the read_write firewall grant here too, but it doesn't exist yet.
const { onClose, open } = props;

/**
* We'll eventually want to check the read_write firewall
* grant here too, but it doesn't exist yet.
*/
const { _hasGrant, _isRestrictedUser } = useAccountManagement();
const { data: grants } = useGrants();

const { mutateAsync } = useCreateFirewall();

const {
Expand Down Expand Up @@ -122,8 +120,19 @@ export const CreateFirewallDrawer = React.memo(
React.useEffect(() => {
if (open) {
resetForm();
setSelectedNodeBalancers([]);
}
}, [open]);
}, [open, resetForm]);

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

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

carrillo-erik marked this conversation as resolved.
Show resolved Hide resolved
const userCannotAddFirewall =
_isRestrictedUser && !_hasGrant('add_firewalls');
Expand All @@ -133,14 +142,29 @@ 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 deviceSelectGuidance =
readOnlyLinodeIds.length > 0 || readOnlyNodebalancerIds.length > 0
? READ_ONLY_DEVICES_HIDDEN_MESSAGE
: null;

const optionsFilter = (nodebalancer: NodeBalancer) => {
const selectedNodeBalancersIds = selectedNodeBalancers.map((nb) => nb.id);

return ![
...readOnlyNodebalancerIds,
...selectedNodeBalancersIds,
].includes(nodebalancer.id);
};

const nodebalancers = data?.filter(optionsFilter);

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 : ''
}`;
// 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 +202,29 @@ export const CreateFirewallDrawer = React.memo(
onChange={handleChange}
value={values.label}
/>
<Box>
<Typography
sx={(theme) => ({
margin: `${theme.spacing(2)} ${theme.spacing(0)}`,
})}
variant="h3"
>
{FIREWALL_LABEL_TEXT}
</Typography>
<Typography>
{FIREWALL_HELPER_TEXT}
{deviceSelectGuidance ? ` ${deviceSelectGuidance}` : null}
</Typography>
<Typography
sx={(theme) => ({
margin: `${theme.spacing(2)} ${theme.spacing(0)}`,
})}
>
{NODEBALANCER_HELPER_TEXT}
<br />
{learnMoreLink}
</Typography>
</Box>
<LinodeSelect
carrillo-erik marked this conversation as resolved.
Show resolved Hide resolved
onSelectionChange={(selected) =>
setFieldValue(
Expand All @@ -187,12 +234,33 @@ 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) => {
setFieldValue(
'devices.nodebalancers',
nodebalancers.map((nodebalancer) => nodebalancer.id)
);
setSelectedNodeBalancers(nodebalancers);
}}
sx={(theme) => ({
marginTop: theme.spacing(2),
})}
disabled={userCannotAddFirewall || !!nodebalancerError}
errorText={errors['devices.nodebalancers']}
isOptionEqualToValue={optionsFilter}
label="NodeBalancers"
loading={nodebalancerIsLoading}
multiple
noMarginTop={false}
noOptionsText="No NodeBalancers available to add"
options={nodebalancers || []}
value={selectedNodeBalancers}
/>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
Expand Down