From a5cc2d9e6d175155e82c4bd994351a8996dc9e89 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Tue, 29 Aug 2023 20:09:21 -0400 Subject: [PATCH 01/29] initial drawer commit --- .../Domains/CreateDomain/CreateDomain.tsx | 7 +- .../Devices/AddDeviceDrawer.tsx | 83 +++++--- .../Devices/FirewallDeviceLanding.tsx | 8 +- .../Linodes/LinodeSelect/LinodeSelect.tsx | 12 +- .../NodeBalancers/NodeBalancerSelect.tsx | 196 ++++++++++++++---- ...inodes.test.ts => mapIdsToDevices.test.ts} | 10 +- .../manager/src/utilities/mapIdsToDevices.ts | 19 ++ .../manager/src/utilities/mapIdsToLinodes.ts | 17 -- 8 files changed, 257 insertions(+), 95 deletions(-) rename packages/manager/src/utilities/{mapIdsToLinodes.test.ts => mapIdsToDevices.test.ts} (57%) create mode 100644 packages/manager/src/utilities/mapIdsToDevices.ts delete mode 100644 packages/manager/src/utilities/mapIdsToLinodes.ts diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 0aaf0db5cbd..ae4247ea6d9 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -413,12 +413,13 @@ export const CreateDomain = () => { defaultRecordsSetting.value === 'nodebalancer' && ( + onSelectionChange={(nodebalancer) => setSelectedDefaultNodeBalancer(nodebalancer) } disabled={disabled} - error={errorMap.defaultNodeBalancer} - value={selectedDefaultNodeBalancer?.id} + errorText={errorMap.defaultNodeBalancer} + multiple={false} + value={selectedDefaultNodeBalancer?.id ?? null} /> {!errorMap.defaultNodeBalancer && ( diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index fa35fc730ad..700ece5621b 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -8,6 +8,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, @@ -18,14 +19,18 @@ 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; } export const AddDeviceDrawer = (props: Props) => { - const { onClose, open } = props; + const { onClose, open, type } = props; const { id } = useParams<{ id: string }>(); @@ -44,6 +49,11 @@ export const AddDeviceDrawer = (props: Props) => { ?.filter((device) => device.entity.type === 'linode') .map((device) => device.entity.id) ?? []; + const currentNodebalancerIds = + currentDevices + ?.filter((device) => device.entity.type === 'nodebalancer') + .map((device) => device.entity.id) ?? []; + const { error, isLoading, @@ -51,16 +61,14 @@ export const AddDeviceDrawer = (props: Props) => { } = useAddFirewallDeviceMutation(Number(id)); const theme = useTheme(); - const [selectedLinodeIds, setSelectedLinodeIds] = React.useState( + const [selectedDeviceIds, setSelectedDeviceIds] = React.useState( [] ); const handleSubmit = async () => { - await Promise.all( - selectedLinodeIds.map((id) => addDevice({ id, type: 'linode' })) - ); + await Promise.all(selectedDeviceIds.map((id) => addDevice({ id, type }))); onClose(); - setSelectedLinodeIds([]); + setSelectedDeviceIds([]); }; // @todo title and error messaging will update to "Device" once NodeBalancers are allowed @@ -109,6 +117,10 @@ export const AddDeviceDrawer = (props: Props) => { ? getEntityIdsByPermission(grants, 'linode', 'read_only') : []; + const readOnlyNodebalancerIds = isRestrictedUser + ? getEntityIdsByPermission(grants, 'nodebalancer', 'read_only') + : []; + const linodeSelectGuidance = readOnlyLinodeIds.length > 0 ? READ_ONLY_LINODES_HIDDEN_MESSAGE : undefined; @@ -116,7 +128,7 @@ export const AddDeviceDrawer = (props: Props) => {
) => { @@ -125,26 +137,49 @@ export const AddDeviceDrawer = (props: Props) => { }} > {errorMessage ? errorNotice(errorMessage) : null} - - 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' ? ( + + setSelectedDeviceIds(linodes.map((linode) => linode.id)) + } + optionsFilter={(linode) => + ![...currentLinodeIds, ...readOnlyLinodeIds].includes(linode.id) + } + disabled={currentDevicesLoading} + loading={currentDevicesLoading} + multiple + noOptionsMessage={`No ${formattedTypes[type]}s available to add`} + value={selectedDeviceIds} + /> + ) : ( + + setSelectedDeviceIds( + nodebalancers.map((nodebalancer) => nodebalancer.id) + ) + } + optionsFilter={(nodebalancer) => + ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( + nodebalancer.id + ) + } + disabled={currentDevicesLoading} + loading={currentDevicesLoading} + multiple + noOptionsMessage={`No ${formattedTypes[type]}s available to add`} + value={selectedDeviceIds} + /> + )} - + multiple && Array.isArray(value) - ? onSelectionChange(value) - : !multiple && !Array.isArray(value) && onSelectionChange(value) + ? onSelectionChange(value as Linode[]) + : !multiple && + !Array.isArray(value) && + onSelectionChange(value as Linode) } renderInput={(params) => ( {renderOption ? ( - renderOption(option, selected) + renderOption(option as Linode, selected) ) : ( <> }} PopperComponent={CustomPopper} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index 9cf65b06c48..a06f42ef835 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -1,21 +1,72 @@ -import { NodeBalancer } from '@linode/api-v4'; +import { APIError, NodeBalancer } from '@linode/api-v4'; +import CloseIcon from '@mui/icons-material/Close'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import Autocomplete from '@mui/material/Autocomplete'; import * as React from 'react'; -import { TextField, TextFieldProps } from 'src/components/TextField'; +import { Box } from 'src/components/Box'; +import { TextField } from 'src/components/TextField'; +import { + CustomPopper, + SelectedIcon, +} from 'src/features/Linodes/LinodeSelect/LinodeSelect.styles'; import { useInfiniteNodebalancersQuery } from 'src/queries/nodebalancers'; +import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; -interface Props { +interface NodeBalancerSelectProps { disabled?: boolean; - error?: string; - onChange: (id: number, nodebalancer: NodeBalancer | undefined) => void; + errorText?: string; + helperText?: string; + label?: string; + loading?: boolean; + noMarginTop?: boolean; + noOptionsMessage?: string; + options?: NodeBalancer[]; + optionsFilter?: (nodebalancer: NodeBalancer) => boolean; + placeholder?: string; region?: string; - textFieldProps?: TextFieldProps; - value?: number; + renderOption?: (nodebalancer: NodeBalancer, selected: boolean) => JSX.Element; } -export const NodeBalancerSelect = (props: Props) => { - const { disabled, error, onChange, region, value } = props; +export interface NodeBalancerMultiSelectProps extends NodeBalancerSelectProps { + /* Enable multi-select. */ + multiple: true; + /* Called when the value changes */ + onSelectionChange: (nodebalancers: NodeBalancer[]) => void; + /* An array of `id`s of NodeBalancers that should be selected or a function that should return `true` if the Linode should be selected. */ + value: ((nodebalancer: NodeBalancer) => boolean) | null | number[]; +} + +export interface NodeBalancerSingleSelectProps extends NodeBalancerSelectProps { + /* Enable single-select. */ + multiple?: false; + /* Called when the value changes */ + onSelectionChange: (nodebalancer: NodeBalancer | undefined) => void; + /* The `id` of the selected NodeBalancer or a function that should return `true` if the NodeBalancer should be selected. */ + value: ((nodebalancer: NodeBalancer) => boolean) | null | number; +} + +export const NodeBalancerSelect = ( + props: NodeBalancerMultiSelectProps | NodeBalancerSingleSelectProps +) => { + const { + disabled, + errorText, + helperText, + label, + loading, + multiple, + noMarginTop, + noOptionsMessage, + onSelectionChange, + options, + optionsFilter, + placeholder, + region, + renderOption, + value, + } = props; + const [inputValue, setInputValue] = React.useState(''); const searchFilter = inputValue @@ -29,6 +80,7 @@ export const NodeBalancerSelect = (props: Props) => { const { data, + error, fetchNextPage, hasNextPage, isLoading, @@ -38,51 +90,117 @@ export const NodeBalancerSelect = (props: Props) => { '+order': 'asc', '+order_by': 'label', }); - const nodebalancers = data?.pages.flatMap((page) => page.data); - const options = nodebalancers?.map(({ id, label }) => ({ id, label })); + React.useEffect(() => { + if (hasNextPage && !isLoading) { + fetchNextPage(); + } + }, [hasNextPage, isLoading, fetchNextPage]); + + const unfilteredNodebalancers = data?.pages.flatMap((page) => page.data); - const selectedNodebalancer = - options?.find((option) => option.id === value) ?? null; + const nodebalancers = optionsFilter + ? unfilteredNodebalancers?.filter(optionsFilter) + : unfilteredNodebalancers; + + React.useEffect(() => { + /** We want to clear the input value when the value prop changes to null. + * This is for use cases where a user changes their region and the Linode + * they had selected is no longer available. + */ + if (value === null) { + setInputValue(''); + } + }, [value]); return ( { - const listboxNode = event.currentTarget; - if ( - listboxNode.scrollTop + listboxNode.clientHeight >= - listboxNode.scrollHeight && - hasNextPage - ) { - fetchNextPage(); - } - }, - }} - onChange={(event, value) => - onChange( - value?.id ?? -1, - nodebalancers?.find((n) => n.id === value?.id) + noOptionsText={ + noOptionsMessage ?? ( + {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} ) } - onInputChange={(event, value) => { - setInputValue(value); - }} + onChange={(_, value) => + multiple && Array.isArray(value) + ? onSelectionChange(value as NodeBalancer[]) + : !multiple && + !Array.isArray(value) && + onSelectionChange(value as NodeBalancer) + } renderInput={(params) => ( )} + renderOption={(props, option, { selected }) => { + return ( +
  • + {renderOption ? ( + renderOption(option as NodeBalancer, selected) + ) : ( + <> + + {option.label} + + + + )} +
  • + ); + }} + value={ + typeof value === 'function' + ? multiple && Array.isArray(value) + ? nodebalancers?.filter(value) ?? null + : nodebalancers?.find(value) ?? null + : mapIdsToDevices(value ?? null, nodebalancers) + } + ChipProps={{ deleteIcon: }} + PopperComponent={CustomPopper} + clearOnBlur={false} + disableCloseOnSelect={multiple} + disablePortal={true} disabled={disabled} inputValue={inputValue} - isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} - loading={isLoading} - options={options ?? []} - value={selectedNodebalancer} + // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} + loading={isLoading || loading} + multiple={multiple} + onInputChange={(_, value) => setInputValue(value)} + options={options || (nodebalancers ?? [])} + popupIcon={} /> ); }; + +const getDefaultNoOptionsMessage = ( + error: APIError[] | null, + loading: boolean, + filteredNodeBalancers: NodeBalancer[] | undefined +) => { + if (error) { + return 'An error occured while fetching your NodeBalancers'; + } else if (loading) { + return 'Loading your NodeBalancers...'; + } else if (!filteredNodeBalancers?.length) { + return 'You have no NodeBalancers to choose from'; + } else { + return 'No options'; + } +}; diff --git a/packages/manager/src/utilities/mapIdsToLinodes.test.ts b/packages/manager/src/utilities/mapIdsToDevices.test.ts similarity index 57% rename from packages/manager/src/utilities/mapIdsToLinodes.test.ts rename to packages/manager/src/utilities/mapIdsToDevices.test.ts index 5ba41fa2b42..3167250b42a 100644 --- a/packages/manager/src/utilities/mapIdsToLinodes.test.ts +++ b/packages/manager/src/utilities/mapIdsToDevices.test.ts @@ -1,22 +1,22 @@ import { linodeFactory } from 'src/factories'; -import { mapIdsToLinodes } from './mapIdsToLinodes'; +import { mapIdsToDevices } from './mapIdsToDevices'; describe('mapIdsToLinodes', () => { const linodes = linodeFactory.buildList(5); it('works with a single id', () => { - expect(mapIdsToLinodes(1, linodes)).toBe(linodes[1]); + expect(mapIdsToDevices(1, linodes)).toBe(linodes[1]); }); it('works with a multiple ids', () => { - expect(mapIdsToLinodes([0, 1, 2], linodes)).toEqual([ + expect(mapIdsToDevices([0, 1, 2], linodes)).toEqual([ linodes[0], linodes[1], linodes[2], ]); }); it('omits missing ids', () => { - expect(mapIdsToLinodes(99, linodes)).toBe(null); - expect(mapIdsToLinodes([0, 99, 2], linodes)).toEqual([ + expect(mapIdsToDevices(99, linodes)).toBe(null); + expect(mapIdsToDevices([0, 99, 2], linodes)).toEqual([ linodes[0], linodes[2], ]); diff --git a/packages/manager/src/utilities/mapIdsToDevices.ts b/packages/manager/src/utilities/mapIdsToDevices.ts new file mode 100644 index 00000000000..724e0c032db --- /dev/null +++ b/packages/manager/src/utilities/mapIdsToDevices.ts @@ -0,0 +1,19 @@ +import { Linode, NodeBalancer } from '@linode/api-v4'; + +import { isNotNullOrUndefined } from './nullOrUndefined'; + +type Device = Linode | NodeBalancer; + +export const mapIdsToDevices = ( + ids: null | number | number[], + devices: Device[] = [] +): Device | Device[] | null => { + const deviceMap = new Map(devices.map((device) => [device.id, device])); + if (Array.isArray(ids)) { + return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); + } else if (ids !== null) { + return deviceMap.get(ids) ?? null; + } else { + return null; + } +}; diff --git a/packages/manager/src/utilities/mapIdsToLinodes.ts b/packages/manager/src/utilities/mapIdsToLinodes.ts deleted file mode 100644 index 37bec20d2ff..00000000000 --- a/packages/manager/src/utilities/mapIdsToLinodes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Linode } from '@linode/api-v4'; - -import { isNotNullOrUndefined } from './nullOrUndefined'; - -export const mapIdsToLinodes = ( - ids: null | number | number[], - linodes: Linode[] = [] -): Linode | Linode[] | null => { - const linodeMap = new Map(linodes.map((linode) => [linode.id, linode])); - if (Array.isArray(ids)) { - return ids.map((id) => linodeMap.get(id)).filter(isNotNullOrUndefined); - } else if (ids !== null) { - return linodeMap.get(ids) ?? null; - } else { - return null; - } -}; From 8bf23e6bea66029c4dfa14df08a061161d0fb9e9 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 30 Aug 2023 10:35:01 -0400 Subject: [PATCH 02/29] finished the add nodebalancer drawer --- .../Devices/AddDeviceDrawer.tsx | 58 ++++++++++--------- .../FirewallLanding/CreateFirewallDrawer.tsx | 15 +++-- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index 700ece5621b..ab634010706 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -18,7 +18,7 @@ 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 { READ_ONLY_DEVICES_HIDDEN_MESSAGE } from '../../FirewallLanding/CreateFirewallDrawer'; import { formattedTypes } from './FirewallDeviceLanding'; import type { FirewallDeviceEntityType } from '@linode/api-v4'; @@ -44,15 +44,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 currentNodebalancerIds = - currentDevices - ?.filter((device) => device.entity.type === 'nodebalancer') - .map((device) => device.entity.id) ?? []; + const currentDeviceIds = getEntityIdsByType(type); const { error, @@ -71,21 +71,23 @@ export const AddDeviceDrawer = (props: Props) => { 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 (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 ( @@ -98,7 +100,7 @@ export const AddDeviceDrawer = (props: Props) => { variant="error" > {errorMsg.substring(0, labelIndex)} - {label} + {label} {errorMsg.substring(labelIndex + label.length)} {openTicket ? ( <> @@ -113,16 +115,18 @@ 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 readOnlyNodebalancerIds = isRestrictedUser - ? getEntityIdsByPermission(grants, 'nodebalancer', 'read_only') - : []; + const readOnlyDeviceIds = getReadOnlyEntityIds(type); - const linodeSelectGuidance = - readOnlyLinodeIds.length > 0 ? READ_ONLY_LINODES_HIDDEN_MESSAGE : undefined; + const deviceSelectGuidance = + readOnlyDeviceIds.length > 0 + ? READ_ONLY_DEVICES_HIDDEN_MESSAGE(type) + : undefined; return ( { {type === 'linode' ? ( setSelectedDeviceIds(linodes.map((linode) => linode.id)) } optionsFilter={(linode) => - ![...currentLinodeIds, ...readOnlyLinodeIds].includes(linode.id) + ![...currentDeviceIds, ...readOnlyDeviceIds].includes(linode.id) } disabled={currentDevicesLoading} loading={currentDevicesLoading} @@ -157,7 +161,7 @@ export const AddDeviceDrawer = (props: Props) => { ) : ( setSelectedDeviceIds( @@ -165,7 +169,7 @@ export const AddDeviceDrawer = (props: Props) => { ) } optionsFilter={(nodebalancer) => - ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( + ![...currentDeviceIds, ...readOnlyDeviceIds].includes( nodebalancer.id ) } diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx index de3d365f1ef..57579d79da2 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx @@ -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'; @@ -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; @@ -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. ${ From f4f2512d35acfef28440781b5407c39af802cd1b Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 30 Aug 2023 10:41:31 -0400 Subject: [PATCH 03/29] Added changeset: Created 'Add Nodebalancer' to Firewall drawer --- .../.changeset/pr-9608-upcoming-features-1693406491148.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-9608-upcoming-features-1693406491148.md diff --git a/packages/manager/.changeset/pr-9608-upcoming-features-1693406491148.md b/packages/manager/.changeset/pr-9608-upcoming-features-1693406491148.md new file mode 100644 index 00000000000..b78c2ad200b --- /dev/null +++ b/packages/manager/.changeset/pr-9608-upcoming-features-1693406491148.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Created 'Add Nodebalancer' to Firewall drawer ([#9608](https://github.com/linode/manager/pull/9608)) From c5ad21d95da0dae895934ec1427e545f5f4ca1e8 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 30 Aug 2023 12:42:23 -0400 Subject: [PATCH 04/29] updated drawer and started to add event handlers --- packages/api-v4/src/account/types.ts | 1 + .../notificationsAndEvents/events.spec.ts | 1 + .../manager/src/features/Events/constants.ts | 1 + .../features/Events/eventMessageGenerator.ts | 4 + .../Devices/AddDeviceDrawer.tsx | 17 +- .../Devices/FirewallDeviceLanding.tsx | 8 +- .../NodeBalancers/NodeBalancerSelect.tsx | 154 ++++++++++-------- .../ToastNotifications/ToastNotifications.tsx | 7 + 8 files changed, 107 insertions(+), 86 deletions(-) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index af49c66108e..bc63fd8a944 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -308,6 +308,7 @@ export type EventAction = | 'nodebalancer_create' | 'nodebalancer_update' | 'nodebalancer_delete' + | 'nodebalancer_added_to_firewall' | 'password_reset' | 'profile_update' | 'stackscript_create' diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts index a7590c34f60..565d27cb2c4 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts @@ -62,6 +62,7 @@ const eventActions: RecPartial[] = [ 'nodebalancer_config_create', 'nodebalancer_config_delete', 'nodebalancer_config_update', + 'nodebalancer_added_to_firewall', 'stackscript_create', 'stackscript_delete', 'stackscript_update', diff --git a/packages/manager/src/features/Events/constants.ts b/packages/manager/src/features/Events/constants.ts index 2cca09963ca..10aeb89f572 100644 --- a/packages/manager/src/features/Events/constants.ts +++ b/packages/manager/src/features/Events/constants.ts @@ -70,6 +70,7 @@ export const EVENT_ACTIONS: Event['action'][] = [ 'nodebalancer_create', 'nodebalancer_update', 'nodebalancer_delete', + // 'nodebalancer_added_to_firewall', 'password_reset', 'profile_update', 'stackscript_create', diff --git a/packages/manager/src/features/Events/eventMessageGenerator.ts b/packages/manager/src/features/Events/eventMessageGenerator.ts index 11e46ed7608..4846ddcb676 100644 --- a/packages/manager/src/features/Events/eventMessageGenerator.ts +++ b/packages/manager/src/features/Events/eventMessageGenerator.ts @@ -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.`, diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index ab634010706..02dfa25136c 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -18,7 +18,6 @@ import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; -import { READ_ONLY_DEVICES_HIDDEN_MESSAGE } from '../../FirewallLanding/CreateFirewallDrawer'; import { formattedTypes } from './FirewallDeviceLanding'; import type { FirewallDeviceEntityType } from '@linode/api-v4'; @@ -29,6 +28,9 @@ interface Props { 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, type } = props; @@ -123,11 +125,6 @@ export const AddDeviceDrawer = (props: Props) => { const readOnlyDeviceIds = getReadOnlyEntityIds(type); - const deviceSelectGuidance = - readOnlyDeviceIds.length > 0 - ? READ_ONLY_DEVICES_HIDDEN_MESSAGE(type) - : undefined; - return ( { {errorMessage ? errorNotice(errorMessage) : null} {type === 'linode' ? ( setSelectedDeviceIds(linodes.map((linode) => linode.id)) } @@ -153,6 +147,7 @@ export const AddDeviceDrawer = (props: Props) => { ![...currentDeviceIds, ...readOnlyDeviceIds].includes(linode.id) } disabled={currentDevicesLoading} + helperText={helperText} loading={currentDevicesLoading} multiple noOptionsMessage={`No ${formattedTypes[type]}s available to add`} @@ -160,9 +155,6 @@ export const AddDeviceDrawer = (props: Props) => { /> ) : ( setSelectedDeviceIds( nodebalancers.map((nodebalancer) => nodebalancer.id) @@ -174,6 +166,7 @@ export const AddDeviceDrawer = (props: Props) => { ) } disabled={currentDevicesLoading} + helperText={helperText} loading={currentDevicesLoading} multiple noOptionsMessage={`No ${formattedTypes[type]}s available to add`} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx index c4f46cd36d7..cbee79a1650 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx @@ -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'; @@ -69,11 +70,14 @@ export const FirewallDeviceLanding = React.memo( /> ) : null} - + The following {formattedType}s have been assigned to this Firewall. A {formattedType} can only be assigned to a single - Firewall. + Firewall.{' '} + + Learn about how Firewall rules apply to {formattedType}s. + diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index a06f42ef835..87b07991e23 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -5,6 +5,8 @@ import Autocomplete from '@mui/material/Autocomplete'; import * as React from 'react'; import { Box } from 'src/components/Box'; +import { Link } from 'src/components/Link'; +import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { CustomPopper, @@ -114,78 +116,86 @@ export const NodeBalancerSelect = ( }, [value]); return ( - {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} - ) - } - onChange={(_, value) => - multiple && Array.isArray(value) - ? onSelectionChange(value as NodeBalancer[]) - : !multiple && - !Array.isArray(value) && - onSelectionChange(value as NodeBalancer) - } - renderInput={(params) => ( - - )} - renderOption={(props, option, { selected }) => { - return ( -
  • - {renderOption ? ( - renderOption(option as NodeBalancer, selected) - ) : ( - <> - - {option.label} - - - - )} -
  • - ); - }} - value={ - typeof value === 'function' - ? multiple && Array.isArray(value) - ? nodebalancers?.filter(value) ?? null - : nodebalancers?.find(value) ?? null - : mapIdsToDevices(value ?? null, nodebalancers) - } - ChipProps={{ deleteIcon: }} - PopperComponent={CustomPopper} - clearOnBlur={false} - disableCloseOnSelect={multiple} - disablePortal={true} - disabled={disabled} - inputValue={inputValue} - // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} - loading={isLoading || loading} - multiple={multiple} - onInputChange={(_, value) => setInputValue(value)} - options={options || (nodebalancers ?? [])} - popupIcon={} - /> + <> + + Only the Firewall's inbound rules apply to NodeBalancers. Any existing + outbound rules won't be applied. + {/* add documentation link */} + Learn more. + + {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} + ) + } + onChange={(_, value) => + multiple && Array.isArray(value) + ? onSelectionChange(value as NodeBalancer[]) + : !multiple && + !Array.isArray(value) && + onSelectionChange(value as NodeBalancer) + } + renderInput={(params) => ( + + )} + renderOption={(props, option, { selected }) => { + return ( +
  • + {renderOption ? ( + renderOption(option as NodeBalancer, selected) + ) : ( + <> + + {option.label} + + + + )} +
  • + ); + }} + value={ + typeof value === 'function' + ? multiple && Array.isArray(value) + ? nodebalancers?.filter(value) ?? null + : nodebalancers?.find(value) ?? null + : mapIdsToDevices(value ?? null, nodebalancers) + } + ChipProps={{ deleteIcon: }} + PopperComponent={CustomPopper} + clearOnBlur={false} + disableCloseOnSelect={multiple} + disablePortal={true} + disabled={disabled} + inputValue={inputValue} + // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} + loading={isLoading || loading} + multiple={multiple} + onInputChange={(_, value) => setInputValue(value)} + options={options || (nodebalancers ?? [])} + popupIcon={} + /> + ); }; diff --git a/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx b/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx index 896e9b59f68..a095fe803fa 100644 --- a/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx +++ b/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx @@ -264,6 +264,13 @@ export const ToastNotifications = () => { failureMessage: `Error creating Longview Client ${label}.`, successMessage: `Longview Client ${label} successfully created.`, }); + // case 'nodebalancer_added_to_firewall': + // return toastSuccessAndFailure({ + // enqueueSnackbar, + // eventStatus: event.status, + // failureMessage: `Error adding Nodebalancer ${label}.`, + // successMessage: `Nodebalancer ${label} successfully added.`, + // }); default: // eslint-disable-next-line array-callback-return return; From 23dcc570a33640b3a487e09def517629f81e8d1d Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 30 Aug 2023 14:35:55 -0400 Subject: [PATCH 05/29] added toast notification --- packages/api-v4/src/account/types.ts | 1 - .../notificationsAndEvents/events.spec.ts | 1 - .../manager/src/features/Events/constants.ts | 1 - .../Devices/AddDeviceDrawer.tsx | 22 +++++++++++++++---- .../ToastNotifications/ToastNotifications.tsx | 7 ------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index bc63fd8a944..af49c66108e 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -308,7 +308,6 @@ export type EventAction = | 'nodebalancer_create' | 'nodebalancer_update' | 'nodebalancer_delete' - | 'nodebalancer_added_to_firewall' | 'password_reset' | 'profile_update' | 'stackscript_create' diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts index 565d27cb2c4..a7590c34f60 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts @@ -62,7 +62,6 @@ const eventActions: RecPartial[] = [ 'nodebalancer_config_create', 'nodebalancer_config_delete', 'nodebalancer_config_update', - 'nodebalancer_added_to_firewall', 'stackscript_create', 'stackscript_delete', 'stackscript_update', diff --git a/packages/manager/src/features/Events/constants.ts b/packages/manager/src/features/Events/constants.ts index 10aeb89f572..2cca09963ca 100644 --- a/packages/manager/src/features/Events/constants.ts +++ b/packages/manager/src/features/Events/constants.ts @@ -70,7 +70,6 @@ export const EVENT_ACTIONS: Event['action'][] = [ 'nodebalancer_create', 'nodebalancer_update', 'nodebalancer_delete', - // 'nodebalancer_added_to_firewall', 'password_reset', 'profile_update', 'stackscript_create', diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index 02dfa25136c..f0ef1a95292 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -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'; @@ -34,6 +35,8 @@ const helperText = export const AddDeviceDrawer = (props: Props) => { const { onClose, open, type } = props; + const { enqueueSnackbar } = useSnackbar(); + const { id } = useParams<{ id: string }>(); const { data: grants } = useGrants(); @@ -68,9 +71,17 @@ export const AddDeviceDrawer = (props: Props) => { ); const handleSubmit = async () => { - await Promise.all(selectedDeviceIds.map((id) => addDevice({ id, type }))); - onClose(); - setSelectedDeviceIds([]); + try { + await Promise.all(selectedDeviceIds.map((id) => addDevice({ id, type }))); + enqueueSnackbar('All devices added successfully.', { + variant: 'success', + }); + + onClose(); + setSelectedDeviceIds([]); + } catch (error) { + enqueueSnackbar('Failed to add some devices.', { variant: 'error' }); + } }; const errorMessage = error @@ -127,7 +138,10 @@ export const AddDeviceDrawer = (props: Props) => { return ( { + setSelectedDeviceIds([]); + onClose(); + }} open={open} title={`Add ${formattedTypes[type]} to Firewall: ${firewall?.label}`} > diff --git a/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx b/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx index a095fe803fa..896e9b59f68 100644 --- a/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx +++ b/packages/manager/src/features/ToastNotifications/ToastNotifications.tsx @@ -264,13 +264,6 @@ export const ToastNotifications = () => { failureMessage: `Error creating Longview Client ${label}.`, successMessage: `Longview Client ${label} successfully created.`, }); - // case 'nodebalancer_added_to_firewall': - // return toastSuccessAndFailure({ - // enqueueSnackbar, - // eventStatus: event.status, - // failureMessage: `Error adding Nodebalancer ${label}.`, - // successMessage: `Nodebalancer ${label} successfully added.`, - // }); default: // eslint-disable-next-line array-callback-return return; From 3cd6d7f93fa49267f16e0f5f8a41a576acabf200 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 30 Aug 2023 15:05:18 -0400 Subject: [PATCH 06/29] added multiple toast notifications --- .../Devices/AddDeviceDrawer.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx index f0ef1a95292..744ce337d8c 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx @@ -71,17 +71,24 @@ export const AddDeviceDrawer = (props: Props) => { ); const handleSubmit = async () => { - try { - await Promise.all(selectedDeviceIds.map((id) => addDevice({ id, type }))); - enqueueSnackbar('All devices added successfully.', { - variant: 'success', - }); - - onClose(); - setSelectedDeviceIds([]); - } catch (error) { - enqueueSnackbar('Failed to add some devices.', { variant: 'error' }); - } + 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(); + setSelectedDeviceIds([]); }; const errorMessage = error From 47ae41f557d37f7c30e429ee92b662a03d755276 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Fri, 1 Sep 2023 12:17:06 -0400 Subject: [PATCH 07/29] separated nodebalancer and linode drawer --- .../Devices/AddLinodeDrawer.tsx | 174 ++++++++++++++++++ ...ceDrawer.tsx => AddNodebalancerDrawer.tsx} | 110 ++++------- .../Devices/FirewallDeviceLanding.tsx | 24 ++- .../NodeBalancers/NodeBalancerSelect.tsx | 154 ++++++++-------- 4 files changed, 303 insertions(+), 159 deletions(-) create mode 100644 packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx rename packages/manager/src/features/Firewalls/FirewallDetail/Devices/{AddDeviceDrawer.tsx => AddNodebalancerDrawer.tsx} (60%) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx new file mode 100644 index 00000000000..68ee2ce2973 --- /dev/null +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -0,0 +1,174 @@ +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 { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; +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 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); + + const { data: firewall } = useFirewallQuery(Number(id)); + const { + data: currentDevices, + isLoading: currentDevicesLoading, + } = useAllFirewallDevicesQuery(Number(id)); + + const currentLinodeIds = + currentDevices + ?.filter((device) => device.entity.type === 'linode') + .map((device) => device.entity.id) ?? []; + + const { + error, + isLoading, + mutateAsync: addDevice, + } = useAddFirewallDeviceMutation(Number(id)); + const theme = useTheme(); + + const [selectedLinodeIds, setSelectedLinodeIds] = React.useState( + [] + ); + + const handleSubmit = async () => { + 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; + enqueueSnackbar(`Failed to add ${errorLabel}.`, { variant: 'error' }); + } + }); + + onClose(); + setSelectedLinodeIds([]); + }; + + // @todo title and error messaging will update to "Device" once NodeBalancers are allowed + const errorMessage = error + ? getAPIErrorOrDefault(error, 'Error adding Linode')[0].reason + : undefined; + + // @todo update regex once error messaging updates + const errorNotice = (errorMsg: string) => { + // match something like: Linode (ID ) + const linode = /linode (.+?) \(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; + const labelIndex = errorMsg.indexOf(label); + errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, ''); + return ( + + {errorMsg.substring(0, labelIndex)} + {label} + {errorMsg.substring(labelIndex + label.length)} + {openTicket ? ( + <> + . + + ) : null} + + ); + } else { + return ; + } + }; + + // If a user is restricted, they can not add a read-only Linode to a firewall. + const readOnlyLinodeIds = isRestrictedUser + ? getEntityIdsByPermission(grants, 'linode', 'read_only') + : []; + + return ( + { + setSelectedLinodeIds([]); + onClose(); + }} + open={open} + title={`Add Linode to Firewall: ${firewall?.label}`} + > + ) => { + e.preventDefault(); + handleSubmit(); + }} + > + {errorMessage ? errorNotice(errorMessage) : null} + + 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" + value={selectedLinodeIds} + /> + + + + ); +}; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx similarity index 60% rename from packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx rename to packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 744ce337d8c..552ae6b147d 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddDeviceDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -8,7 +8,6 @@ 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 { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect'; import { useAddFirewallDeviceMutation, @@ -19,21 +18,14 @@ import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; -import { formattedTypes } from './FirewallDeviceLanding'; - -import type { FirewallDeviceEntityType } from '@linode/api-v4'; - interface Props { + helperText: string; 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, type } = props; +export const AddNodebalancerDrawer = (props: Props) => { + const { helperText, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); @@ -49,15 +41,10 @@ export const AddDeviceDrawer = (props: Props) => { isLoading: currentDevicesLoading, } = useAllFirewallDevicesQuery(Number(id)); - const getEntityIdsByType = (entityType: FirewallDeviceEntityType) => { - return ( - currentDevices - ?.filter((device) => device.entity.type === entityType) - .map((device) => device.entity.id) ?? [] - ); - }; - - const currentDeviceIds = getEntityIdsByType(type); + const currentNodebalancerIds = + currentDevices + ?.filter((device) => device.entity.type === 'nodebalancer') + .map((device) => device.entity.id) ?? []; const { error, @@ -72,7 +59,7 @@ export const AddDeviceDrawer = (props: Props) => { const handleSubmit = async () => { const results = await Promise.allSettled( - selectedDeviceIds.map((id) => addDevice({ id, type })) + selectedDeviceIds.map((id) => addDevice({ id, type: 'nodebalancer' })) ); results.forEach((result, _) => { @@ -92,16 +79,13 @@ export const AddDeviceDrawer = (props: Props) => { }; const errorMessage = error - ? getAPIErrorOrDefault(error, `Error adding ${formattedTypes[type]}`)[0] - .reason + ? getAPIErrorOrDefault(error, `Error adding Nodebalancer`)[0].reason : undefined; // @todo update regex once error messaging updates const errorNotice = (errorMsg: string) => { // match something like: Linode (ID ) - const device = /(linode|nodebalancer) (.+?) \(id ([^()]*)\)/i.exec( - errorMsg - ); + 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, ''); @@ -120,7 +104,7 @@ export const AddDeviceDrawer = (props: Props) => { variant="error" > {errorMsg.substring(0, labelIndex)} - {label} + {label} {errorMsg.substring(labelIndex + label.length)} {openTicket ? ( <> @@ -134,14 +118,10 @@ export const AddDeviceDrawer = (props: Props) => { } }; - // If a user is restricted, they can not add a read-only Linode to a firewall. - const getReadOnlyEntityIds = (entityType: FirewallDeviceEntityType) => { - return isRestrictedUser - ? getEntityIdsByPermission(grants, entityType, 'read_only') - : []; - }; - - const readOnlyDeviceIds = getReadOnlyEntityIds(type); + // 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 ( { onClose(); }} open={open} - title={`Add ${formattedTypes[type]} to Firewall: ${firewall?.label}`} + title={`Add Nodebalancer to Firewall: ${firewall?.label}`} > + + Only the Firewall's inbound rules apply to NodeBalancers. Any existing + outbound rules won't be applied. + {/* add documentation link */} + Learn more. +
    ) => { e.preventDefault(); @@ -159,41 +145,23 @@ export const AddDeviceDrawer = (props: Props) => { }} > {errorMessage ? errorNotice(errorMessage) : null} - {type === 'linode' ? ( - - 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} - /> - ) : ( - - 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} - /> - )} + + setSelectedDeviceIds( + nodebalancers.map((nodebalancer) => nodebalancer.id) + ) + } + optionsFilter={(nodebalancer) => + ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( + nodebalancer.id + ) + } + disabled={currentDevicesLoading} + helperText={helperText} + loading={currentDevicesLoading} + multiple + value={selectedDeviceIds} + /> { const { disabled, firewallID, firewallLabel, type } = props; @@ -101,11 +105,19 @@ export const FirewallDeviceLanding = React.memo( error={error ?? undefined} loading={isLoading} /> - + {type === 'linode' ? ( + + ) : ( + + )} - - Only the Firewall's inbound rules apply to NodeBalancers. Any existing - outbound rules won't be applied. - {/* add documentation link */} - Learn more. - - {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} - ) - } - onChange={(_, value) => - multiple && Array.isArray(value) - ? onSelectionChange(value as NodeBalancer[]) - : !multiple && - !Array.isArray(value) && - onSelectionChange(value as NodeBalancer) - } - renderInput={(params) => ( - - )} - renderOption={(props, option, { selected }) => { - return ( -
  • - {renderOption ? ( - renderOption(option as NodeBalancer, selected) - ) : ( - <> - - {option.label} - - - - )} -
  • - ); - }} - value={ - typeof value === 'function' - ? multiple && Array.isArray(value) - ? nodebalancers?.filter(value) ?? null - : nodebalancers?.find(value) ?? null - : mapIdsToDevices(value ?? null, nodebalancers) - } - ChipProps={{ deleteIcon: }} - PopperComponent={CustomPopper} - clearOnBlur={false} - disableCloseOnSelect={multiple} - disablePortal={true} - disabled={disabled} - inputValue={inputValue} - // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} - loading={isLoading || loading} - multiple={multiple} - onInputChange={(_, value) => setInputValue(value)} - options={options || (nodebalancers ?? [])} - popupIcon={} - /> - + {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} + ) + } + onChange={(_, value) => + multiple && Array.isArray(value) + ? onSelectionChange(value as NodeBalancer[]) + : !multiple && + !Array.isArray(value) && + onSelectionChange(value as NodeBalancer) + } + renderInput={(params) => ( + + )} + renderOption={(props, option, { selected }) => { + return ( +
  • + {renderOption ? ( + renderOption(option as NodeBalancer, selected) + ) : ( + <> + + {option.label} + + + + )} +
  • + ); + }} + value={ + typeof value === 'function' + ? multiple && Array.isArray(value) + ? nodebalancers?.filter(value) ?? null + : nodebalancers?.find(value) ?? null + : mapIdsToDevices(value ?? null, nodebalancers) + } + ChipProps={{ deleteIcon: }} + PopperComponent={CustomPopper} + clearOnBlur={false} + disableCloseOnSelect={multiple} + disablePortal={true} + disabled={disabled} + inputValue={inputValue} + // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} + loading={isLoading || loading} + multiple={multiple} + onInputChange={(_, value) => setInputValue(value)} + options={options || (nodebalancers ?? [])} + popupIcon={} + /> ); }; From 7e7c861c12b4b71fa7f43dfef234e4acd9229b91 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Fri, 1 Sep 2023 14:43:32 -0400 Subject: [PATCH 08/29] added infinite scrolling --- .../Devices/AddNodebalancerDrawer.tsx | 2 +- .../features/NodeBalancers/NodeBalancerSelect.tsx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 552ae6b147d..7654e0d2780 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -84,7 +84,7 @@ export const AddNodebalancerDrawer = (props: Props) => { // @todo update regex once error messaging updates const errorNotice = (errorMsg: string) => { - // match something like: Linode (ID ) + // match something like: Nodebalancer (ID ) const device = /(nodebalancer) (.+?) \(id ([^()]*)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); if (openTicket) { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index a06f42ef835..d2fcd496b08 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -91,12 +91,6 @@ export const NodeBalancerSelect = ( '+order_by': 'label', }); - React.useEffect(() => { - if (hasNextPage && !isLoading) { - fetchNextPage(); - } - }, [hasNextPage, isLoading, fetchNextPage]); - const unfilteredNodebalancers = data?.pages.flatMap((page) => page.data); const nodebalancers = optionsFilter @@ -113,6 +107,12 @@ export const NodeBalancerSelect = ( } }, [value]); + const onScroll = () => { + if (hasNextPage && !isLoading) { + fetchNextPage(); + } + }; + return ( setInputValue(value)} + onScroll={onScroll} options={options || (nodebalancers ?? [])} popupIcon={} /> From 6822f9905965d4ef7f8c64e197b9fb7eeab0b299 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 6 Sep 2023 12:15:49 -0400 Subject: [PATCH 09/29] fixed pr suggestions --- .../Devices/AddNodebalancerDrawer.tsx | 1 - .../NodeBalancers/NodeBalancerSelect.tsx | 19 ++++++++++++------- .../manager/src/utilities/mapIdsToDevices.ts | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 7654e0d2780..8652a7615dc 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -164,7 +164,6 @@ export const AddNodebalancerDrawer = (props: Props) => { /> { - if (hasNextPage && !isLoading) { - fetchNextPage(); - } - }; - return ( { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight >= + listboxNode.scrollHeight && + hasNextPage + ) { + fetchNextPage(); + } + }, + }} noOptionsText={ noOptionsMessage ?? ( {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} @@ -183,7 +189,6 @@ export const NodeBalancerSelect = ( loading={isLoading || loading} multiple={multiple} onInputChange={(_, value) => setInputValue(value)} - onScroll={onScroll} options={options || (nodebalancers ?? [])} popupIcon={} /> diff --git a/packages/manager/src/utilities/mapIdsToDevices.ts b/packages/manager/src/utilities/mapIdsToDevices.ts index 724e0c032db..324ab47739a 100644 --- a/packages/manager/src/utilities/mapIdsToDevices.ts +++ b/packages/manager/src/utilities/mapIdsToDevices.ts @@ -17,3 +17,22 @@ export const mapIdsToDevices = ( return null; } }; + +// type Device = Linode | NodeBalancer; + +// export const mapIdsToDevices = ( +// ids: null | number | number[], +// devices: T[] = [] +// ): T | T[] | null => { +// const deviceMap = new Map( +// // Even though the types extend Device. type insertion is still required here +// devices.map((device) => [(device as Device).id, device]) +// ); +// if (Array.isArray(ids)) { +// return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); +// } else if (ids !== null) { +// return deviceMap.get(ids) ?? null; +// } else { +// return null; +// } +// }; From 6d59e9eda70a3517c6588638cfec2b4f7ecd9797 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 6 Sep 2023 12:24:20 -0400 Subject: [PATCH 10/29] partially eliminated type definitions in LinodeSelect --- .../Linodes/LinodeSelect/LinodeSelect.tsx | 8 ++-- .../NodeBalancers/NodeBalancerSelect.tsx | 2 +- .../manager/src/utilities/mapIdsToDevices.ts | 44 +++++++++---------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx index 981854b3e9a..4ca12cb2e73 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx @@ -126,10 +126,8 @@ export const LinodeSelect = ( } onChange={(_, value) => multiple && Array.isArray(value) - ? onSelectionChange(value as Linode[]) - : !multiple && - !Array.isArray(value) && - onSelectionChange(value as Linode) + ? onSelectionChange(value) + : !multiple && !Array.isArray(value) && onSelectionChange(value) } renderInput={(params) => ( (value, linodes) } ChipProps={{ deleteIcon: }} PopperComponent={CustomPopper} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index 82def68c778..0f4b32ba8f4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -176,7 +176,7 @@ export const NodeBalancerSelect = ( ? multiple && Array.isArray(value) ? nodebalancers?.filter(value) ?? null : nodebalancers?.find(value) ?? null - : mapIdsToDevices(value ?? null, nodebalancers) + : mapIdsToDevices(value ?? null, nodebalancers) } ChipProps={{ deleteIcon: }} PopperComponent={CustomPopper} diff --git a/packages/manager/src/utilities/mapIdsToDevices.ts b/packages/manager/src/utilities/mapIdsToDevices.ts index 324ab47739a..f9a05f2fd6f 100644 --- a/packages/manager/src/utilities/mapIdsToDevices.ts +++ b/packages/manager/src/utilities/mapIdsToDevices.ts @@ -4,30 +4,11 @@ import { isNotNullOrUndefined } from './nullOrUndefined'; type Device = Linode | NodeBalancer; -export const mapIdsToDevices = ( - ids: null | number | number[], - devices: Device[] = [] -): Device | Device[] | null => { - const deviceMap = new Map(devices.map((device) => [device.id, device])); - if (Array.isArray(ids)) { - return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); - } else if (ids !== null) { - return deviceMap.get(ids) ?? null; - } else { - return null; - } -}; - -// type Device = Linode | NodeBalancer; - -// export const mapIdsToDevices = ( +// export const mapIdsToDevices = ( // ids: null | number | number[], -// devices: T[] = [] -// ): T | T[] | null => { -// const deviceMap = new Map( -// // Even though the types extend Device. type insertion is still required here -// devices.map((device) => [(device as Device).id, device]) -// ); +// devices: Device[] = [] +// ): Device | Device[] | null => { +// const deviceMap = new Map(devices.map((device) => [device.id, device])); // if (Array.isArray(ids)) { // return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); // } else if (ids !== null) { @@ -36,3 +17,20 @@ export const mapIdsToDevices = ( // return null; // } // }; + +export const mapIdsToDevices = ( + ids: null | number | number[], + devices: T[] = [] +): T | T[] | null => { + const deviceMap = new Map( + // Even though the types extend Device. type insertion is still required here + devices.map((device) => [(device as Device).id, device]) + ); + if (Array.isArray(ids)) { + return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); + } else if (ids !== null) { + return deviceMap.get(ids) ?? null; + } else { + return null; + } +}; From e01c79efb11a52d7cfcc011222a404b96627cf55 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 6 Sep 2023 12:25:08 -0400 Subject: [PATCH 11/29] eliminated type definitions in LinodeSelect --- .../manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx index 4ca12cb2e73..b9d9e38c29d 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx @@ -151,7 +151,7 @@ export const LinodeSelect = ( return (
  • {renderOption ? ( - renderOption(option as Linode, selected) + renderOption(option, selected) ) : ( <> Date: Wed, 6 Sep 2023 12:29:02 -0400 Subject: [PATCH 12/29] changed type definition of onSelectionChange in NodeBalancerSelect --- .../manager/src/features/Domains/CreateDomain/CreateDomain.tsx | 2 +- .../manager/src/features/NodeBalancers/NodeBalancerSelect.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index ae4247ea6d9..010dba740a3 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -414,7 +414,7 @@ export const CreateDomain = () => { - setSelectedDefaultNodeBalancer(nodebalancer) + setSelectedDefaultNodeBalancer(nodebalancer ?? undefined) } disabled={disabled} errorText={errorMap.defaultNodeBalancer} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index 0f4b32ba8f4..a29228ce6f5 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -41,7 +41,7 @@ export interface NodeBalancerSingleSelectProps extends NodeBalancerSelectProps { /* Enable single-select. */ multiple?: false; /* Called when the value changes */ - onSelectionChange: (nodebalancer: NodeBalancer | undefined) => void; + onSelectionChange: (nodebalancer: NodeBalancer | null) => void; /* The `id` of the selected NodeBalancer or a function that should return `true` if the NodeBalancer should be selected. */ value: ((nodebalancer: NodeBalancer) => boolean) | null | number; } From 6451bdd064a2e6a66121bd108e26ed6a49433609 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 6 Sep 2023 12:30:32 -0400 Subject: [PATCH 13/29] eliminated type declarations in NodeBalancerSelect --- .../src/features/NodeBalancers/NodeBalancerSelect.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index a29228ce6f5..bed053e17f9 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -128,10 +128,8 @@ export const NodeBalancerSelect = ( } onChange={(_, value) => multiple && Array.isArray(value) - ? onSelectionChange(value as NodeBalancer[]) - : !multiple && - !Array.isArray(value) && - onSelectionChange(value as NodeBalancer) + ? onSelectionChange(value) + : !multiple && !Array.isArray(value) && onSelectionChange(value) } renderInput={(params) => ( {renderOption ? ( - renderOption(option as NodeBalancer, selected) + renderOption(option, selected) ) : ( <> Date: Wed, 6 Sep 2023 12:33:33 -0400 Subject: [PATCH 14/29] erased commented out code --- packages/manager/src/utilities/mapIdsToDevices.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/manager/src/utilities/mapIdsToDevices.ts b/packages/manager/src/utilities/mapIdsToDevices.ts index f9a05f2fd6f..953b57fa630 100644 --- a/packages/manager/src/utilities/mapIdsToDevices.ts +++ b/packages/manager/src/utilities/mapIdsToDevices.ts @@ -4,20 +4,6 @@ import { isNotNullOrUndefined } from './nullOrUndefined'; type Device = Linode | NodeBalancer; -// export const mapIdsToDevices = ( -// ids: null | number | number[], -// devices: Device[] = [] -// ): Device | Device[] | null => { -// const deviceMap = new Map(devices.map((device) => [device.id, device])); -// if (Array.isArray(ids)) { -// return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); -// } else if (ids !== null) { -// return deviceMap.get(ids) ?? null; -// } else { -// return null; -// } -// }; - export const mapIdsToDevices = ( ids: null | number | number[], devices: T[] = [] From f59fa25c5d87f7baac92c542349b2bb662fa9f38 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Wed, 6 Sep 2023 15:12:39 -0400 Subject: [PATCH 15/29] initial round of fixes --- .../Devices/AddLinodeDrawer.tsx | 11 ++++------- .../Devices/AddNodebalancerDrawer.tsx | 19 +++++++++++-------- .../Devices/FirewallDeviceRow.tsx | 7 ++----- .../Devices/RemoveDeviceDialog.tsx | 2 -- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index 68ee2ce2973..698d75b8cbb 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -1,4 +1,3 @@ -import { useTheme } from '@mui/material/styles'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useParams } from 'react-router-dom'; @@ -51,7 +50,6 @@ export const AddLinodeDrawer = (props: Props) => { isLoading, mutateAsync: addDevice, } = useAddFirewallDeviceMutation(Number(id)); - const theme = useTheme(); const [selectedLinodeIds, setSelectedLinodeIds] = React.useState( [] @@ -84,7 +82,8 @@ export const AddLinodeDrawer = (props: Props) => { : undefined; // @todo update regex once error messaging updates - const errorNotice = (errorMsg: string) => { + const errorNotice = () => { + let errorMsg = errorMessage || ''; // match something like: Linode (ID ) const linode = /linode (.+?) \(id ([^()]*)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); @@ -98,8 +97,8 @@ export const AddLinodeDrawer = (props: Props) => { return ( { handleSubmit(); }} > - {errorMessage ? errorNotice(errorMessage) : null} + {errorMessage ? errorNotice() : null} setSelectedLinodeIds(linodes.map((linode) => linode.id)) @@ -156,14 +155,12 @@ export const AddLinodeDrawer = (props: Props) => { /> { isLoading, mutateAsync: addDevice, } = useAddFirewallDeviceMutation(Number(id)); - const theme = useTheme(); const [selectedDeviceIds, setSelectedDeviceIds] = React.useState( [] @@ -62,20 +60,25 @@ export const AddNodebalancerDrawer = (props: Props) => { selectedDeviceIds.map((id) => addDevice({ id, type: 'nodebalancer' })) ); + let hasError = false; + 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 { + hasError = true; // 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([]); + if (!hasError) { + onClose(); + setSelectedDeviceIds([]); + } }; const errorMessage = error @@ -83,7 +86,8 @@ export const AddNodebalancerDrawer = (props: Props) => { : undefined; // @todo update regex once error messaging updates - const errorNotice = (errorMsg: string) => { + const errorNotice = () => { + let errorMsg = errorMessage || ''; // match something like: Nodebalancer (ID ) const device = /(nodebalancer) (.+?) \(id ([^()]*)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); @@ -97,8 +101,8 @@ export const AddNodebalancerDrawer = (props: Props) => { return ( { handleSubmit(); }} > - {errorMessage ? errorNotice(errorMessage) : null} + {errorMessage ? errorNotice() : null} setSelectedDeviceIds( @@ -170,7 +174,6 @@ export const AddNodebalancerDrawer = (props: Props) => { onClick: handleSubmit, }} secondaryButtonProps={{ - 'data-testid': 'cancel', label: 'Cancel', onClick: onClose, }} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx index 0c8384e3e94..0407ad6df96 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx @@ -11,13 +11,10 @@ import { export const FirewallDeviceRow = React.memo( (props: FirewallDeviceActionMenuProps) => { - const { deviceEntityID, deviceID, deviceLabel } = props; + const { deviceEntityID, deviceLabel } = props; return ( - + {deviceLabel} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx index 78db05a90a8..2e83dc1ba81 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx @@ -70,13 +70,11 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { actions={ Date: Wed, 6 Sep 2023 15:18:59 -0400 Subject: [PATCH 16/29] fixed linode error drawer not closing --- .../Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index 698d75b8cbb..9416b0b6721 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -60,20 +60,25 @@ export const AddLinodeDrawer = (props: Props) => { selectedLinodeIds.map((id) => addDevice({ id, type: 'linode' })) ); + let hasError = false; + 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 { + hasError = true; // 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([]); + if (!hasError) { + onClose(); + setSelectedLinodeIds([]); + } }; // @todo title and error messaging will update to "Device" once NodeBalancers are allowed From a6bfbfe0fd0878bffa9f2e96ec3bf57ac71752f1 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Thu, 7 Sep 2023 08:54:00 -0400 Subject: [PATCH 17/29] eliminated event message --- packages/manager/src/features/Events/eventMessageGenerator.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/manager/src/features/Events/eventMessageGenerator.ts b/packages/manager/src/features/Events/eventMessageGenerator.ts index c2bf6586e89..0af31239c62 100644 --- a/packages/manager/src/features/Events/eventMessageGenerator.ts +++ b/packages/manager/src/features/Events/eventMessageGenerator.ts @@ -584,10 +584,6 @@ 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.`, From 33bba734bd9454f2664167310ccacc84ea554d62 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Thu, 7 Sep 2023 08:58:36 -0400 Subject: [PATCH 18/29] added todo comment --- .../Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx index 83f75d4d706..0de3e087c9f 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceLanding.tsx @@ -78,10 +78,11 @@ export const FirewallDeviceLanding = React.memo( The following {formattedType}s have been assigned to this Firewall. A {formattedType} can only be assigned to a single - Firewall.{' '} + Firewall. Learn about how Firewall rules apply to {formattedType}s. + {/* @todo add documentation link */} From 697d5fd6b9068f415043811d858c0be8e8e23b7d Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Thu, 7 Sep 2023 09:00:02 -0400 Subject: [PATCH 19/29] added todo comment --- .../Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index e9cbc69305c..9adf82d0719 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -139,7 +139,7 @@ export const AddNodebalancerDrawer = (props: Props) => { Only the Firewall's inbound rules apply to NodeBalancers. Any existing outbound rules won't be applied. - {/* add documentation link */} + {/* @todo add documentation link */} Learn more. Date: Thu, 7 Sep 2023 17:58:30 -0400 Subject: [PATCH 20/29] fixed toast notification, error reset, and error text --- .../Devices/AddLinodeDrawer.tsx | 30 +++++----- .../Devices/AddNodebalancerDrawer.tsx | 58 +++++++++++-------- .../manager/src/utilities/mapIdsToDevices.ts | 2 +- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index 9416b0b6721..ffceeb5d8a3 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -1,3 +1,4 @@ +import { Linode } from '@linode/api-v4'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useParams } from 'react-router-dom'; @@ -51,33 +52,32 @@ export const AddLinodeDrawer = (props: Props) => { mutateAsync: addDevice, } = useAddFirewallDeviceMutation(Number(id)); - const [selectedLinodeIds, setSelectedLinodeIds] = React.useState( - [] - ); + const [selectedLinodes, setSelectedLinodes] = React.useState([]); const handleSubmit = async () => { const results = await Promise.allSettled( - selectedLinodeIds.map((id) => addDevice({ id, type: 'linode' })) + selectedLinodes.map((linode) => + addDevice({ id: linode.id, type: 'linode' }) + ) ); let hasError = false; - results.forEach((result, _) => { + results.forEach((result, index) => { + const label = selectedLinodes[index].label; 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 { hasError = true; // 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' }); + enqueueSnackbar(`Failed to add ${label}.`, { variant: 'error' }); } }); if (!hasError) { onClose(); - setSelectedLinodeIds([]); + setSelectedLinodes([]); } }; @@ -90,7 +90,7 @@ export const AddLinodeDrawer = (props: Props) => { const errorNotice = () => { let errorMsg = errorMessage || ''; // match something like: Linode (ID ) - const linode = /linode (.+?) \(id ([^()]*)\)/i.exec(errorMsg); + const linode = /Linode (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); if (openTicket) { errorMsg = errorMsg.replace(/open a support ticket\./i, ''); @@ -131,7 +131,7 @@ export const AddLinodeDrawer = (props: Props) => { return ( { - setSelectedLinodeIds([]); + setSelectedLinodes([]); onClose(); }} open={open} @@ -145,9 +145,6 @@ export const AddLinodeDrawer = (props: Props) => { > {errorMessage ? errorNotice() : null} - setSelectedLinodeIds(linodes.map((linode) => linode.id)) - } optionsFilter={(linode) => ![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id) } @@ -156,11 +153,12 @@ export const AddLinodeDrawer = (props: Props) => { loading={currentDevicesLoading} multiple noOptionsMessage="No Linodes available to add" - value={selectedLinodeIds} + onSelectionChange={(linodes) => setSelectedLinodes(linodes)} + value={selectedLinodes.map((linode) => linode.id)} /> { mutateAsync: addDevice, } = useAddFirewallDeviceMutation(Number(id)); - const [selectedDeviceIds, setSelectedDeviceIds] = React.useState( - [] + const [selectedNodebalancers, setSelectedNodebalancers] = React.useState< + NodeBalancer[] + >([]); + + const [localError, setLocalError] = React.useState( + undefined ); const handleSubmit = async () => { const results = await Promise.allSettled( - selectedDeviceIds.map((id) => addDevice({ id, type: 'nodebalancer' })) + selectedNodebalancers.map((nodebalancer) => + addDevice({ id: nodebalancer.id, type: 'nodebalancer' }) + ) ); let hasError = false; - results.forEach((result, _) => { + results.forEach((result, index) => { + const label = selectedNodebalancers[index].label; 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 { hasError = true; // 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' }); + // const errorLabel = result.reason.label; + enqueueSnackbar(`Failed to add ${label}.`, { + variant: 'error', + }); } }); if (!hasError) { onClose(); - setSelectedDeviceIds([]); + setSelectedNodebalancers([]); } }; - const errorMessage = error - ? getAPIErrorOrDefault(error, `Error adding Nodebalancer`)[0].reason - : undefined; + React.useEffect(() => { + setLocalError( + error + ? getAPIErrorOrDefault(error, `Error adding Nodebalancer`)[0].reason + : undefined + ); + }, [error]); // @todo update regex once error messaging updates const errorNotice = () => { - let errorMsg = errorMessage || ''; + let errorMsg = localError || ''; // match something like: Nodebalancer (ID ) - const device = /(nodebalancer) (.+?) \(id ([^()]*)\)/i.exec(errorMsg); + const nodebalancer = /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; + if (nodebalancer) { + const [, label, id] = nodebalancer; const labelIndex = errorMsg.indexOf(label); errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, ''); return ( @@ -118,7 +131,7 @@ export const AddNodebalancerDrawer = (props: Props) => { ); } else { - return ; + return ; } }; @@ -130,7 +143,8 @@ export const AddNodebalancerDrawer = (props: Props) => { return ( { - setSelectedDeviceIds([]); + setSelectedNodebalancers([]); + setLocalError(undefined); onClose(); }} open={open} @@ -148,12 +162,10 @@ export const AddNodebalancerDrawer = (props: Props) => { handleSubmit(); }} > - {errorMessage ? errorNotice() : null} + {localError ? errorNotice() : null} - setSelectedDeviceIds( - nodebalancers.map((nodebalancer) => nodebalancer.id) - ) + setSelectedNodebalancers(nodebalancers) } optionsFilter={(nodebalancer) => ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( @@ -164,11 +176,11 @@ export const AddNodebalancerDrawer = (props: Props) => { helperText={helperText} loading={currentDevicesLoading} multiple - value={selectedDeviceIds} + value={selectedNodebalancers.map((nodebalancer) => nodebalancer.id)} /> ( ): T | T[] | null => { const deviceMap = new Map( // Even though the types extend Device. type insertion is still required here - devices.map((device) => [(device as Device).id, device]) + devices.map((device) => [device.id, device]) ); if (Array.isArray(ids)) { return ids.map((id) => deviceMap.get(id)).filter(isNotNullOrUndefined); From 5c5baf89ab61c7165d986c643a65a52d5fec53e1 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Fri, 8 Sep 2023 10:24:34 -0400 Subject: [PATCH 21/29] fixed toast notification, error reset, and error text for Linode Drawer --- .../Devices/AddLinodeDrawer.tsx | 42 +++++++++++++------ .../Devices/AddNodebalancerDrawer.tsx | 30 ++++++++----- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index ffceeb5d8a3..96ef3e0e8bb 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -54,6 +54,10 @@ export const AddLinodeDrawer = (props: Props) => { const [selectedLinodes, setSelectedLinodes] = React.useState([]); + const [localError, setLocalError] = React.useState( + undefined + ); + const handleSubmit = async () => { const results = await Promise.allSettled( selectedLinodes.map((linode) => @@ -81,24 +85,37 @@ export const AddLinodeDrawer = (props: Props) => { } }; - // @todo title and error messaging will update to "Device" once NodeBalancers are allowed - const errorMessage = error - ? getAPIErrorOrDefault(error, 'Error adding Linode')[0].reason - : undefined; + React.useEffect(() => { + setLocalError( + error + ? getAPIErrorOrDefault(error, `Error adding Linode`)[0].reason + : undefined + ); + }, [error]); - // @todo update regex once error messaging updates const errorNotice = () => { - let errorMsg = errorMessage || ''; + let errorMsg = localError || ''; // match something like: Linode (ID ) + const linode = /Linode (.+?) \(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; - const labelIndex = errorMsg.indexOf(label); - errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, ''); + + // Break the errorMsg into two parts: before and after the linode pattern + const startMsg = errorMsg.substring( + 0, + errorMsg.indexOf(`Linode ${label}`) + ); + const endMsg = errorMsg.substring( + errorMsg.indexOf(`(ID ${id})`) + `(ID ${id})`.length + ); + return ( { }} variant="error" > - {errorMsg.substring(0, labelIndex)} + {startMsg} {label} - {errorMsg.substring(labelIndex + label.length)} + {endMsg} {openTicket ? ( <> . @@ -119,7 +136,7 @@ export const AddLinodeDrawer = (props: Props) => { ); } else { - return ; + return ; } }; @@ -132,6 +149,7 @@ export const AddLinodeDrawer = (props: Props) => { { setSelectedLinodes([]); + setLocalError(undefined); onClose(); }} open={open} @@ -143,7 +161,7 @@ export const AddLinodeDrawer = (props: Props) => { handleSubmit(); }} > - {errorMessage ? errorNotice() : null} + {localError ? errorNotice() : null} ![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 25b40b4451b..bb02740b0e3 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -98,19 +98,29 @@ export const AddNodebalancerDrawer = (props: Props) => { ); }, [error]); - // @todo update regex once error messaging updates const errorNotice = () => { let errorMsg = localError || ''; - // match something like: Nodebalancer (ID ) - const nodebalancer = /NodeBalancer (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); + // match something like: Linode (ID ) + + const linode = /Linode (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); + if (openTicket) { errorMsg = errorMsg.replace(/open a support ticket\./i, ''); } - if (nodebalancer) { - const [, label, id] = nodebalancer; - const labelIndex = errorMsg.indexOf(label); - errorMsg = errorMsg.replace(/\(id ([^()]*)\)/i, ''); + + if (linode) { + const [, label, id] = linode; + + // Break the errorMsg into two parts: before and after the linode pattern + const startMsg = errorMsg.substring( + 0, + errorMsg.indexOf(`NodeBalancer ${label}`) + ); + const endMsg = errorMsg.substring( + errorMsg.indexOf(`(ID ${id})`) + `(ID ${id})`.length + ); + return ( { }} variant="error" > - {errorMsg.substring(0, labelIndex)} - {label} - {errorMsg.substring(labelIndex + label.length)} + {startMsg} + {label} + {endMsg} {openTicket ? ( <> . From c9533de6fea3d453ae36aeee5afed54af3969680 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Sun, 10 Sep 2023 13:41:12 -0400 Subject: [PATCH 22/29] fixed nodebalancer drawer error notices --- .../FirewallDetail/Devices/AddNodebalancerDrawer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index bb02740b0e3..613a26aa8e1 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -100,9 +100,9 @@ export const AddNodebalancerDrawer = (props: Props) => { const errorNotice = () => { let errorMsg = localError || ''; - // match something like: Linode (ID ) + // match something like: NodeBalancer (ID ) - const linode = /Linode (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); + const linode = /NodeBalancer (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); const openTicket = errorMsg.match(/open a support ticket\./i); if (openTicket) { @@ -112,7 +112,7 @@ export const AddNodebalancerDrawer = (props: Props) => { if (linode) { const [, label, id] = linode; - // Break the errorMsg into two parts: before and after the linode pattern + // Break the errorMsg into two parts: before and after the nodebalancer pattern const startMsg = errorMsg.substring( 0, errorMsg.indexOf(`NodeBalancer ${label}`) @@ -131,7 +131,7 @@ export const AddNodebalancerDrawer = (props: Props) => { variant="error" > {startMsg} - {label} + {label} {endMsg} {openTicket ? ( <> From c70fa4de48baf84242d991d3f73e12ea7ec06cbb Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Sun, 10 Sep 2023 13:50:39 -0400 Subject: [PATCH 23/29] merged with develop --- .../src/features/NodeBalancers/NodeBalancerSelect.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index bed053e17f9..1392e7306ff 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -4,12 +4,12 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import Autocomplete from '@mui/material/Autocomplete'; import * as React from 'react'; -import { Box } from 'src/components/Box'; -import { TextField } from 'src/components/TextField'; import { CustomPopper, SelectedIcon, -} from 'src/features/Linodes/LinodeSelect/LinodeSelect.styles'; +} from 'src/components/Autocomplete/Autocomplete.styles'; +import { Box } from 'src/components/Box'; +import { TextField } from 'src/components/TextField'; import { useInfiniteNodebalancersQuery } from 'src/queries/nodebalancers'; import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; From b91edfb8f42b45d5632dcd8b2c2cdcdcb9152579 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Sun, 10 Sep 2023 15:24:31 -0400 Subject: [PATCH 24/29] initial migration to new autocomplete component, still some errors --- .../Devices/AddLinodeDrawer.tsx | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index 96ef3e0e8bb..d4fbb3cb997 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -4,19 +4,21 @@ import * as React from 'react'; import { useParams } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; 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 { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useAddFirewallDeviceMutation, useAllFirewallDevicesQuery, useFirewallQuery, } from 'src/queries/firewalls'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; +import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; interface Props { helperText: string; @@ -140,11 +142,45 @@ export const AddLinodeDrawer = (props: Props) => { } }; + type OptionType = { id: number; label: string }; + // 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 optionsFilter = (linode: Linode) => { + ![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id); + }; + + const { + data, + // error: linodesError, + isLoading: linodesIsLoading, + } = useAllLinodesQuery(); + + const linodes = data?.filter(optionsFilter); + + const options = + linodes?.map((linode) => ({ id: linode.id, label: linode.label })) || []; + + const onChange = (linodes: OptionType[]) => { + let mappedLinodes: Linode[] = []; + + if (Array.isArray(linodes)) { + const result = mapIdsToDevices( + linodes.map((linode) => linode.id) + ); + if (Array.isArray(result)) { + mappedLinodes = result; + } else if (result) { + mappedLinodes = [result]; + } + } + + setSelectedLinodes(mappedLinodes); + }; + return ( { @@ -162,17 +198,19 @@ export const AddLinodeDrawer = (props: Props) => { }} > {localError ? errorNotice() : null} - - ![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id) - } - disabled={currentDevicesLoading} + + value={selectedLinodes.map((linode) => ({ + id: linode.id, + label: linode.label, + }))} + disabled={currentDevicesLoading || linodesIsLoading} helperText={helperText} - loading={currentDevicesLoading} - multiple - noOptionsMessage="No Linodes available to add" - onSelectionChange={(linodes) => setSelectedLinodes(linodes)} - value={selectedLinodes.map((linode) => linode.id)} + label="Linodes" + loading={currentDevicesLoading || linodesIsLoading} + multiple={true} + noOptionsText="No Linodes available to add" + onChange={(_, linodes) => onChange(linodes)} + options={options} /> Date: Sun, 10 Sep 2023 17:23:42 -0400 Subject: [PATCH 25/29] can select linodes now, but the linodes arent showing as selected --- .../Devices/AddLinodeDrawer.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index d4fbb3cb997..ba8bbeab4c9 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -43,11 +43,6 @@ export const AddLinodeDrawer = (props: Props) => { isLoading: currentDevicesLoading, } = useAllFirewallDevicesQuery(Number(id)); - const currentLinodeIds = - currentDevices - ?.filter((device) => device.entity.type === 'linode') - .map((device) => device.entity.id) ?? []; - const { error, isLoading, @@ -144,13 +139,18 @@ export const AddLinodeDrawer = (props: Props) => { type OptionType = { id: number; label: string }; + const currentLinodeIds = + currentDevices + ?.filter((device) => device.entity.type === 'linode') + .map((device) => device.entity.id) ?? []; + // 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 optionsFilter = (linode: Linode) => { - ![...readOnlyLinodeIds, ...currentLinodeIds].includes(linode.id); + return ![...currentLinodeIds, ...readOnlyLinodeIds].includes(linode.id); }; const { @@ -164,19 +164,17 @@ export const AddLinodeDrawer = (props: Props) => { const options = linodes?.map((linode) => ({ id: linode.id, label: linode.label })) || []; - const onChange = (linodes: OptionType[]) => { - let mappedLinodes: Linode[] = []; + const onChange = (selectedLinodes: OptionType[]) => { + const result = mapIdsToDevices( + selectedLinodes.map((linode) => linode.id), + linodes + ); - if (Array.isArray(linodes)) { - const result = mapIdsToDevices( - linodes.map((linode) => linode.id) - ); - if (Array.isArray(result)) { - mappedLinodes = result; - } else if (result) { - mappedLinodes = [result]; - } - } + const mappedLinodes: Linode[] = Array.isArray(result) + ? result + : result + ? [result] + : []; setSelectedLinodes(mappedLinodes); }; @@ -207,7 +205,7 @@ export const AddLinodeDrawer = (props: Props) => { helperText={helperText} label="Linodes" loading={currentDevicesLoading || linodesIsLoading} - multiple={true} + multiple noOptionsText="No Linodes available to add" onChange={(_, linodes) => onChange(linodes)} options={options} From 64ed737656d166e50245dad93dc9bde2a1ce4562 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Sun, 10 Sep 2023 17:55:57 -0400 Subject: [PATCH 26/29] fixed selection issue with autocomplete --- .../Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index ba8bbeab4c9..b971eeffb04 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -197,10 +197,9 @@ export const AddLinodeDrawer = (props: Props) => { > {localError ? errorNotice() : null} - value={selectedLinodes.map((linode) => ({ - id: linode.id, - label: linode.label, - }))} + value={options.filter((option) => + selectedLinodes.some((linode) => linode.id === option.id) + )} disabled={currentDevicesLoading || linodesIsLoading} helperText={helperText} label="Linodes" From 3cc936b8dbee119b627e45f013f1cf0d5d074ea9 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Mon, 11 Sep 2023 10:28:16 -0400 Subject: [PATCH 27/29] migrated to new autocomplete component --- .../Devices/AddLinodeDrawer.tsx | 60 ++++---- .../Devices/AddNodebalancerDrawer.tsx | 130 ++++++++++++------ 2 files changed, 118 insertions(+), 72 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index b971eeffb04..4e15719c643 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -43,11 +43,9 @@ export const AddLinodeDrawer = (props: Props) => { isLoading: currentDevicesLoading, } = useAllFirewallDevicesQuery(Number(id)); - const { - error, - isLoading, - mutateAsync: addDevice, - } = useAddFirewallDeviceMutation(Number(id)); + const { isLoading, mutateAsync: addDevice } = useAddFirewallDeviceMutation( + Number(id) + ); const [selectedLinodes, setSelectedLinodes] = React.useState([]); @@ -56,40 +54,43 @@ export const AddLinodeDrawer = (props: Props) => { ); const handleSubmit = async () => { + let firstError: string | undefined = undefined; + const failedLinodes: Linode[] = []; + const results = await Promise.allSettled( selectedLinodes.map((linode) => addDevice({ id: linode.id, type: 'linode' }) ) ); - let hasError = false; - results.forEach((result, index) => { const label = selectedLinodes[index].label; + const id = selectedLinodes[index].id; if (result.status === 'fulfilled') { - // Assuming the response contains the device label, replace with the appropriate property if not. enqueueSnackbar(`${label} added successfully.`, { variant: 'success' }); } else { - hasError = true; - // Assuming the error object contains the device label, replace with the appropriate property if not. + failedLinodes?.push(selectedLinodes[index]); + const errorReason = getAPIErrorOrDefault( + result.reason, + `Failed to add Linode ${label} (ID ${id}).` + )[0].reason; + + if (!firstError) { + firstError = errorReason; + } + enqueueSnackbar(`Failed to add ${label}.`, { variant: 'error' }); } }); - if (!hasError) { + setLocalError(firstError); + setSelectedLinodes(failedLinodes); + + if (!firstError) { onClose(); - setSelectedLinodes([]); } }; - React.useEffect(() => { - setLocalError( - error - ? getAPIErrorOrDefault(error, `Error adding Linode`)[0].reason - : undefined - ); - }, [error]); - const errorNotice = () => { let errorMsg = localError || ''; // match something like: Linode (ID ) @@ -155,10 +156,16 @@ export const AddLinodeDrawer = (props: Props) => { const { data, - // error: linodesError, - isLoading: linodesIsLoading, + error: linodeError, + isLoading: linodeIsLoading, } = useAllLinodesQuery(); + React.useEffect(() => { + if (linodeError) { + setLocalError('Could not load Linode Data'); + } + }, [linodeError]); + const linodes = data?.filter(optionsFilter); const options = @@ -197,17 +204,16 @@ export const AddLinodeDrawer = (props: Props) => { > {localError ? errorNotice() : null} - value={options.filter((option) => - selectedLinodes.some((linode) => linode.id === option.id) - )} - disabled={currentDevicesLoading || linodesIsLoading} + disabled={currentDevicesLoading || linodeIsLoading} helperText={helperText} + isOptionEqualToValue={(option, value) => option.id == value.id} label="Linodes" - loading={currentDevicesLoading || linodesIsLoading} + loading={currentDevicesLoading || linodeIsLoading} multiple noOptionsText="No Linodes available to add" onChange={(_, linodes) => onChange(linodes)} options={options} + value={selectedLinodes} /> { 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 { isLoading, mutateAsync: addDevice } = useAddFirewallDeviceMutation( + Number(id) + ); const [selectedNodebalancers, setSelectedNodebalancers] = React.useState< NodeBalancer[] @@ -61,56 +56,56 @@ export const AddNodebalancerDrawer = (props: Props) => { ); const handleSubmit = async () => { + let firstError: string | undefined = undefined; + const failedNodebalancers: NodeBalancer[] = []; + const results = await Promise.allSettled( selectedNodebalancers.map((nodebalancer) => addDevice({ id: nodebalancer.id, type: 'nodebalancer' }) ) ); - let hasError = false; - results.forEach((result, index) => { const label = selectedNodebalancers[index].label; + const id = selectedNodebalancers[index].id; if (result.status === 'fulfilled') { - // Assuming the response contains the device label, replace with the appropriate property if not. enqueueSnackbar(`${label} added successfully.`, { variant: 'success' }); } else { - hasError = true; - // Assuming the error object contains the device label, replace with the appropriate property if not. - // const errorLabel = result.reason.label; - enqueueSnackbar(`Failed to add ${label}.`, { - variant: 'error', - }); + failedNodebalancers?.push(selectedNodebalancers[index]); + const errorReason = getAPIErrorOrDefault( + result.reason, + `Failed to add NodeBalancer ${label} (ID ${id}).` + )[0].reason; + + if (!firstError) { + firstError = errorReason; + } + + enqueueSnackbar(`Failed to add ${label}.`, { variant: 'error' }); } }); - if (!hasError) { + setLocalError(firstError); + setSelectedNodebalancers(failedNodebalancers); + + if (!firstError) { onClose(); - setSelectedNodebalancers([]); } }; - React.useEffect(() => { - setLocalError( - error - ? getAPIErrorOrDefault(error, `Error adding Nodebalancer`)[0].reason - : undefined - ); - }, [error]); - const errorNotice = () => { let errorMsg = localError || ''; // match something like: NodeBalancer (ID ) - const linode = /NodeBalancer (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); + const nodebalancer = /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 (nodebalancer) { + const [, label, id] = nodebalancer; // Break the errorMsg into two parts: before and after the nodebalancer pattern const startMsg = errorMsg.substring( @@ -145,11 +140,59 @@ export const AddNodebalancerDrawer = (props: Props) => { } }; + type OptionType = { id: number; label: string }; + + const currentNodebalancerIds = + currentDevices + ?.filter((device) => device.entity.type === 'nodebalancer') + .map((device) => device.entity.id) ?? []; + // 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 optionsFilter = (nodebalancer: NodeBalancer) => { + return ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( + nodebalancer.id + ); + }; + + const { + data, + error: nodebalancerError, + isLoading: nodebalancerIsLoading, + } = useAllNodeBalancersQuery(); + + React.useEffect(() => { + if (nodebalancerError) { + setLocalError('Could not load NodeBalancer Data'); + } + }, [nodebalancerError]); + + const nodebalancers = data?.filter(optionsFilter); + + const options = + nodebalancers?.map((nodebalancer) => ({ + id: nodebalancer.id, + label: nodebalancer.label, + })) || []; + + const onChange = (selectedNodebalancers: OptionType[]) => { + const result = mapIdsToDevices( + selectedNodebalancers.map((nodebalancer) => nodebalancer.id), + nodebalancers + ); + + const mappedNodebalancers: NodeBalancer[] = Array.isArray(result) + ? result + : result + ? [result] + : []; + + setSelectedNodebalancers(mappedNodebalancers); + }; + return ( { @@ -173,20 +216,17 @@ export const AddNodebalancerDrawer = (props: Props) => { }} > {localError ? errorNotice() : null} - - setSelectedNodebalancers(nodebalancers) - } - optionsFilter={(nodebalancer) => - ![...currentNodebalancerIds, ...readOnlyNodebalancerIds].includes( - nodebalancer.id - ) - } - disabled={currentDevicesLoading} + + disabled={currentDevicesLoading || nodebalancerIsLoading} helperText={helperText} - loading={currentDevicesLoading} + isOptionEqualToValue={(option, value) => option.id == value.id} + label="NodeBalancers" + loading={currentDevicesLoading || nodebalancerIsLoading} multiple - value={selectedNodebalancers.map((nodebalancer) => nodebalancer.id)} + noOptionsText="No NodeBalancers available to add" + onChange={(_, nodebalancers) => onChange(nodebalancers)} + options={options} + value={selectedNodebalancers} /> Date: Mon, 11 Sep 2023 11:11:16 -0400 Subject: [PATCH 28/29] remove NodeBalancerSelect file changes --- .../Domains/CreateDomain/CreateDomain.tsx | 9 +- .../NodeBalancers/NodeBalancerSelect.tsx | 176 +++--------------- 2 files changed, 31 insertions(+), 154 deletions(-) diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 010dba740a3..0aaf0db5cbd 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -413,13 +413,12 @@ export const CreateDomain = () => { defaultRecordsSetting.value === 'nodebalancer' && ( - setSelectedDefaultNodeBalancer(nodebalancer ?? undefined) + onChange={(_, nodebalancer) => + setSelectedDefaultNodeBalancer(nodebalancer) } disabled={disabled} - errorText={errorMap.defaultNodeBalancer} - multiple={false} - value={selectedDefaultNodeBalancer?.id ?? null} + error={errorMap.defaultNodeBalancer} + value={selectedDefaultNodeBalancer?.id} /> {!errorMap.defaultNodeBalancer && ( diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index 1392e7306ff..9cf65b06c48 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -1,72 +1,21 @@ -import { APIError, NodeBalancer } from '@linode/api-v4'; -import CloseIcon from '@mui/icons-material/Close'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { NodeBalancer } from '@linode/api-v4'; import Autocomplete from '@mui/material/Autocomplete'; import * as React from 'react'; -import { - CustomPopper, - SelectedIcon, -} from 'src/components/Autocomplete/Autocomplete.styles'; -import { Box } from 'src/components/Box'; -import { TextField } from 'src/components/TextField'; +import { TextField, TextFieldProps } from 'src/components/TextField'; import { useInfiniteNodebalancersQuery } from 'src/queries/nodebalancers'; -import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; -interface NodeBalancerSelectProps { +interface Props { disabled?: boolean; - errorText?: string; - helperText?: string; - label?: string; - loading?: boolean; - noMarginTop?: boolean; - noOptionsMessage?: string; - options?: NodeBalancer[]; - optionsFilter?: (nodebalancer: NodeBalancer) => boolean; - placeholder?: string; + error?: string; + onChange: (id: number, nodebalancer: NodeBalancer | undefined) => void; region?: string; - renderOption?: (nodebalancer: NodeBalancer, selected: boolean) => JSX.Element; + textFieldProps?: TextFieldProps; + value?: number; } -export interface NodeBalancerMultiSelectProps extends NodeBalancerSelectProps { - /* Enable multi-select. */ - multiple: true; - /* Called when the value changes */ - onSelectionChange: (nodebalancers: NodeBalancer[]) => void; - /* An array of `id`s of NodeBalancers that should be selected or a function that should return `true` if the Linode should be selected. */ - value: ((nodebalancer: NodeBalancer) => boolean) | null | number[]; -} - -export interface NodeBalancerSingleSelectProps extends NodeBalancerSelectProps { - /* Enable single-select. */ - multiple?: false; - /* Called when the value changes */ - onSelectionChange: (nodebalancer: NodeBalancer | null) => void; - /* The `id` of the selected NodeBalancer or a function that should return `true` if the NodeBalancer should be selected. */ - value: ((nodebalancer: NodeBalancer) => boolean) | null | number; -} - -export const NodeBalancerSelect = ( - props: NodeBalancerMultiSelectProps | NodeBalancerSingleSelectProps -) => { - const { - disabled, - errorText, - helperText, - label, - loading, - multiple, - noMarginTop, - noOptionsMessage, - onSelectionChange, - options, - optionsFilter, - placeholder, - region, - renderOption, - value, - } = props; - +export const NodeBalancerSelect = (props: Props) => { + const { disabled, error, onChange, region, value } = props; const [inputValue, setInputValue] = React.useState(''); const searchFilter = inputValue @@ -80,7 +29,6 @@ export const NodeBalancerSelect = ( const { data, - error, fetchNextPage, hasNextPage, isLoading, @@ -90,22 +38,12 @@ export const NodeBalancerSelect = ( '+order': 'asc', '+order_by': 'label', }); + const nodebalancers = data?.pages.flatMap((page) => page.data); - const unfilteredNodebalancers = data?.pages.flatMap((page) => page.data); - - const nodebalancers = optionsFilter - ? unfilteredNodebalancers?.filter(optionsFilter) - : unfilteredNodebalancers; + const options = nodebalancers?.map(({ id, label }) => ({ id, label })); - React.useEffect(() => { - /** We want to clear the input value when the value prop changes to null. - * This is for use cases where a user changes their region and the Linode - * they had selected is no longer available. - */ - if (value === null) { - setInputValue(''); - } - }, [value]); + const selectedNodebalancer = + options?.find((option) => option.id === value) ?? null; return ( {getDefaultNoOptionsMessage(error, isLoading, nodebalancers)} + onChange={(event, value) => + onChange( + value?.id ?? -1, + nodebalancers?.find((n) => n.id === value?.id) ) } - onChange={(_, value) => - multiple && Array.isArray(value) - ? onSelectionChange(value) - : !multiple && !Array.isArray(value) && onSelectionChange(value) - } + onInputChange={(event, value) => { + setInputValue(value); + }} renderInput={(params) => ( )} - renderOption={(props, option, { selected }) => { - return ( -
  • - {renderOption ? ( - renderOption(option, selected) - ) : ( - <> - - {option.label} - - - - )} -
  • - ); - }} - value={ - typeof value === 'function' - ? multiple && Array.isArray(value) - ? nodebalancers?.filter(value) ?? null - : nodebalancers?.find(value) ?? null - : mapIdsToDevices(value ?? null, nodebalancers) - } - ChipProps={{ deleteIcon: }} - PopperComponent={CustomPopper} - clearOnBlur={false} - disableCloseOnSelect={multiple} - disablePortal={true} disabled={disabled} inputValue={inputValue} - // isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} - loading={isLoading || loading} - multiple={multiple} - onInputChange={(_, value) => setInputValue(value)} - options={options || (nodebalancers ?? [])} - popupIcon={} + isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} + loading={isLoading} + options={options ?? []} + value={selectedNodebalancer} /> ); }; - -const getDefaultNoOptionsMessage = ( - error: APIError[] | null, - loading: boolean, - filteredNodeBalancers: NodeBalancer[] | undefined -) => { - if (error) { - return 'An error occured while fetching your NodeBalancers'; - } else if (loading) { - return 'Loading your NodeBalancers...'; - } else if (!filteredNodeBalancers?.length) { - return 'You have no NodeBalancers to choose from'; - } else { - return 'No options'; - } -}; From ce25f88ccd67644e1602fee18c3966de04328a04 Mon Sep 17 00:00:00 2001 From: TylerWJ Date: Mon, 11 Sep 2023 14:48:08 -0400 Subject: [PATCH 29/29] added banks PR suggestions --- .../Devices/AddLinodeDrawer.tsx | 28 ++-------------- .../Devices/AddNodebalancerDrawer.tsx | 33 +++---------------- 2 files changed, 8 insertions(+), 53 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index 4e15719c643..c1b3a826d5a 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -18,7 +18,6 @@ import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; -import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; interface Props { helperText: string; @@ -138,8 +137,6 @@ export const AddLinodeDrawer = (props: Props) => { } }; - type OptionType = { id: number; label: string }; - const currentLinodeIds = currentDevices ?.filter((device) => device.entity.type === 'linode') @@ -168,24 +165,6 @@ export const AddLinodeDrawer = (props: Props) => { const linodes = data?.filter(optionsFilter); - const options = - linodes?.map((linode) => ({ id: linode.id, label: linode.label })) || []; - - const onChange = (selectedLinodes: OptionType[]) => { - const result = mapIdsToDevices( - selectedLinodes.map((linode) => linode.id), - linodes - ); - - const mappedLinodes: Linode[] = Array.isArray(result) - ? result - : result - ? [result] - : []; - - setSelectedLinodes(mappedLinodes); - }; - return ( { @@ -203,16 +182,15 @@ export const AddLinodeDrawer = (props: Props) => { }} > {localError ? errorNotice() : null} - + option.id == value.id} label="Linodes" loading={currentDevicesLoading || linodeIsLoading} multiple noOptionsText="No Linodes available to add" - onChange={(_, linodes) => onChange(linodes)} - options={options} + onChange={(_, linodes) => setSelectedLinodes(linodes)} + options={linodes || []} value={selectedLinodes} /> { } }; - type OptionType = { id: number; label: string }; - const currentNodebalancerIds = currentDevices ?.filter((device) => device.entity.type === 'nodebalancer') @@ -172,27 +169,6 @@ export const AddNodebalancerDrawer = (props: Props) => { const nodebalancers = data?.filter(optionsFilter); - const options = - nodebalancers?.map((nodebalancer) => ({ - id: nodebalancer.id, - label: nodebalancer.label, - })) || []; - - const onChange = (selectedNodebalancers: OptionType[]) => { - const result = mapIdsToDevices( - selectedNodebalancers.map((nodebalancer) => nodebalancer.id), - nodebalancers - ); - - const mappedNodebalancers: NodeBalancer[] = Array.isArray(result) - ? result - : result - ? [result] - : []; - - setSelectedNodebalancers(mappedNodebalancers); - }; - return ( { @@ -216,16 +192,17 @@ export const AddNodebalancerDrawer = (props: Props) => { }} > {localError ? errorNotice() : null} - + + setSelectedNodebalancers(nodebalancers) + } disabled={currentDevicesLoading || nodebalancerIsLoading} helperText={helperText} - isOptionEqualToValue={(option, value) => option.id == value.id} label="NodeBalancers" loading={currentDevicesLoading || nodebalancerIsLoading} multiple noOptionsText="No NodeBalancers available to add" - onChange={(_, nodebalancers) => onChange(nodebalancers)} - options={options} + options={nodebalancers || []} value={selectedNodebalancers} />