From 02eeb058c9cef11ebdd00dd6dd92dc32a9754ff1 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 00:46:19 -0400 Subject: [PATCH 01/19] add links and default chip --- .../src/features/Account/DefaultFirewalls.tsx | 5 +- .../FirewallLanding/FirewallActionMenu.tsx | 35 +++-- .../FirewallLanding/FirewallLanding.tsx | 17 ++- .../FirewallLanding/FirewallRow.test.tsx | 40 +++++- .../Firewalls/FirewallLanding/FirewallRow.tsx | 124 +++++++++++++++--- .../Firewalls/FirewallLanding/constants.ts | 5 + .../components/DefaultFirewallChip.tsx | 22 ++++ .../components/FirewallSelectOption.utils.tsx | 85 ++++++++++++ .../LinodeFirewallsActionMenu.tsx | 9 +- .../NodeBalancerFirewallsActionMenu.tsx | 4 +- packages/queries/src/firewalls/firewalls.ts | 14 +- packages/ui/src/components/Chip/Chip.tsx | 8 +- 12 files changed, 316 insertions(+), 52 deletions(-) create mode 100644 packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx diff --git a/packages/manager/src/features/Account/DefaultFirewalls.tsx b/packages/manager/src/features/Account/DefaultFirewalls.tsx index 6d8fecc4716..324ce4c6db5 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.tsx @@ -21,18 +21,21 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + import type { UpdateFirewallSettings } from '@linode/api-v4'; const DEFAULT_FIREWALL_PLACEHOLDER = 'None'; export const DefaultFirewalls = () => { const { enqueueSnackbar } = useSnackbar(); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { data: firewallSettings, error: firewallSettingsError, isLoading: isLoadingFirewallSettings, - } = useFirewallSettingsQuery(); + } = useFirewallSettingsQuery({ enabled: isLinodeInterfacesEnabled }); const { mutateAsync: updateFirewallSettings } = useMutateFirewallSettings(); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx index cce9abb8f58..2e04059fb12 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx @@ -1,13 +1,20 @@ -import { FirewallStatus } from '@linode/api-v4/lib/firewalls'; -import { Theme, useTheme } from '@mui/material/styles'; +import { useGrants, useProfile } from '@linode/queries'; +import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; -import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { useGrants, useProfile } from '@linode/queries'; import { checkIfUserCanModifyFirewall } from '../shared'; +import { + DEFAULT_FIREWALL_TOOLTIP_TEXT, + NO_PERMISSIONS_TOOLTIP_TEXT, +} from './constants'; + +import type { FirewallStatus } from '@linode/api-v4/lib/firewalls'; +import type { Theme } from '@mui/material/styles'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; export interface ActionHandlers { [index: string]: any; @@ -20,11 +27,9 @@ interface Props extends ActionHandlers { firewallID: number; firewallLabel: string; firewallStatus: FirewallStatus; + isDefaultFirewall: boolean; } -export const noPermissionTooltipText = - "You don't have permissions to modify this Firewall."; - export const FirewallActionMenu = React.memo((props: Props) => { const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); @@ -35,6 +40,7 @@ export const FirewallActionMenu = React.memo((props: Props) => { firewallID, firewallLabel, firewallStatus, + isDefaultFirewall, triggerDeleteFirewall, triggerDisableFirewall, triggerEnableFirewall, @@ -46,12 +52,15 @@ export const FirewallActionMenu = React.memo((props: Props) => { grants ); - const disabledProps = !userCanModifyFirewall - ? { - disabled: true, - tooltip: noPermissionTooltipText, - } - : {}; + const disabledProps = + !userCanModifyFirewall || isDefaultFirewall + ? { + disabled: true, + tooltip: isDefaultFirewall + ? DEFAULT_FIREWALL_TOOLTIP_TEXT + : NO_PERMISSIONS_TOOLTIP_TEXT, + } + : {}; const actions: Action[] = [ { diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index 4a8842f12c2..bba852519a9 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -1,3 +1,4 @@ +import { useFirewallSettingsQuery, useFirewallsQuery } from '@linode/queries'; import { Button, CircleProgress, ErrorState } from '@linode/ui'; import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; @@ -19,8 +20,8 @@ import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; -import { useFirewallsQuery } from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { CreateFirewallDrawer } from './CreateFirewallDrawer'; import { FirewallDialog } from './FirewallDialog'; @@ -35,6 +36,7 @@ const preferenceKey = 'firewalls'; const FirewallLanding = () => { const navigate = useNavigate(); const location = useLocation(); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const pagination = usePagination(1, preferenceKey); const { handleOrderChange, order, orderBy } = useOrder( { @@ -54,6 +56,10 @@ const FirewallLanding = () => { page_size: pagination.pageSize, }; + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled, + }); + const { data, error, isLoading } = useFirewallsQuery(params, filter); const isCreateFirewallDrawerOpen = location.pathname.endsWith('create'); @@ -189,14 +195,19 @@ const FirewallLanding = () => { Rules - Services + Services {data?.data.map((firewall) => ( - + ))} diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index 6dd12b34133..4ea808fc738 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -49,11 +49,33 @@ describe('FirewallRow', () => { const baseProps = { ...firewall, + firewallSettings: undefined, triggerDeleteFirewall: mockTriggerDeleteFirewall, triggerDisableFirewall: mockTriggerDisableFirewall, triggerEnableFirewall: mockTriggerEnableFirewall, }; + it('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { + const { getByTestId, getByText } = render( + wrapWithTableBody( + + ) + ); + getByTestId('firewall-row-1'); + getByText(firewall.label); + getByText('DEFAULT'); + }); + it('renders a TableRow with label, status, rules, and Linodes', () => { const { getByTestId, getByText } = render( wrapWithTableBody() @@ -68,21 +90,33 @@ describe('FirewallRow', () => { describe('getDeviceLinks', () => { it('should return a single Link if one Device is attached', () => { const device = firewallDeviceFactory.build(); - const links = getDeviceLinks([device.entity]); + const links = getDeviceLinks({ + entities: [device.entity], + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { getByText } = renderWithTheme(links); expect(getByText(device.entity.label ?? '')); }); it('should render up to three comma-separated links', () => { const devices = firewallDeviceFactory.buildList(3); - const links = getDeviceLinks(devices.map((device) => device.entity)); + const links = getDeviceLinks({ + entities: devices.map((device) => device.entity), + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { queryAllByTestId } = renderWithTheme(links); expect(queryAllByTestId('firewall-row-link')).toHaveLength(3); }); it('should render "plus N more" text for any devices over three', () => { const devices = firewallDeviceFactory.buildList(13); - const links = getDeviceLinks(devices.map((device) => device.entity)); + const links = getDeviceLinks({ + entities: devices.map((device) => device.entity), + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { getByText, queryAllByTestId } = renderWithTheme(links); expect(queryAllByTestId('firewall-row-link')).toHaveLength(3); expect(getByText(/10 more/)); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index f5b2c02701c..a26bdbc4c64 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -1,21 +1,67 @@ +import { useAllLinodesQuery } from '@linode/queries'; import { capitalize } from '@linode/utilities'; import React from 'react'; import { Hidden } from 'src/components/Hidden'; import { Link } from 'src/components/Link'; +import { Skeleton } from 'src/components/Skeleton'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { DefaultFirewallChip } from '../components/DefaultFirewallChip'; +import { getDefaultFirewallDescription } from '../components/FirewallSelectOption.utils'; import { FirewallActionMenu } from './FirewallActionMenu'; import type { ActionHandlers } from './FirewallActionMenu'; -import type { Firewall, FirewallDeviceEntity } from '@linode/api-v4'; +import type { + Filter, + Firewall, + FirewallDeviceEntity, + FirewallSettings, + Linode, +} from '@linode/api-v4'; -export interface FirewallRowProps extends Firewall, ActionHandlers {} +export interface FirewallRowProps extends Firewall, ActionHandlers { + firewallSettings: FirewallSettings | undefined; +} export const FirewallRow = React.memo((props: FirewallRowProps) => { - const { entities, id, label, rules, status, ...actionHandlers } = props; + const { + entities, + firewallSettings, + id, + label, + rules, + status, + ...actionHandlers + } = props; + + const tooltipText = + firewallSettings && getDefaultFirewallDescription(id, firewallSettings); + const isDefaultFirewall = !!tooltipText; + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const neededLinodeIdsForInterfaceDevices = entities + .slice(0, 3) // only take the first three entities since we only show those entity links + .filter((entity) => entity.type === 'interface') + .map((entity) => { + return { id: Number(entity.url.split('/')[4]) }; + }); + + const filterForInterfaceDeviceLinodes: Filter = { + ['+or']: neededLinodeIdsForInterfaceDevices, + }; + + // only fire this query if we have linode interface devices. We fetch the Linodes those devices are attached to + // so that we can add a label to the devices for sorting and display purposes + const { data: linodesWithInterfaceDevices, isLoading } = useAllLinodesQuery( + {}, + filterForInterfaceDeviceLinodes, + isLinodeInterfacesEnabled && neededLinodeIdsForInterfaceDevices.length > 0 + ); const count = getCountOfRules(rules); @@ -25,6 +71,12 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {label} + {isDefaultFirewall && ( + + )} @@ -32,7 +84,13 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {getRuleString(count)} - {getDevicesCellString(entities)} + + {getDevicesCellString({ + entities, + isLoading, + linodesWithInterfaceDevices, + })} + { firewallID={id} firewallLabel={label} firewallStatus={status} + isDefaultFirewall={isDefaultFirewall} {...actionHandlers} /> @@ -77,31 +136,58 @@ export const getCountOfRules = (rules: Firewall['rules']): [number, number] => { return [(rules.inbound || []).length, (rules.outbound || []).length]; }; -const getDevicesCellString = (entities: FirewallDeviceEntity[]) => { +interface DeviceLinkInputs { + entities: FirewallDeviceEntity[]; + isLoading: boolean; + linodesWithInterfaceDevices: Linode[] | undefined; +} +const getDevicesCellString = (inputs: DeviceLinkInputs) => { + const { entities, isLoading, linodesWithInterfaceDevices } = inputs; if (entities.length === 0) { return 'None assigned'; } - return getDeviceLinks(entities); + return getDeviceLinks({ entities, isLoading, linodesWithInterfaceDevices }); }; -export const getDeviceLinks = (entities: FirewallDeviceEntity[]) => { +export const getDeviceLinks = (inputs: DeviceLinkInputs) => { + const { entities, isLoading, linodesWithInterfaceDevices } = inputs; const firstThree = entities.slice(0, 3); + if (isLoading) { + return ; + } + return ( <> - {firstThree.map((entity, idx) => ( - - {idx > 0 && ', '} - - {entity.label} - - - ))} + {firstThree.map((entity, idx) => { + // TODO @Linode Interfaces - switch to parent entity when endpoints are updated + // TODO @Linode Interfaces - update interface links to interface details soon + const entityId = + entity.type === 'interface' + ? Number(entity.url.split('/')[4]) + : entity.id; + const entityLabel = + entity.type === 'interface' + ? linodesWithInterfaceDevices?.find( + (linode) => linode.id === entityId + )?.label ?? entity.label + : entity.label; + const entityLink = entity.type === 'interface' ? 'linode' : entity.type; + + return ( + + {idx > 0 && ', '} + + {entityLabel} + + + ); + })} {entities.length > 3 && , plus {entities.length - 3} more.} ); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts index d2523ab4dbf..fee644396f3 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts +++ b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts @@ -12,3 +12,8 @@ export const NODEBALANCER_HELPER_TEXT = export const STRENGTHEN_TEMPLATE_RULES = 'It is recommended to further strengthen these rules by limiting the allowed IPv4 and IPv6 ranges.'; + +export const NO_PERMISSIONS_TOOLTIP_TEXT = + "You don't have permissions to modify this Firewall."; +export const DEFAULT_FIREWALL_TOOLTIP_TEXT = + 'This firewall is used as an interface default and cannot be modified. Change the firewall default assignment in Account Settings to modify the firewall.'; diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx new file mode 100644 index 00000000000..7f45ea82312 --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -0,0 +1,22 @@ +import { Chip, Tooltip } from '@linode/ui'; +import React from 'react'; + +import type { SxProps } from '@mui/material'; + +interface Props { + chipProps?: { + sx?: SxProps; + }; + tooltipText: React.ReactNode; +} + +export const DefaultFirewallChip = (props: Props) => { + return ( + + + + ); +}; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx new file mode 100644 index 00000000000..33b325bb922 --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx @@ -0,0 +1,85 @@ +import { List, ListItem, Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import type { FirewallSettings } from '@linode/api-v4'; + +export type FirewallDefaultEntity = keyof FirewallSettings['default_firewall_ids']; + +/** + * Maps an entity that supports default firewalls to a readable name. + */ +const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< + FirewallDefaultEntity, + string +> = { + linode: 'Configuration Profile Interfaces', + nodebalancer: 'NodeBalancers', + public_interface: 'Public (Linode Interfaces)', + vpc_interface: 'VPC (Linode Interfaces)', +}; + +/** + * getEntitiesThatFirewallIsDefaultFor + * + * @param firewallId The ID of the Firewall + * @param firewallSettings The account FirewallSettings from the API + * + * @returns An array of entities that this Firewall is a default for. + * @example ['nodebalancer', 'vpc_interface'] + */ +export function getEntitiesThatFirewallIsDefaultFor( + firewallId: number, + firewallSettings: FirewallSettings +) { + const defaultFor: FirewallDefaultEntity[] = []; + + for (const key in firewallSettings.default_firewall_ids) { + const entity = key as FirewallDefaultEntity; + if (firewallSettings.default_firewall_ids[entity] === firewallId) { + defaultFor.push(entity); + } + } + + return defaultFor; +} + +/** + * getDefaultFirewallDescription + * + * @param firewallId The ID of the Firewall + * @param firewallSettings The account FirewallSettings from the API + * + * @returns A human readable string that explains what entities this Firewall is a default for. + * It will return `null` if this Firewall is not a default for anything. + */ +export function getDefaultFirewallDescription( + firewallId: number, + firewallSettings: FirewallSettings +) { + const entitiesThatFirewallIsDefaultFor = getEntitiesThatFirewallIsDefaultFor( + firewallId, + firewallSettings + ); + + if (entitiesThatFirewallIsDefaultFor.length === 0) { + // This means that a Firewall is not a default. + return null; + } + + const readableEntities = entitiesThatFirewallIsDefaultFor.map( + (entity) => FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME[entity] + ); + + return ( + + Default Firewall for: + + {readableEntities.map((entity) => ( + + {entity} + + ))} + + + ); +} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx index 848557129eb..1c8a85bafdd 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx @@ -1,10 +1,11 @@ +import { useGrants, useProfile } from '@linode/queries'; import * as React from 'react'; -import { Action } from 'src/components/ActionMenu/ActionMenu'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { noPermissionTooltipText } from 'src/features/Firewalls/FirewallLanding/FirewallActionMenu'; +import { NO_PERMISSIONS_TOOLTIP_TEXT } from 'src/features/Firewalls/FirewallLanding/constants'; import { checkIfUserCanModifyFirewall } from 'src/features/Firewalls/shared'; -import { useGrants, useProfile } from '@linode/queries'; + +import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface LinodeFirewallsActionMenuProps { firewallID: number; @@ -28,7 +29,7 @@ export const LinodeFirewallsActionMenu = ( const disabledProps = !userCanModifyFirewall ? { disabled: true, - tooltip: noPermissionTooltipText, + tooltip: NO_PERMISSIONS_TOOLTIP_TEXT, } : {}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewallsActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewallsActionMenu.tsx index 8fa521d279c..4a9ad264265 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewallsActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewallsActionMenu.tsx @@ -2,7 +2,7 @@ import { useGrants, useProfile } from '@linode/queries'; import * as React from 'react'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { noPermissionTooltipText } from 'src/features/Firewalls/FirewallLanding/FirewallActionMenu'; +import { NO_PERMISSIONS_TOOLTIP_TEXT } from 'src/features/Firewalls/FirewallLanding/constants'; import { checkIfUserCanModifyFirewall } from 'src/features/Firewalls/shared'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; @@ -27,7 +27,7 @@ export const NodeBalancerFirewallsActionMenu = (props: Props) => { const disabledProps = !userCanModifyFirewall ? { disabled: true, - tooltip: noPermissionTooltipText, + tooltip: NO_PERMISSIONS_TOOLTIP_TEXT, } : {}; diff --git a/packages/queries/src/firewalls/firewalls.ts b/packages/queries/src/firewalls/firewalls.ts index 0e7b89a8247..fea962ac76f 100644 --- a/packages/queries/src/firewalls/firewalls.ts +++ b/packages/queries/src/firewalls/firewalls.ts @@ -5,12 +5,12 @@ import { deleteFirewallDevice, getFirewall, getFirewallDevices, + getFirewallSettings, getFirewalls, getTemplate, getTemplates, updateFirewall, updateFirewallRules, - getFirewallSettings, updateFirewallSettings, } from '@linode/api-v4/lib/firewalls'; import { getAll } from '@linode/utilities'; @@ -36,14 +36,15 @@ import type { FirewallDevice, FirewallDevicePayload, FirewallRules, + FirewallSettings, FirewallTemplate, FirewallTemplateSlug, Params, ResourcePage, UpdateFirewallRules, - FirewallSettings, UpdateFirewallSettings, } from '@linode/api-v4'; +import type { UseQueryOptions } from '@tanstack/react-query'; const getAllFirewallDevices = ( id: number, @@ -285,8 +286,13 @@ export const useFirewallsQuery = (params?: Params, filter?: Filter) => { }); }; -export const useFirewallSettingsQuery = () => { - return useQuery(firewallQueries.settings); +export const useFirewallSettingsQuery = ( + options?: Partial> +) => { + return useQuery({ + ...firewallQueries.settings, + ...options, + }); }; export const useFirewallTemplatesQuery = () => { diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index 18a6b36f144..8f5a56d1f2b 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -10,6 +10,8 @@ export interface ChipProps extends _ChipProps { component?: React.ElementType; } -export const Chip = (props: ChipProps) => { - return <_Chip {...props} />; -}; +export const Chip = React.forwardRef( + (props: ChipProps, ref) => { + return <_Chip ref={ref} {...props} />; + } +); From 091775968ae8d7c9dedd544a360437e9a757cf49 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 09:21:03 -0400 Subject: [PATCH 02/19] hide stuff behind feature flag --- .../FirewallLanding/FirewallActionMenu.tsx | 4 ++- .../FirewallLanding/FirewallRow.test.tsx | 3 ++- .../Firewalls/FirewallLanding/FirewallRow.tsx | 27 +++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx index 2e04059fb12..699b7e23c2a 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx @@ -15,6 +15,7 @@ import { import type { FirewallStatus } from '@linode/api-v4/lib/firewalls'; import type { Theme } from '@mui/material/styles'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; export interface ActionHandlers { [index: string]: any; @@ -35,6 +36,7 @@ export const FirewallActionMenu = React.memo((props: Props) => { const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); const { data: profile } = useProfile(); const { data: grants } = useGrants(); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { firewallID, @@ -53,7 +55,7 @@ export const FirewallActionMenu = React.memo((props: Props) => { ); const disabledProps = - !userCanModifyFirewall || isDefaultFirewall + !userCanModifyFirewall || (isLinodeInterfacesEnabled && isDefaultFirewall) ? { disabled: true, tooltip: isDefaultFirewall diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index 4ea808fc738..0519f27e3f0 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -68,7 +68,8 @@ describe('FirewallRow', () => { vpc_interface: null, }, }} - /> + />, + { flags: { linodeInterfaces: { enabled: true } } } ) ); getByTestId('firewall-row-1'); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index a26bdbc4c64..c17b3dada1b 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -71,7 +71,7 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {label} - {isDefaultFirewall && ( + {isLinodeInterfacesEnabled && isDefaultFirewall && ( { {getDevicesCellString({ entities, + isLinodeInterfacesEnabled, isLoading, linodesWithInterfaceDevices, })} @@ -138,19 +139,35 @@ export const getCountOfRules = (rules: Firewall['rules']): [number, number] => { interface DeviceLinkInputs { entities: FirewallDeviceEntity[]; + isLinodeInterfacesEnabled: boolean; isLoading: boolean; linodesWithInterfaceDevices: Linode[] | undefined; } const getDevicesCellString = (inputs: DeviceLinkInputs) => { - const { entities, isLoading, linodesWithInterfaceDevices } = inputs; - if (entities.length === 0) { + const { + entities, + isLinodeInterfacesEnabled, + isLoading, + linodesWithInterfaceDevices, + } = inputs; + const filteredEntities = isLinodeInterfacesEnabled + ? entities + : entities.filter((entity) => entity.type !== 'interface'); + + if (filteredEntities.length === 0) { return 'None assigned'; } - return getDeviceLinks({ entities, isLoading, linodesWithInterfaceDevices }); + return getDeviceLinks({ + entities: filteredEntities, + isLoading, + linodesWithInterfaceDevices, + }); }; -export const getDeviceLinks = (inputs: DeviceLinkInputs) => { +export const getDeviceLinks = ( + inputs: Omit +) => { const { entities, isLoading, linodesWithInterfaceDevices } = inputs; const firstThree = entities.slice(0, 3); From 37f1dc6e7a99472e936e50eb3166529fc4ce2f54 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 09:22:20 -0400 Subject: [PATCH 03/19] Added changeset: Update Firewall Landing table to account for Linode Interface devices and Default Firewalls --- .../.changeset/pr-11920-upcoming-features-1742908940171.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md diff --git a/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md b/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md new file mode 100644 index 00000000000..84d25a7c2ed --- /dev/null +++ b/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Firewall Landing table to account for Linode Interface devices and Default Firewalls ([#11920](https://github.com/linode/manager/pull/11920)) From 93adb52aac606b16803572155b4c68ab3960d172 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:12:28 -0400 Subject: [PATCH 04/19] add default firewall paper in firewall detail --- .../Firewalls/FirewallDetail/index.tsx | 43 ++++++++++++++++++- .../FirewallLanding/FirewallLanding.tsx | 2 +- .../components/FirewallSelectOption.utils.tsx | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 28df22916cf..8973ac78be4 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -1,11 +1,13 @@ import { useAllFirewallDevicesQuery, useFirewallQuery, + useFirewallSettingsQuery, useGrants, useMutateFirewall, useProfile, } from '@linode/queries'; -import { CircleProgress, ErrorState } from '@linode/ui'; +import { Box, Chip, CircleProgress, ErrorState, Paper } from '@linode/ui'; +import { Typography } from '@mui/material'; import { useParams } from '@tanstack/react-router'; import * as React from 'react'; @@ -15,6 +17,7 @@ import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/Ge import { LandingHeader } from 'src/components/LandingHeader'; import { LinkButton } from 'src/components/LinkButton'; import { NotFound } from 'src/components/NotFound'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; @@ -25,8 +28,11 @@ import { useTabs } from 'src/hooks/useTabs'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { + FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME, + getEntitiesThatFirewallIsDefaultFor, +} from '../components/FirewallSelectOption.utils'; import { checkIfUserCanModifyFirewall } from '../shared'; -import { SuspenseLoader } from 'src/components/SuspenseLoader'; const FirewallRulesLanding = React.lazy(() => import('./Rules/FirewallRulesLanding').then((module) => ({ @@ -57,6 +63,14 @@ export const FirewallDetail = () => { const firewallId = Number(id); + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled, + }); + + const defaultForEntities = + firewallSettings && + getEntitiesThatFirewallIsDefaultFor(firewallId, firewallSettings); + const userCanModifyFirewall = checkIfUserCanModifyFirewall( firewallId, profile, @@ -173,6 +187,31 @@ export const FirewallDetail = () => { {...secureVMFirewallBanner.firewallDetails} /> )} + {isLinodeInterfacesEnabled && defaultForEntities && ( + + + Default + + + {defaultForEntities.map((defaultEntity) => ( + + ))} + + + )} }> diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index bba852519a9..34dc036f2bd 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -195,7 +195,7 @@ const FirewallLanding = () => { Rules - Services + Services diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx index 33b325bb922..32b3090d710 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx @@ -8,7 +8,7 @@ export type FirewallDefaultEntity = keyof FirewallSettings['default_firewall_ids /** * Maps an entity that supports default firewalls to a readable name. */ -const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< +export const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< FirewallDefaultEntity, string > = { From 5b0efca62c1353bf0885c8de2ccc0e0b66802edc Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:13:30 -0400 Subject: [PATCH 05/19] update conditions --- .../Firewalls/FirewallDetail/index.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 8973ac78be4..a5ed41a2c25 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -187,31 +187,35 @@ export const FirewallDetail = () => { {...secureVMFirewallBanner.firewallDetails} /> )} - {isLinodeInterfacesEnabled && defaultForEntities && ( - - - Default - - - {defaultForEntities.map((defaultEntity) => ( - - ))} - - - )} + {isLinodeInterfacesEnabled && + defaultForEntities && + defaultForEntities.length > 0 && ( + + + Default + + + {defaultForEntities.map((defaultEntity) => ( + + ))} + + + )} }> From 5b0c1cdae95378a76fa673ac487aeb29f5933a8e Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:16:43 -0400 Subject: [PATCH 06/19] update spacing --- .../src/features/Firewalls/FirewallDetail/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index a5ed41a2c25..9c6f9712017 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -191,14 +191,16 @@ export const FirewallDetail = () => { defaultForEntities && defaultForEntities.length > 0 && ( ({ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 2, - margin: '16px 0px', - padding: 2, - }} + margin: `${theme.spacingFunction(16)} 0`, + padding: `${theme.spacingFunction(8)} ${theme.spacingFunction( + 16 + )}`, + })} > Default From bfc7a4f00d07888240bd680cee3014c828968f71 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:17:33 -0400 Subject: [PATCH 07/19] update spacing again... --- .../manager/src/features/Firewalls/FirewallDetail/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 9c6f9712017..2c1556b2082 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -196,7 +196,7 @@ export const FirewallDetail = () => { display: 'flex', flexWrap: 'wrap', gap: 2, - margin: `${theme.spacingFunction(16)} 0`, + margin: `${theme.spacingFunction(8)} 0`, padding: `${theme.spacingFunction(8)} ${theme.spacingFunction( 16 )}`, From 8625f9c7e3f385df5515102ddcd05f245e189ea3 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:19:23 -0400 Subject: [PATCH 08/19] Added changeset: Add Default Firewall chips to Firewall Detail page --- .../.changeset/pr-11920-upcoming-features-1742912363636.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md diff --git a/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md b/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md new file mode 100644 index 00000000000..2c67bcdf023 --- /dev/null +++ b/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Default Firewall chips to Firewall Detail page ([#11920](https://github.com/linode/manager/pull/11920)) From 3bc2bbd8780f83145d50b850587057f806093288 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 10:29:56 -0400 Subject: [PATCH 09/19] what is spacing --- .../Firewalls/FirewallDetail/index.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 2c1556b2082..a60331e823b 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -6,7 +6,7 @@ import { useMutateFirewall, useProfile, } from '@linode/queries'; -import { Box, Chip, CircleProgress, ErrorState, Paper } from '@linode/ui'; +import { Chip, CircleProgress, ErrorState, Paper } from '@linode/ui'; import { Typography } from '@mui/material'; import { useParams } from '@tanstack/react-router'; import * as React from 'react'; @@ -193,29 +193,26 @@ export const FirewallDetail = () => { ({ alignItems: 'center', + columnGap: 2, display: 'flex', flexWrap: 'wrap', - gap: 2, margin: `${theme.spacingFunction(8)} 0`, padding: `${theme.spacingFunction(8)} ${theme.spacingFunction( 16 )}`, + rowGap: 1, })} > Default - - {defaultForEntities.map((defaultEntity) => ( - - ))} - + {defaultForEntities.map((defaultEntity) => ( + + ))} )} From f40f5a34cab86823d8b264ee71ff4f2df99b68c8 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 11:12:03 -0400 Subject: [PATCH 10/19] spacing update --- .../manager/src/features/Firewalls/FirewallDetail/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index a60331e823b..9f2d03d5ef1 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -193,7 +193,7 @@ export const FirewallDetail = () => { ({ alignItems: 'center', - columnGap: 2, + columnGap: 1, display: 'flex', flexWrap: 'wrap', margin: `${theme.spacingFunction(8)} 0`, From f40fe864be87707a337397f30b7aa9179bca44df Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:32:02 -0400 Subject: [PATCH 11/19] Update packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> --- .../src/features/Firewalls/components/DefaultFirewallChip.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx index 7f45ea82312..e40395d35df 100644 --- a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -4,9 +4,7 @@ import React from 'react'; import type { SxProps } from '@mui/material'; interface Props { - chipProps?: { - sx?: SxProps; - }; + chipProps?: Partial; tooltipText: React.ReactNode; } From 3d93fa421ee8e58c28228913bafb119dcd169278 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 13:43:54 -0400 Subject: [PATCH 12/19] feedback pt1 --- .../FirewallLanding/FirewallLanding.tsx | 15 +------ .../FirewallLanding/FirewallRow.test.tsx | 42 ++++++++++++------- .../Firewalls/FirewallLanding/FirewallRow.tsx | 25 ++++------- .../components/DefaultFirewallChip.tsx | 2 +- 4 files changed, 38 insertions(+), 46 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index 34dc036f2bd..9609bb1e2ff 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -1,4 +1,4 @@ -import { useFirewallSettingsQuery, useFirewallsQuery } from '@linode/queries'; +import { useFirewallsQuery } from '@linode/queries'; import { Button, CircleProgress, ErrorState } from '@linode/ui'; import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; @@ -21,7 +21,6 @@ import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { CreateFirewallDrawer } from './CreateFirewallDrawer'; import { FirewallDialog } from './FirewallDialog'; @@ -36,7 +35,6 @@ const preferenceKey = 'firewalls'; const FirewallLanding = () => { const navigate = useNavigate(); const location = useLocation(); - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const pagination = usePagination(1, preferenceKey); const { handleOrderChange, order, orderBy } = useOrder( { @@ -56,10 +54,6 @@ const FirewallLanding = () => { page_size: pagination.pageSize, }; - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled, - }); - const { data, error, isLoading } = useFirewallsQuery(params, filter); const isCreateFirewallDrawerOpen = location.pathname.endsWith('create'); @@ -202,12 +196,7 @@ const FirewallLanding = () => { {data?.data.map((firewall) => ( - + ))} diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index 0519f27e3f0..02f69236690 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -20,6 +20,18 @@ import { getRuleString, } from './FirewallRow'; +const queryMocks = vi.hoisted(() => ({ + useFirewallSettingsQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useFirewallSettingsQuery: queryMocks.useFirewallSettingsQuery, + }; +}); + beforeAll(() => mockMatchMedia()); describe('FirewallRow', () => { @@ -49,28 +61,26 @@ describe('FirewallRow', () => { const baseProps = { ...firewall, - firewallSettings: undefined, triggerDeleteFirewall: mockTriggerDeleteFirewall, triggerDisableFirewall: mockTriggerDisableFirewall, triggerEnableFirewall: mockTriggerEnableFirewall, }; - it('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { + it.only('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { + queryMocks.useFirewallSettingsQuery.mockReturnValue({ + data: { + default_firewall_ids: { + linode: null, + nodebalancer: null, + public_interface: 1, + vpc_interface: null, + }, + }, + }); const { getByTestId, getByText } = render( - wrapWithTableBody( - , - { flags: { linodeInterfaces: { enabled: true } } } - ) + wrapWithTableBody(, { + flags: { linodeInterfaces: { enabled: true } }, + }) ); getByTestId('firewall-row-1'); getByText(firewall.label); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index c17b3dada1b..5e75626c431 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -1,4 +1,4 @@ -import { useAllLinodesQuery } from '@linode/queries'; +import { useAllLinodesQuery, useFirewallSettingsQuery } from '@linode/queries'; import { capitalize } from '@linode/utilities'; import React from 'react'; @@ -19,31 +19,24 @@ import type { Filter, Firewall, FirewallDeviceEntity, - FirewallSettings, Linode, } from '@linode/api-v4'; -export interface FirewallRowProps extends Firewall, ActionHandlers { - firewallSettings: FirewallSettings | undefined; -} +export interface FirewallRowProps extends Firewall, ActionHandlers {} export const FirewallRow = React.memo((props: FirewallRowProps) => { - const { - entities, - firewallSettings, - id, - label, - rules, - status, - ...actionHandlers - } = props; + const { entities, id, label, rules, status, ...actionHandlers } = props; + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled, + }); const tooltipText = firewallSettings && getDefaultFirewallDescription(id, firewallSettings); const isDefaultFirewall = !!tooltipText; - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const neededLinodeIdsForInterfaceDevices = entities .slice(0, 3) // only take the first three entities since we only show those entity links .filter((entity) => entity.type === 'interface') diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx index e40395d35df..83bab44f855 100644 --- a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -1,7 +1,7 @@ import { Chip, Tooltip } from '@linode/ui'; import React from 'react'; -import type { SxProps } from '@mui/material'; +import type { ChipProps } from '@linode/ui'; interface Props { chipProps?: Partial; From 7fa37a326a1e262abc9c4bffa06eca11e6dfcada Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 25 Mar 2025 14:29:10 -0400 Subject: [PATCH 13/19] feedback pt2 --- .../Devices/FirewallDeviceTable.tsx | 3 ++- .../Firewalls/FirewallDetail/index.tsx | 9 ++++--- .../FirewallLanding/FirewallRow.test.tsx | 2 +- .../Firewalls/FirewallLanding/FirewallRow.tsx | 5 ++-- .../src/features/Firewalls/shared.test.ts | 27 +++++++++++++++++-- .../manager/src/features/Firewalls/shared.ts | 16 ++++++++++- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx index 903bd4d3259..6f249c535c3 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx @@ -17,6 +17,7 @@ import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { getLinodeIdFromInterfaceDevice } from '../../shared'; import { formattedTypes } from './constants'; import { FirewallDeviceRow } from './FirewallDeviceRow'; @@ -69,7 +70,7 @@ export const FirewallDeviceTable = React.memo( const updatedDevices = devices.map((device) => { if (device.entity.type === 'interface') { - const linodeId = Number(device.entity.url.split('/')[4]); + const linodeId = getLinodeIdFromInterfaceDevice(device.entity); const associatedLinode = linodesWithInterfaces?.find( (linode) => linode.id === linodeId ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 9f2d03d5ef1..aeb4a8ea70f 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -32,7 +32,10 @@ import { FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME, getEntitiesThatFirewallIsDefaultFor, } from '../components/FirewallSelectOption.utils'; -import { checkIfUserCanModifyFirewall } from '../shared'; +import { + checkIfUserCanModifyFirewall, + getLinodeIdFromInterfaceDevice, +} from '../shared'; const FirewallRulesLanding = React.lazy(() => import('./Rules/FirewallRulesLanding').then((module) => ({ @@ -89,7 +92,7 @@ export const FirewallDetail = () => { isLinodeInterfacesEnabled && device.entity.type === 'interface' ) { - const linodeId = device.entity.url.split('/')[4]; + const linodeId = getLinodeIdFromInterfaceDevice(device.entity); if (!acc.seenLinodeIdsForInterfaces.has(linodeId)) { acc.linodeCount += 1; } @@ -100,7 +103,7 @@ export const FirewallDetail = () => { { linodeCount: 0, nodebalancerCount: 0, - seenLinodeIdsForInterfaces: new Set(), + seenLinodeIdsForInterfaces: new Set(), } ) || { linodeCount: 0, diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index 02f69236690..5ec4a411e72 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -66,7 +66,7 @@ describe('FirewallRow', () => { triggerEnableFirewall: mockTriggerEnableFirewall, }; - it.only('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { + it('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { queryMocks.useFirewallSettingsQuery.mockReturnValue({ data: { default_firewall_ids: { diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index 5e75626c431..3bd417f79be 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -12,6 +12,7 @@ import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { DefaultFirewallChip } from '../components/DefaultFirewallChip'; import { getDefaultFirewallDescription } from '../components/FirewallSelectOption.utils'; +import { getLinodeIdFromInterfaceDevice } from '../shared'; import { FirewallActionMenu } from './FirewallActionMenu'; import type { ActionHandlers } from './FirewallActionMenu'; @@ -41,7 +42,7 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { .slice(0, 3) // only take the first three entities since we only show those entity links .filter((entity) => entity.type === 'interface') .map((entity) => { - return { id: Number(entity.url.split('/')[4]) }; + return { id: getLinodeIdFromInterfaceDevice(entity) }; }); const filterForInterfaceDeviceLinodes: Filter = { @@ -175,7 +176,7 @@ export const getDeviceLinks = ( // TODO @Linode Interfaces - update interface links to interface details soon const entityId = entity.type === 'interface' - ? Number(entity.url.split('/')[4]) + ? getLinodeIdFromInterfaceDevice(entity) : entity.id; const entityLabel = entity.type === 'interface' diff --git a/packages/manager/src/features/Firewalls/shared.test.ts b/packages/manager/src/features/Firewalls/shared.test.ts index dfec1c0560f..092edb562c5 100644 --- a/packages/manager/src/features/Firewalls/shared.test.ts +++ b/packages/manager/src/features/Firewalls/shared.test.ts @@ -1,12 +1,16 @@ -import { FirewallRuleType } from '@linode/api-v4/lib/firewalls/types'; - import { allIPv4, allIPv6, generateAddressesLabel, + getLinodeIdFromInterfaceDevice, predefinedFirewallFromRule, } from './shared'; +import type { + FirewallDeviceEntityType, + FirewallRuleType, +} from '@linode/api-v4/lib/firewalls/types'; + const addresses = { ipv4: [allIPv4], ipv6: [allIPv6], @@ -144,3 +148,22 @@ describe('generateAddressLabel', () => { ); }); }); + +describe('getLinodeIdFromInterfaceDevice', () => { + it('returns the ID', () => { + const entity = { + id: 123, + label: null, + type: 'interface' as FirewallDeviceEntityType, + url: '/v4/linode/instances/123/interfaces/123', + }; + + expect(getLinodeIdFromInterfaceDevice(entity)).toEqual(123); + expect( + getLinodeIdFromInterfaceDevice({ + ...entity, + url: '/v4/linode/instances/456/interfaces/123', + }) + ).toEqual(456); + }); +}); diff --git a/packages/manager/src/features/Firewalls/shared.ts b/packages/manager/src/features/Firewalls/shared.ts index efdf5c7b857..7b80e847261 100644 --- a/packages/manager/src/features/Firewalls/shared.ts +++ b/packages/manager/src/features/Firewalls/shared.ts @@ -2,7 +2,7 @@ import { truncateAndJoinList } from '@linode/utilities'; import { capitalize } from '@linode/utilities'; import type { PORT_PRESETS } from './FirewallDetail/Rules/shared'; -import type { Grants, Profile } from '@linode/api-v4'; +import type { FirewallDeviceEntity, Grants, Profile } from '@linode/api-v4'; import type { Firewall, FirewallRuleProtocol, @@ -268,3 +268,17 @@ export const getFirewallDescription = (firewall: Firewall) => { ]; return description.join(', '); }; + +// TODO @Linode Interfaces - probably get rid of this once the API changes to FirewallDevice come in +/** + * Utility function to extract the Linode ID from firewall interface device entities. For Interface devices, + * the URL is "/v4/linode/instances/123/interfaces/123" + * + * Assumptions: the entity device being passed into this function always has type "interface". The URL is + * always in the above format. + */ +export const getLinodeIdFromInterfaceDevice = ( + entity: FirewallDeviceEntity +): number => { + return Number(entity.url.split('/')[4]); +}; From f269092e86820d96c529d65421e843ccc6324c6b Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 26 Mar 2025 16:31:02 -0400 Subject: [PATCH 14/19] fix test --- .../manager/src/features/Account/DefaultFirewalls.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Account/DefaultFirewalls.test.tsx b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx index 6043c5765c0..a1d4b9478d1 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.test.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx @@ -20,7 +20,9 @@ describe('NetworkInterfaces', () => { HttpResponse.json(makeResourcePage(firewallFactory.buildList(1))) ) ); - const { getByTestId, getByText } = renderWithTheme(); + const { getByTestId, getByText } = renderWithTheme(, { + flags: { linodeInterfaces: { enabled: true } }, + }); // Loading state should render expect(getByTestId(loadingTestId)).toBeInTheDocument(); From 3879ace53e8a42de6575a426c45f3256a198ac13 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 26 Mar 2025 17:01:40 -0400 Subject: [PATCH 15/19] create hook for shared logic with default firewall chips --- .../Firewalls/FirewallLanding/FirewallRow.tsx | 21 +++++----- .../components/DefaultFirewallChip.tsx | 12 +++++- .../Firewalls/components/FirewallSelect.tsx | 29 +++++-------- .../components/FirewallSelectOption.tsx | 24 +++++------ .../useDefaultFirewallChipInformation.ts | 42 +++++++++++++++++++ 5 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 packages/manager/src/hooks/useDefaultFirewallChipInformation.ts diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index 3bd417f79be..456fa9bbe13 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -1,4 +1,4 @@ -import { useAllLinodesQuery, useFirewallSettingsQuery } from '@linode/queries'; +import { useAllLinodesQuery } from '@linode/queries'; import { capitalize } from '@linode/utilities'; import React from 'react'; @@ -8,10 +8,10 @@ import { Skeleton } from 'src/components/Skeleton'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { DefaultFirewallChip } from '../components/DefaultFirewallChip'; -import { getDefaultFirewallDescription } from '../components/FirewallSelectOption.utils'; import { getLinodeIdFromInterfaceDevice } from '../shared'; import { FirewallActionMenu } from './FirewallActionMenu'; @@ -30,13 +30,11 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled, - }); - - const tooltipText = - firewallSettings && getDefaultFirewallDescription(id, firewallSettings); - const isDefaultFirewall = !!tooltipText; + const { + isDefault, + numEntitiesIsDefaultFor, + tooltipText, + } = useDefaultFirewallChipInformation(id); const neededLinodeIdsForInterfaceDevices = entities .slice(0, 3) // only take the first three entities since we only show those entity links @@ -65,9 +63,10 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {label} - {isLinodeInterfacesEnabled && isDefaultFirewall && ( + {isLinodeInterfacesEnabled && isDefault && ( )} @@ -94,7 +93,7 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { firewallID={id} firewallLabel={label} firewallStatus={status} - isDefaultFirewall={isDefaultFirewall} + isDefaultFirewall={isDefault} {...actionHandlers} /> diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx index 83bab44f855..7c5512e9ba5 100644 --- a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -5,16 +5,24 @@ import type { ChipProps } from '@linode/ui'; interface Props { chipProps?: Partial; + numEntitiesIsDefaultFor: number; tooltipText: React.ReactNode; } export const DefaultFirewallChip = (props: Props) => { + const { chipProps, numEntitiesIsDefaultFor, tooltipText } = props; return ( - + 1 ? ` (${numEntitiesIsDefaultFor})` : '' + }`} + size="small" + {...chipProps} + /> ); }; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx index ddd6e66190c..bd12be14272 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx @@ -1,15 +1,11 @@ -import { - useAllFirewallsQuery, - useFirewallSettingsQuery, -} from '@linode/queries'; +import { useAllFirewallsQuery } from '@linode/queries'; import { Autocomplete } from '@linode/ui'; import React, { useMemo } from 'react'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; import { DefaultFirewallChip } from './DefaultFirewallChip'; import { FirewallSelectOption } from './FirewallSelectOption'; -import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; import type { Firewall } from '@linode/api-v4'; import type { EnhancedAutocompleteProps } from '@linode/ui'; @@ -53,19 +49,13 @@ export const FirewallSelect = ( ) => { const { errorText, hideDefaultChips, loading, value, ...rest } = props; - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled && !hideDefaultChips, - }); - - const defaultDescription = - firewallSettings && - value && - getDefaultFirewallDescription(value, firewallSettings); - const isDefault = !!defaultDescription; + const { + isDefault, + numEntitiesIsDefaultFor, + tooltipText, + } = useDefaultFirewallChipInformation(value, hideDefaultChips); const selectedFirewall = useMemo( () => firewalls?.find((firewall) => firewall.id === value) ?? null, @@ -86,7 +76,10 @@ export const FirewallSelect = ( textFieldProps={{ InputProps: { endAdornment: isDefault && !hideDefaultChips && ( - + ), }, }} diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx index 4ebdf5f236a..5b5aa70e28e 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx @@ -1,11 +1,9 @@ -import { useFirewallSettingsQuery } from '@linode/queries'; import { Box, SelectedIcon, Stack } from '@linode/ui'; import React from 'react'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; import { DefaultFirewallChip } from './DefaultFirewallChip'; -import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; import type { Firewall } from '@linode/api-v4'; import type { AutocompleteRenderOptionState } from '@mui/material'; @@ -24,16 +22,11 @@ interface Props { export const FirewallSelectOption = (props: Props) => { const { hideDefaultChip, listItemProps, option, state } = props; - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled && !hideDefaultChip, - }); - - const defaultDescription = - firewallSettings && - getDefaultFirewallDescription(option.id, firewallSettings); - - const isDefault = !!defaultDescription; + const { + isDefault, + numEntitiesIsDefaultFor, + tooltipText, + } = useDefaultFirewallChipInformation(option.id); return (
  • @@ -41,7 +34,10 @@ export const FirewallSelectOption = (props: Props) => { {option.label} {isDefault && !hideDefaultChip && ( - + )} {state.selected && } diff --git a/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts new file mode 100644 index 00000000000..317b2f9ca5b --- /dev/null +++ b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts @@ -0,0 +1,42 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; + +import { + getDefaultFirewallDescription, + getEntitiesThatFirewallIsDefaultFor, +} from 'src/features/Firewalls/components/FirewallSelectOption.utils'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +/** + * Hook to obtain the information regarding Default Firewalls which can be used for + * for Default Firewall chips. + * + * Determines if the given firewall (via ID) is a default firewall + * Determines the tooltip and chip text to be used for the chip + */ +export const useDefaultFirewallChipInformation = ( + firewallId: null | number | undefined, + hideDefaultChips?: boolean +) => { + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled && !hideDefaultChips, + }); + + const tooltipText = + firewallId && + firewallSettings && + getDefaultFirewallDescription(firewallId, firewallSettings); + const numEntitiesIsDefaultFor = + firewallSettings && firewallId + ? getEntitiesThatFirewallIsDefaultFor(firewallId, firewallSettings).length + : 0; + + const isDefault = !!tooltipText; + + return { + isDefault, + numEntitiesIsDefaultFor, + tooltipText, + }; +}; From 8d2d3cc992e5dad82c3545ebaa320b00e5db8b05 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 27 Mar 2025 11:09:31 -0400 Subject: [PATCH 16/19] update firewall landing links --- .../FirewallLanding/FirewallActionMenu.tsx | 2 +- .../Firewalls/FirewallLanding/FirewallRow.tsx | 28 ++++++++++--------- .../VPCInterfaceDetailsContent.tsx | 6 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx index 699b7e23c2a..9d16b6f055e 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { checkIfUserCanModifyFirewall } from '../shared'; import { @@ -15,7 +16,6 @@ import { import type { FirewallStatus } from '@linode/api-v4/lib/firewalls'; import type { Theme } from '@mui/material/styles'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; export interface ActionHandlers { [index: string]: any; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index 456fa9bbe13..ff6d145eeec 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -172,18 +172,20 @@ export const getDeviceLinks = ( <> {firstThree.map((entity, idx) => { // TODO @Linode Interfaces - switch to parent entity when endpoints are updated - // TODO @Linode Interfaces - update interface links to interface details soon - const entityId = - entity.type === 'interface' - ? getLinodeIdFromInterfaceDevice(entity) - : entity.id; - const entityLabel = - entity.type === 'interface' - ? linodesWithInterfaceDevices?.find( - (linode) => linode.id === entityId - )?.label ?? entity.label - : entity.label; - const entityLink = entity.type === 'interface' ? 'linode' : entity.type; + const isInterfaceDevice = entity.type === 'interface'; + const parentEntityId = isInterfaceDevice // for Linode Interface devices, we also need the Linode ID + ? getLinodeIdFromInterfaceDevice(entity) + : entity.id; + const entityLabel = isInterfaceDevice + ? linodesWithInterfaceDevices?.find( + (linode) => linode.id === parentEntityId + )?.label ?? entity.label + : entity.label; + const entityLink = isInterfaceDevice + ? `/linodes/${parentEntityId}/networking/interfaces/${entity.id}` + : `/${entity.type}s/${entity.id}/${ + entity.type === 'linode' ? 'networking' : 'summary' + }`; return ( @@ -191,7 +193,7 @@ export const getDeviceLinks = ( {entityLabel} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx index e92fb1b5099..9bed4dcac59 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx @@ -17,18 +17,16 @@ export const VPCInterfaceDetailsContent = (props: VPCInterfaceData) => { <> {ipv4.addresses.map((address) => address.nat_1_1_address ? ( - <> + - + ) : ( Date: Thu, 27 Mar 2025 15:24:34 -0400 Subject: [PATCH 17/19] Update packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> --- .../Firewalls/FirewallLanding/FirewallRow.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index ff6d145eeec..b433f99a5b1 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -173,19 +173,17 @@ export const getDeviceLinks = ( {firstThree.map((entity, idx) => { // TODO @Linode Interfaces - switch to parent entity when endpoints are updated const isInterfaceDevice = entity.type === 'interface'; - const parentEntityId = isInterfaceDevice // for Linode Interface devices, we also need the Linode ID - ? getLinodeIdFromInterfaceDevice(entity) - : entity.id; - const entityLabel = isInterfaceDevice - ? linodesWithInterfaceDevices?.find( - (linode) => linode.id === parentEntityId - )?.label ?? entity.label - : entity.label; - const entityLink = isInterfaceDevice - ? `/linodes/${parentEntityId}/networking/interfaces/${entity.id}` - : `/${entity.type}s/${entity.id}/${ - entity.type === 'linode' ? 'networking' : 'summary' - }`; + let entityLabel = entity.label; + let entityLink = + `/${entity.type}s/${entity.id}/${entity.type === 'linode' ? 'networking' : 'summary'}`; + + if (isInterfaceDevice) { + const parentEntityId = getLinodeIdFromInterfaceDevice(entity); + entityLabel = linodesWithInterfaceDevices?.find( + (linode) => linode.id === parentEntityId + )?.label ?? entity.label; + entityLink = `/linodes/${parentEntityId}/networking/interfaces/${entity.id}` + } return ( From 2122b66890c5f65bc76ae8f266f5ff791fdcbdc5 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 27 Mar 2025 15:41:12 -0400 Subject: [PATCH 18/19] update naming feedback --- .../Firewalls/FirewallDetail/index.tsx | 12 ++++++------ .../Firewalls/FirewallLanding/FirewallRow.tsx | 18 ++++++++++-------- .../components/DefaultFirewallChip.tsx | 6 +++--- .../Firewalls/components/FirewallSelect.tsx | 4 ++-- .../components/FirewallSelectOption.tsx | 4 ++-- .../FirewallSelectOption.utils.test.tsx | 14 +++++--------- .../components/FirewallSelectOption.utils.tsx | 6 +++--- .../hooks/useDefaultFirewallChipInformation.ts | 8 ++++---- 8 files changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index aeb4a8ea70f..6918ec6f927 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -30,7 +30,7 @@ import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME, - getEntitiesThatFirewallIsDefaultFor, + getFirewallDefaultEntities, } from '../components/FirewallSelectOption.utils'; import { checkIfUserCanModifyFirewall, @@ -70,9 +70,9 @@ export const FirewallDetail = () => { enabled: isLinodeInterfacesEnabled, }); - const defaultForEntities = + const defaultEntities = firewallSettings && - getEntitiesThatFirewallIsDefaultFor(firewallId, firewallSettings); + getFirewallDefaultEntities(firewallId, firewallSettings); const userCanModifyFirewall = checkIfUserCanModifyFirewall( firewallId, @@ -191,8 +191,8 @@ export const FirewallDetail = () => { /> )} {isLinodeInterfacesEnabled && - defaultForEntities && - defaultForEntities.length > 0 && ( + defaultEntities && + defaultEntities.length > 0 && ( ({ alignItems: 'center', @@ -209,7 +209,7 @@ export const FirewallDetail = () => { Default - {defaultForEntities.map((defaultEntity) => ( + {defaultEntities.map((defaultEntity) => ( { const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { + defaultNumEntities, isDefault, - numEntitiesIsDefaultFor, tooltipText, } = useDefaultFirewallChipInformation(id); @@ -66,7 +66,7 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {isLinodeInterfacesEnabled && isDefault && ( )} @@ -174,15 +174,17 @@ export const getDeviceLinks = ( // TODO @Linode Interfaces - switch to parent entity when endpoints are updated const isInterfaceDevice = entity.type === 'interface'; let entityLabel = entity.label; - let entityLink = - `/${entity.type}s/${entity.id}/${entity.type === 'linode' ? 'networking' : 'summary'}`; + let entityLink = `/${entity.type}s/${entity.id}/${ + entity.type === 'linode' ? 'networking' : 'summary' + }`; if (isInterfaceDevice) { const parentEntityId = getLinodeIdFromInterfaceDevice(entity); - entityLabel = linodesWithInterfaceDevices?.find( - (linode) => linode.id === parentEntityId - )?.label ?? entity.label; - entityLink = `/linodes/${parentEntityId}/networking/interfaces/${entity.id}` + entityLabel = + linodesWithInterfaceDevices?.find( + (linode) => linode.id === parentEntityId + )?.label ?? entity.label; + entityLink = `/linodes/${parentEntityId}/networking/interfaces/${entity.id}`; } return ( diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx index 7c5512e9ba5..60f9fe8834b 100644 --- a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -5,12 +5,12 @@ import type { ChipProps } from '@linode/ui'; interface Props { chipProps?: Partial; - numEntitiesIsDefaultFor: number; + defaultNumEntities: number; tooltipText: React.ReactNode; } export const DefaultFirewallChip = (props: Props) => { - const { chipProps, numEntitiesIsDefaultFor, tooltipText } = props; + const { chipProps, defaultNumEntities, tooltipText } = props; return ( { > 1 ? ` (${numEntitiesIsDefaultFor})` : '' + defaultNumEntities > 1 ? ` (${defaultNumEntities})` : '' }`} size="small" {...chipProps} diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx index bd12be14272..cf86f651a07 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx @@ -52,8 +52,8 @@ export const FirewallSelect = ( const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); const { + defaultNumEntities, isDefault, - numEntitiesIsDefaultFor, tooltipText, } = useDefaultFirewallChipInformation(value, hideDefaultChips); @@ -77,7 +77,7 @@ export const FirewallSelect = ( InputProps: { endAdornment: isDefault && !hideDefaultChips && ( ), diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx index 5b5aa70e28e..92fdf56934b 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx @@ -23,8 +23,8 @@ export const FirewallSelectOption = (props: Props) => { const { hideDefaultChip, listItemProps, option, state } = props; const { + defaultNumEntities, isDefault, - numEntitiesIsDefaultFor, tooltipText, } = useDefaultFirewallChipInformation(option.id); @@ -35,7 +35,7 @@ export const FirewallSelectOption = (props: Props) => { {isDefault && !hideDefaultChip && ( )} diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx index 0d2eef0bb6e..dd7c9060eab 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx @@ -3,10 +3,10 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { getDefaultFirewallDescription, - getEntitiesThatFirewallIsDefaultFor, + getFirewallDefaultEntities, } from './FirewallSelectOption.utils'; -describe('getEntitiesThatFirewallIsDefaultFor', () => { +describe('getFirewallDefaultEntities', () => { it('returns entities that a firewall is a default for', () => { const firewallSettings = firewallSettingsFactory.build({ default_firewall_ids: { @@ -17,7 +17,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(4, firewallSettings)).toEqual([ + expect(getFirewallDefaultEntities(4, firewallSettings)).toEqual([ 'linode', 'nodebalancer', 'public_interface', @@ -34,9 +34,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( - [] - ); + expect(getFirewallDefaultEntities(1, firewallSettings)).toEqual([]); }); it('returns an empty array if the user has no default firewalls set', () => { @@ -49,9 +47,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( - [] - ); + expect(getFirewallDefaultEntities(1, firewallSettings)).toEqual([]); }); }); diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx index e0758e030af..9f991daaf3e 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx @@ -19,7 +19,7 @@ export const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< }; /** - * getEntitiesThatFirewallIsDefaultFor + * getFirewallDefaultEntities * * @param firewallId The ID of the Firewall * @param firewallSettings The account FirewallSettings from the API @@ -27,7 +27,7 @@ export const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< * @returns An array of entities that this Firewall is a default for. * @example ['nodebalancer', 'vpc_interface'] */ -export function getEntitiesThatFirewallIsDefaultFor( +export function getFirewallDefaultEntities( firewallId: number, firewallSettings: FirewallSettings ) { @@ -56,7 +56,7 @@ export function getDefaultFirewallDescription( firewallId: number, firewallSettings: FirewallSettings ) { - const entitiesThatFirewallIsDefaultFor = getEntitiesThatFirewallIsDefaultFor( + const entitiesThatFirewallIsDefaultFor = getFirewallDefaultEntities( firewallId, firewallSettings ); diff --git a/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts index 317b2f9ca5b..77e5e9c3ce5 100644 --- a/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts +++ b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts @@ -2,7 +2,7 @@ import { useFirewallSettingsQuery } from '@linode/queries'; import { getDefaultFirewallDescription, - getEntitiesThatFirewallIsDefaultFor, + getFirewallDefaultEntities, } from 'src/features/Firewalls/components/FirewallSelectOption.utils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; @@ -27,16 +27,16 @@ export const useDefaultFirewallChipInformation = ( firewallId && firewallSettings && getDefaultFirewallDescription(firewallId, firewallSettings); - const numEntitiesIsDefaultFor = + const defaultNumEntities = firewallSettings && firewallId - ? getEntitiesThatFirewallIsDefaultFor(firewallId, firewallSettings).length + ? getFirewallDefaultEntities(firewallId, firewallSettings).length : 0; const isDefault = !!tooltipText; return { + defaultNumEntities, isDefault, - numEntitiesIsDefaultFor, tooltipText, }; }; From c604c78e7b966e85636991beacc60d9c18e2f277 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 27 Mar 2025 16:17:23 -0400 Subject: [PATCH 19/19] update spacing - ux feedback --- .../manager/src/features/Firewalls/FirewallDetail/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 6918ec6f927..5344eaaf0c4 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -206,7 +206,9 @@ export const FirewallDetail = () => { rowGap: 1, })} > - + ({ marginRight: theme.spacingFunction(8) })} + > Default {defaultEntities.map((defaultEntity) => (