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-7019] - Create add Nodebalancer to Firewall drawer #9608

Merged
merged 32 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a5cc2d9
initial drawer commit
TylerWJ Aug 30, 2023
8bf23e6
finished the add nodebalancer drawer
TylerWJ Aug 30, 2023
f4f2512
Added changeset: Created 'Add Nodebalancer' to Firewall drawer
TylerWJ Aug 30, 2023
c5ad21d
updated drawer and started to add event handlers
TylerWJ Aug 30, 2023
23dcc57
added toast notification
TylerWJ Aug 30, 2023
3cd6d7f
added multiple toast notifications
TylerWJ Aug 30, 2023
c5f87f9
Merge branch 'feature/firewall-nodebalancer' into M3-7019-Create-Add-…
TylerWJ Aug 31, 2023
2366ef7
Merge branch 'feature/firewall-nodebalancer' into M3-7019-Create-Add-…
TylerWJ Sep 1, 2023
47ae41f
separated nodebalancer and linode drawer
TylerWJ Sep 1, 2023
7e7c861
added infinite scrolling
TylerWJ Sep 1, 2023
6822f99
fixed pr suggestions
TylerWJ Sep 6, 2023
6d59e9e
partially eliminated type definitions in LinodeSelect
TylerWJ Sep 6, 2023
e01c79e
eliminated type definitions in LinodeSelect
TylerWJ Sep 6, 2023
5cbc9da
changed type definition of onSelectionChange in NodeBalancerSelect
TylerWJ Sep 6, 2023
6451bdd
eliminated type declarations in NodeBalancerSelect
TylerWJ Sep 6, 2023
8c8423e
erased commented out code
TylerWJ Sep 6, 2023
f59fa25
initial round of fixes
TylerWJ Sep 6, 2023
9cd1ac3
fixed linode error drawer not closing
TylerWJ Sep 6, 2023
a6bfbfe
eliminated event message
TylerWJ Sep 7, 2023
33bba73
added todo comment
TylerWJ Sep 7, 2023
697d5fd
added todo comment
TylerWJ Sep 7, 2023
eb2c4b4
fixed toast notification, error reset, and error text
TylerWJ Sep 7, 2023
5c5baf8
fixed toast notification, error reset, and error text for Linode Drawer
TylerWJ Sep 8, 2023
c9533de
fixed nodebalancer drawer error notices
TylerWJ Sep 10, 2023
4cf314f
Merge branch 'feature/firewall-nodebalancer' into M3-7019-Create-Add-…
TylerWJ Sep 10, 2023
c70fa4d
merged with develop
TylerWJ Sep 10, 2023
b91edfb
initial migration to new autocomplete component, still some errors
TylerWJ Sep 10, 2023
ac3f050
can select linodes now, but the linodes arent showing as selected
TylerWJ Sep 10, 2023
64ed737
fixed selection issue with autocomplete
TylerWJ Sep 10, 2023
3cc936b
migrated to new autocomplete component
TylerWJ Sep 11, 2023
bdd6767
remove NodeBalancerSelect file changes
TylerWJ Sep 11, 2023
ce25f88
added banks PR suggestions
TylerWJ Sep 11, 2023
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
---

