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 10 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
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 @@ -17,18 +18,19 @@ 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';

interface Props {
helperText: string;
onClose: () => void;
open: boolean;
}

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

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

const { enqueueSnackbar } = useSnackbar();

const { data: grants } = useGrants();
const { data: profile } = useProfile();
const isRestrictedUser = Boolean(profile?.restricted);
Expand Down Expand Up @@ -56,9 +58,22 @@ export const AddDeviceDrawer = (props: Props) => {
);

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

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;
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
enqueueSnackbar(`Failed to add ${errorLabel}.`, { variant: 'error' });
}
});

onClose();
setSelectedLinodeIds([]);
};
Expand Down Expand Up @@ -109,12 +124,12 @@ export const AddDeviceDrawer = (props: Props) => {
? getEntityIdsByPermission(grants, 'linode', 'read_only')
: [];

const linodeSelectGuidance =
readOnlyLinodeIds.length > 0 ? READ_ONLY_LINODES_HIDDEN_MESSAGE : undefined;

return (
<Drawer
onClose={onClose}
onClose={() => {
setSelectedLinodeIds([]);
onClose();
}}
open={open}
title={`Add Linode to Firewall: ${firewall?.label}`}
>
Expand All @@ -126,16 +141,14 @@ 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}
helperText={helperText}
loading={currentDevicesLoading}
multiple
noOptionsMessage="No Linodes available to add"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { useTheme } from '@mui/material/styles';
import { useSnackbar } from 'notistack';
import * as React from 'react';
import { useParams } from 'react-router-dom';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { SupportLink } from 'src/components/SupportLink';
import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect';
import {
useAddFirewallDeviceMutation,
useAllFirewallDevicesQuery,
useFirewallQuery,
} from 'src/queries/firewalls';
import { useGrants, useProfile } from 'src/queries/profile';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import { getEntityIdsByPermission } from 'src/utilities/grants';

interface Props {
helperText: string;
onClose: () => void;
open: boolean;
}

export const AddNodebalancerDrawer = (props: Props) => {
const { helperText, onClose, open } = props;

const { enqueueSnackbar } = useSnackbar();

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

const { data: grants } = useGrants();
const { data: profile } = useProfile();
const isRestrictedUser = Boolean(profile?.restricted);

const { data: firewall } = useFirewallQuery(Number(id));
const {
data: currentDevices,
isLoading: currentDevicesLoading,
} = useAllFirewallDevicesQuery(Number(id));

const currentNodebalancerIds =
currentDevices
?.filter((device) => device.entity.type === 'nodebalancer')
.map((device) => device.entity.id) ?? [];

const {
error,
isLoading,
mutateAsync: addDevice,
} = useAddFirewallDeviceMutation(Number(id));
const theme = useTheme();

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

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

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();
setSelectedDeviceIds([]);
};

const errorMessage = error
? getAPIErrorOrDefault(error, `Error adding Nodebalancer`)[0].reason
: undefined;

// @todo update regex once error messaging updates
const errorNotice = (errorMsg: string) => {
// match something like: Nodebalancer <nodebalancer_label> (ID <nodebalancer_id>)
const device = /(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 (device) {
const [, label, id] = device;
const labelIndex = errorMsg.indexOf(label);
errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, '');
return (
<Notice
sx={{
fontFamily: theme.font.bold,
fontSize: '1rem',
lineHeight: '20px',
}}
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
variant="error"
>
{errorMsg.substring(0, labelIndex)}
<Link to={`/nodebalancers/${id}`}>{label}</Link>
{errorMsg.substring(labelIndex + label.length)}
{openTicket ? (
<>
<SupportLink text="open a Support ticket" />.
</>
) : null}
</Notice>
);
} else {
return <Notice text={errorMessage} variant="error" />;
}
};

// If a user is restricted, they can not add a read-only Nodebalancer to a firewall.
const readOnlyNodebalancerIds = isRestrictedUser
? getEntityIdsByPermission(grants, 'nodebalancer', 'read_only')
: [];

return (
<Drawer
onClose={() => {
setSelectedDeviceIds([]);
onClose();
}}
open={open}
title={`Add Nodebalancer to Firewall: ${firewall?.label}`}
>
<Notice variant={'warning'}>
Only the Firewall's inbound rules apply to NodeBalancers. Any existing
outbound rules won't be applied.
{/* add documentation link */}
<Link to="#"> Learn more.</Link>
</Notice>
<form
onSubmit={(e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault();
handleSubmit();
}}
>
{errorMessage ? errorNotice(errorMessage) : null}
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
<NodeBalancerSelect
onSelectionChange={(nodebalancers) =>
setSelectedDeviceIds(
nodebalancers.map((nodebalancer) => nodebalancer.id)
)
}
optionsFilter={(nodebalancer) =>
![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes(
nodebalancer.id
)
}
disabled={currentDevicesLoading}
helperText={helperText}
loading={currentDevicesLoading}
multiple
value={selectedDeviceIds}
/>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
disabled: selectedDeviceIds.length === 0,
label: 'Add',
loading: isLoading,
onClick: handleSubmit,
}}
secondaryButtonProps={{
'data-testid': 'cancel',
tyler-akamai marked this conversation as resolved.
Show resolved Hide resolved
label: 'Cancel',
onClick: onClose,
}}
/>
</form>
</Drawer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ 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';

import { AddDeviceDrawer } from './AddDeviceDrawer';
import { AddLinodeDrawer } from './AddLinodeDrawer';
import { AddNodebalancerDrawer } from './AddNodebalancerDrawer';
import { FirewallDevicesTable } from './FirewallDevicesTable';
import { RemoveDeviceDialog } from './RemoveDeviceDialog';

Expand All @@ -20,11 +22,14 @@ export interface FirewallDeviceLandingProps {
type: FirewallDeviceEntityType;
}

const formattedTypes = {
export const formattedTypes = {
linode: 'Linode',
nodebalancer: 'NodeBalancer',
};

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 FirewallDeviceLanding = React.memo(
(props: FirewallDeviceLandingProps) => {
const { disabled, firewallID, firewallLabel, type } = props;
Expand Down Expand Up @@ -69,11 +74,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 +105,19 @@ export const FirewallDeviceLanding = React.memo(
error={error ?? undefined}
loading={isLoading}
/>
<AddDeviceDrawer onClose={handleClose} open={addDeviceDrawerOpen} />
{type === 'linode' ? (
<AddLinodeDrawer
helperText={helperText}
onClose={handleClose}
open={addDeviceDrawerOpen}
/>
) : (
<AddNodebalancerDrawer
helperText={helperText}
onClose={handleClose}
open={addDeviceDrawerOpen}
/>
)}
<RemoveDeviceDialog
device={selectedDevice}
firewallId={firewallID}
Expand Down
Loading