Created 'Add Nodebalancer' to Firewall drawer ([#9608](https://github.com/linode/manager/pull/9608))
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,13 @@ export const CreateDomain = () => {
defaultRecordsSetting.value === 'nodebalancer' && (
<React.Fragment>
<NodeBalancerSelect
onChange={(_, nodebalancer) =>
onSelectionChange={(nodebalancer) =>
setSelectedDefaultNodeBalancer(nodebalancer)
}
disabled={disabled}
error={errorMap.defaultNodeBalancer}
value={selectedDefaultNodeBalancer?.id}
errorText={errorMap.defaultNodeBalancer}
multiple={false}
value={selectedDefaultNodeBalancer?.id ?? null}
/>
{!errorMap.defaultNodeBalancer && (
<FormHelperText>
Expand Down
4 changes: 4 additions & 0 deletions packages/manager/src/features/Events/eventMessageGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@ export const eventMessageCreators: { [index: string]: CreatorsForStatus } = {
managed_service_delete: {
notification: (e) => `Managed service ${e.entity!.label} has been deleted.`,
},
nodebalancer_added_to_firewall: {
notification: (e) =>
`NodeBalancer ${e.entity!.label} has been added to Firewall.`,
},
nodebalancer_config_create: {
notification: (e) =>
`A config on NodeBalancer ${e.entity!.label} has been created.`,
Expand Down
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTheme } from '@mui/material/styles';
import { useSnackbar } from 'notistack';
import * as React from 'react';
import { useParams } from 'react-router-dom';

Expand All @@ -8,6 +9,7 @@ import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { SupportLink } from 'src/components/SupportLink';
import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect';
import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect';
import {
useAddFirewallDeviceMutation,
useAllFirewallDevicesQuery,
Expand All @@ -17,15 +19,23 @@ import { useGrants, useProfile } from 'src/queries/profile';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import { getEntityIdsByPermission } from 'src/utilities/grants';

import { READ_ONLY_LINODES_HIDDEN_MESSAGE } from '../../FirewallLanding/CreateFirewallDrawer';
import { formattedTypes } from './FirewallDeviceLanding';

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

interface Props {
onClose: () => void;
open: boolean;
type: FirewallDeviceEntityType;
}

const helperText =
'Assign one or more devices to this firewall. You can add devices later if you want to customize your rules first.';

export const AddDeviceDrawer = (props: Props) => {
const { onClose, open } = props;
const { onClose, open, type } = props;

const { enqueueSnackbar } = useSnackbar();

const { id } = useParams<{ id: string }>();

Expand All @@ -39,10 +49,15 @@ export const AddDeviceDrawer = (props: Props) => {
isLoading: currentDevicesLoading,
} = useAllFirewallDevicesQuery(Number(id));

const currentLinodeIds =
currentDevices
?.filter((device) => device.entity.type === 'linode')
.map((device) => device.entity.id) ?? [];
const getEntityIdsByType = (entityType: FirewallDeviceEntityType) => {
return (
currentDevices
?.filter((device) => device.entity.type === entityType)
.map((device) => device.entity.id) ?? []
);
};

const currentDeviceIds = getEntityIdsByType(type);

const {
error,
Expand All @@ -51,33 +66,48 @@ export const AddDeviceDrawer = (props: Props) => {
} = useAddFirewallDeviceMutation(Number(id));
const theme = useTheme();

const [selectedLinodeIds, setSelectedLinodeIds] = React.useState<number[]>(
const [selectedDeviceIds, setSelectedDeviceIds] = React.useState<number[]>(
[]
);

const handleSubmit = async () => {
await Promise.all(
selectedLinodeIds.map((id) => addDevice({ id, type: 'linode' }))
const results = await Promise.allSettled(
selectedDeviceIds.map((id) => addDevice({ id, type }))
);

results.forEach((result, _) => {
if (result.status === 'fulfilled') {
// Assuming the response contains the device label, replace with the appropriate property if not.
const label = result.value.entity.label;
enqueueSnackbar(`${label} added successfully.`, { variant: 'success' });
} else {
// Assuming the error object contains the device label, replace with the appropriate property if not.
const errorLabel = result.reason.label;
enqueueSnackbar(`Failed to add ${errorLabel}.`, { variant: 'error' });
}
});

onClose();
setSelectedLinodeIds([]);
setSelectedDeviceIds([]);
};

// @todo title and error messaging will update to "Device" once NodeBalancers are allowed
const errorMessage = error
? getAPIErrorOrDefault(error, 'Error adding Linode')[0].reason
? getAPIErrorOrDefault(error, `Error adding ${formattedTypes[type]}`)[0]
.reason
: undefined;

// @todo update regex once error messaging updates
const errorNotice = (errorMsg: string) => {
// match something like: Linode <linode_label> (ID <linode_id>)
const linode = /linode (.+?) \(id ([^()]*)\)/i.exec(errorMsg);
const device = /(linode|nodebalancer) (.+?) \(id ([^()]*)\)/i.exec(
errorMsg
);
const openTicket = errorMsg.match(/open a support ticket\./i);
if (openTicket) {
errorMsg = errorMsg.replace(/open a support ticket\./i, '');
}
if (linode) {
const [, label, id] = linode;
if (device) {
const [, label, id] = device;
const labelIndex = errorMsg.indexOf(label);
errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, '');
return (
Expand All @@ -90,7 +120,7 @@ export const AddDeviceDrawer = (props: Props) => {
variant="error"
>
{errorMsg.substring(0, labelIndex)}
<Link to={`/linodes/${id}`}>{label}</Link>
<Link to={`/${type}s/${id}`}>{label}</Link>
{errorMsg.substring(labelIndex + label.length)}
{openTicket ? (
<>
Expand All @@ -105,18 +135,22 @@ export const AddDeviceDrawer = (props: Props) => {
};

// If a user is restricted, they can not add a read-only Linode to a firewall.
const readOnlyLinodeIds = isRestrictedUser
? getEntityIdsByPermission(grants, 'linode', 'read_only')
: [];
const getReadOnlyEntityIds = (entityType: FirewallDeviceEntityType) => {
return isRestrictedUser
? getEntityIdsByPermission(grants, entityType, 'read_only')
: [];
};

const linodeSelectGuidance =
readOnlyLinodeIds.length > 0 ? READ_ONLY_LINODES_HIDDEN_MESSAGE : undefined;
const readOnlyDeviceIds = getReadOnlyEntityIds(type);

return (
<Drawer
onClose={onClose}
onClose={() => {
setSelectedDeviceIds([]);
onClose();
}}
open={open}
title={`Add Linode to Firewall: ${firewall?.label}`}
title={`Add ${formattedTypes[type]} to Firewall: ${firewall?.label}`}
>
<form
onSubmit={(e: React.ChangeEvent<HTMLFormElement>) => {
Expand All @@ -125,26 +159,45 @@ export const AddDeviceDrawer = (props: Props) => {
}}
>
{errorMessage ? errorNotice(errorMessage) : null}
<LinodeSelect
helperText={`You can assign one or more Linodes to this Firewall. Each Linode can only be assigned to a single Firewall. ${
linodeSelectGuidance ? linodeSelectGuidance : ''
}`}
onSelectionChange={(linodes) =>
setSelectedLinodeIds(linodes.map((linode) => linode.id))
}
optionsFilter={(linode) =>
![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id)
}
disabled={currentDevicesLoading}
loading={currentDevicesLoading}
multiple
noOptionsMessage="No Linodes available to add"
value={selectedLinodeIds}
/>
{type === 'linode' ? (
<LinodeSelect
onSelectionChange={(linodes) =>
setSelectedDeviceIds(linodes.map((linode) => linode.id))
}
optionsFilter={(linode) =>
![...currentDeviceIds, ...readOnlyDeviceIds].includes(linode.id)
}
disabled={currentDevicesLoading}
helperText={helperText}
loading={currentDevicesLoading}
multiple
noOptionsMessage={`No ${formattedTypes[type]}s available to add`}
value={selectedDeviceIds}
/>
) : (
<NodeBalancerSelect
onSelectionChange={(nodebalancers) =>
setSelectedDeviceIds(
nodebalancers.map((nodebalancer) => nodebalancer.id)
)
}
optionsFilter={(nodebalancer) =>
![...currentDeviceIds, ...readOnlyDeviceIds].includes(
nodebalancer.id
)
}
disabled={currentDevicesLoading}
helperText={helperText}
loading={currentDevicesLoading}
multiple
noOptionsMessage={`No ${formattedTypes[type]}s available to add`}
value={selectedDeviceIds}
/>
)}
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
disabled: selectedLinodeIds.length === 0,
disabled: selectedDeviceIds.length === 0,
label: 'Add',
loading: isLoading,
onClick: handleSubmit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { styled } from '@mui/material/styles';
import * as React from 'react';

import { Button } from 'src/components/Button/Button';
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { Typography } from 'src/components/Typography';
import { useAllFirewallDevicesQuery } from 'src/queries/firewalls';
Expand All @@ -20,7 +21,7 @@ export interface FirewallDeviceLandingProps {
type: FirewallDeviceEntityType;
}

const formattedTypes = {
export const formattedTypes = {
linode: 'Linode',
nodebalancer: 'NodeBalancer',
};
Expand Down Expand Up @@ -69,11 +70,14 @@ export const FirewallDeviceLanding = React.memo(
/>
) : null}
<Grid container direction="column">
<Grid style={{ paddingBottom: 0 }}>
<Grid sx={{ paddingBottom: 0, width: 'calc(100% - 300px)' }}>
<StyledTypography>
The following {formattedType}s have been assigned to this
Firewall. A {formattedType} can only be assigned to a single
Firewall.
Firewall.{' '}
<Link to="#">
Learn about how Firewall rules apply to {formattedType}s.
</Link>
</StyledTypography>
</Grid>
<StyledGrid>
Expand All @@ -97,7 +101,11 @@ export const FirewallDeviceLanding = React.memo(
error={error ?? undefined}
loading={isLoading}
/>
<AddDeviceDrawer onClose={handleClose} open={addDeviceDrawerOpen} />
<AddDeviceDrawer
onClose={handleClose}
open={addDeviceDrawerOpen}
type={type}
/>
<RemoveDeviceDialog
device={selectedDevice}
firewallId={firewallID}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { CreateFirewallPayload } from '@linode/api-v4/lib/firewalls';
import {
CreateFirewallPayload,
FirewallDeviceEntityType,
} from '@linode/api-v4/lib/firewalls';
import { CreateFirewallSchema } from '@linode/validation/lib/firewalls.schema';
import { useFormik } from 'formik';
import * as React from 'react';
Expand All @@ -18,8 +21,12 @@ import {
} from 'src/utilities/formikErrorUtils';
import { getEntityIdsByPermission } from 'src/utilities/grants';

export const READ_ONLY_LINODES_HIDDEN_MESSAGE =
'Only Linodes you have permission to modify are shown.';
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 interface CreateFirewallDrawerProps {
onClose: () => void;
Expand Down Expand Up @@ -128,7 +135,7 @@ export const CreateFirewallDrawer = React.memo(

const linodeSelectGuidance =
readOnlyLinodeIds.length > 0
? READ_ONLY_LINODES_HIDDEN_MESSAGE
? READ_ONLY_DEVICES_HIDDEN_MESSAGE('linode')
: undefined;

const firewallHelperText = `Assign one or more Linodes to this firewall. You can add Linodes later if you want to customize your rules first. ${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React from 'react';
import { Box } from 'src/components/Box';
import { TextField } from 'src/components/TextField';
import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import { mapIdsToLinodes } from 'src/utilities/mapIdsToLinodes';
import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices';

import { CustomPopper, SelectedIcon } from './LinodeSelect.styles';

Expand Down Expand Up @@ -126,8 +126,10 @@ export const LinodeSelect = (
}
onChange={(_, value) =>
multiple && Array.isArray(value)
? onSelectionChange(value)
: !multiple && !Array.isArray(value) && onSelectionChange(value)
? onSelectionChange(value as Linode[])
: !multiple &&
!Array.isArray(value) &&
onSelectionChange(value as Linode)
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
}
renderInput={(params) => (
<TextField
Expand All @@ -151,7 +153,7 @@ export const LinodeSelect = (
return (
<li {...props}>
{renderOption ? (
renderOption(option, selected)
renderOption(option as Linode, selected)
) : (
<>
<Box
Expand All @@ -172,7 +174,7 @@ export const LinodeSelect = (
? multiple && Array.isArray(value)
? linodes?.filter(value) ?? null
: linodes?.find(value) ?? null
: mapIdsToLinodes(value, linodes)
: mapIdsToDevices(value, linodes)
}
ChipProps={{ deleteIcon: <CloseIcon /> }}
PopperComponent={CustomPopper}
Expand Down
Loading