From 3c52d87ba077c2ffea431abd9a5efc8d837ad73e Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 8 Nov 2023 15:41:05 -0500 Subject: [PATCH 01/17] disable linode entity detail ip address and access inputs for vpc-only linodes --- .../components/CopyTooltip/CopyTooltip.tsx | 52 ++++++++++++++----- .../features/Linodes/LinodeEntityDetail.tsx | 47 +++++++++++++++-- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx index f1e747d0ab4..7fdb0d045da 100644 --- a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx +++ b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx @@ -28,6 +28,11 @@ export interface CopyTooltipProps { * The text to be copied to the clipboard. */ text: string; + /** + * If true, the copy button will be disabled and there will be no tooltip. + * @default false + */ + disabled?: boolean; } /** @@ -38,7 +43,14 @@ export interface CopyTooltipProps { export const CopyTooltip = (props: CopyTooltipProps) => { const [copied, setCopied] = React.useState(false); - const { className, copyableText, onClickCallback, placement, text } = props; + const { + className, + copyableText, + onClickCallback, + placement, + text, + disabled, + } = props; const handleIconClick = () => { setCopied(true); @@ -49,6 +61,24 @@ export const CopyTooltip = (props: CopyTooltipProps) => { } }; + const CopyButton = ( + + {copyableText ? text : } + + ); + + if (disabled) { + return CopyButton; + } + return ( { placement={placement ?? 'top'} title={copied ? 'Copied!' : 'Copy'} > - - {copyableText ? text : } - + {CopyButton} ); }; -const StyledCopyTooltipButton = styled('button', { - label: 'StyledCopyTooltipButton', +const StyledCopyButton = styled('button', { + label: 'StyledCopyButton', shouldForwardProp: omittedProps(['copyableText', 'text']), })>(({ theme, ...props }) => ({ '& svg': { @@ -102,4 +122,8 @@ const StyledCopyTooltipButton = styled('button', { font: 'inherit', padding: 0, }), + ...(props.disabled && { + cursor: 'default', + color: theme.color.disabledText, + }), })); diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index fdb1c367f1a..ae2c0fa853b 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -38,6 +38,7 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { formatDate } from 'src/utilities/formatDate'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { pluralize } from 'src/utilities/pluralize'; +import { TooltipIcon } from 'src/components/TooltipIcon'; import { StyledBodyGrid, @@ -400,6 +401,10 @@ export const Body = React.memo((props: BodyProps) => { return interfaceWithVPC; }); + // A VPC-only Linode is a Linode that has at least one config interface with primary set to true and purpose vpc and no ipv4.nat_1_1 value + const isVPCOnlyLinode = Boolean( + _configInterfaceWithVPC?.primary && !_configInterfaceWithVPC.ipv4?.nat_1_1 + ); const numIPAddresses = ipv4.length + (ipv6 ? 1 : 0); const firstAddress = ipv4[0]; @@ -465,7 +470,8 @@ export const Body = React.memo((props: BodyProps) => { }} gridProps={{ md: 5 }} rows={[{ text: firstAddress }, { text: secondAddress }]} - title={`IP Address${numIPAddresses > 1 ? 'es' : ''}`} + title={`Public IP Address${numIPAddresses > 1 ? 'es' : ''}`} + isVPCOnlyLinode={isVPCOnlyLinode} /> { }} gridProps={{ md: 7 }} title="Access" + isVPCOnlyLinode={isVPCOnlyLinode} /> @@ -564,8 +571,14 @@ interface AccessTableProps { rows: AccessTableRow[]; sx?: SxProps; title: string; + isVPCOnlyLinode: boolean; } +const sxTooltipIcon = { + padding: '0', + paddingLeft: '4px', +}; + export const AccessTable = React.memo((props: AccessTableProps) => { return ( { sx={props.sx} {...props.gridProps} > - {props.title} + + {props.title}{' '} + {props.isVPCOnlyLinode && props.title.includes('Public IP Address') && ( + + The Public IP Addresses have been unassigned from the + configuration profile.{' '} + + Learn more + + . + + } + interactive + /> + )} + @@ -590,9 +622,16 @@ export const AccessTable = React.memo((props: AccessTableProps) => { ) : null} - + - + ) : null; From 0ec900f91ffd59f806ac21b27d7a90ec4667be86 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 8 Nov 2023 15:51:19 -0500 Subject: [PATCH 02/17] move AccessTable to its own file --- .../src/features/Linodes/AccessTable.tsx | 104 +++++++++++++++++ .../features/Linodes/LinodeEntityDetail.tsx | 109 +----------------- 2 files changed, 108 insertions(+), 105 deletions(-) create mode 100644 packages/manager/src/features/Linodes/AccessTable.tsx diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx new file mode 100644 index 00000000000..810ef23f57f --- /dev/null +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -0,0 +1,104 @@ +import Grid, { Grid2Props } from '@mui/material/Unstable_Grid2'; +import { SxProps } from '@mui/system'; +import * as React from 'react'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { Link } from 'src/components/Link'; +import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { Typography } from 'src/components/Typography'; + +import { + StyledColumnLabelGrid, + StyledCopyTooltip, + StyledGradientDiv, + StyledTable, + StyledTableCell, + StyledTableGrid, + StyledTableRow, +} from './LinodeEntityDetail.styles'; + +interface AccessTableRow { + heading?: string; + text: null | string; +} + +interface AccessTableProps { + footer?: JSX.Element; + gridProps?: Grid2Props; + isVPCOnlyLinode: boolean; + rows: AccessTableRow[]; + sx?: SxProps; + title: string; +} + +const sxTooltipIcon = { + padding: '0', + paddingLeft: '4px', +}; + +export const AccessTable = React.memo((props: AccessTableProps) => { + return ( + + + {props.title}{' '} + {props.isVPCOnlyLinode && props.title.includes('Public IP Address') && ( + + The Public IP Addresses have been unassigned from the + configuration profile.{' '} + + Learn more + + . + + } + interactive + status="help" + sxTooltipIcon={sxTooltipIcon} + /> + )} + + + + + {props.rows.map((thisRow) => { + return thisRow.text ? ( + + {thisRow.heading ? ( + + {thisRow.heading} + + ) : null} + + + + + + + + ) : null; + })} + + + {props.footer ? {props.footer} : null} + + + ); +}); diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index ae2c0fa853b..69353abfb60 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -1,22 +1,19 @@ import { LinodeBackups } from '@linode/api-v4/lib/linodes'; -import Grid, { Grid2Props } from '@mui/material/Unstable_Grid2'; +import Grid from '@mui/material/Unstable_Grid2'; import { useTheme } from '@mui/material/styles'; -import { SxProps } from '@mui/system'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { HashLink } from 'react-router-hash-link'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import EntityDetail from 'src/components/EntityDetail'; import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; import { Hidden } from 'src/components/Hidden'; import { Link } from 'src/components/Link'; -import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography, TypographyProps } from 'src/components/Typography'; +import { AccessTable } from 'src/features/Linodes/AccessTable'; import { LinodeActionMenu } from 'src/features/Linodes/LinodesLanding/LinodeActionMenu'; import { ProgressDisplay } from 'src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow'; import { lishLaunch } from 'src/features/Lish/lishUtils'; @@ -38,24 +35,17 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { formatDate } from 'src/utilities/formatDate'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { pluralize } from 'src/utilities/pluralize'; -import { TooltipIcon } from 'src/components/TooltipIcon'; import { StyledBodyGrid, StyledBox, StyledChip, StyledColumnLabelGrid, - StyledCopyTooltip, - StyledGradientDiv, StyledLabelBox, StyledLink, StyledListItem, StyledRightColumnGrid, StyledSummaryGrid, - StyledTable, - StyledTableCell, - StyledTableGrid, - StyledTableRow, StyledVPCBox, sxLastListItem, sxListItemFirstChild, @@ -469,11 +459,10 @@ export const Body = React.memo((props: BodyProps) => { }, }} gridProps={{ md: 5 }} + isVPCOnlyLinode={isVPCOnlyLinode} rows={[{ text: firstAddress }, { text: secondAddress }]} title={`Public IP Address${numIPAddresses > 1 ? 'es' : ''}`} - isVPCOnlyLinode={isVPCOnlyLinode} /> - { }, }} gridProps={{ md: 7 }} - title="Access" isVPCOnlyLinode={isVPCOnlyLinode} + title="Access" /> @@ -554,96 +543,6 @@ export const Body = React.memo((props: BodyProps) => { ); }); -// ============================================================================= -// AccessTable -// ============================================================================= -// @todo: Maybe move this component somewhere to its own file? Could potentially -// be used elsewhere. - -interface AccessTableRow { - heading?: string; - text: null | string; -} - -interface AccessTableProps { - footer?: JSX.Element; - gridProps?: Grid2Props; - rows: AccessTableRow[]; - sx?: SxProps; - title: string; - isVPCOnlyLinode: boolean; -} - -const sxTooltipIcon = { - padding: '0', - paddingLeft: '4px', -}; - -export const AccessTable = React.memo((props: AccessTableProps) => { - return ( - - - {props.title}{' '} - {props.isVPCOnlyLinode && props.title.includes('Public IP Address') && ( - - The Public IP Addresses have been unassigned from the - configuration profile.{' '} - - Learn more - - . - - } - interactive - /> - )} - - - - - {props.rows.map((thisRow) => { - return thisRow.text ? ( - - {thisRow.heading ? ( - - {thisRow.heading} - - ) : null} - - - - - - - - ) : null; - })} - - - {props.footer ? {props.footer} : null} - - - ); -}); - // ============================================================================= // Footer // ============================================================================= From d44a421064ef15e9b65d8a2a92ea99cf51907b21 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 8 Nov 2023 16:58:35 -0500 Subject: [PATCH 03/17] add unit test for AccessTable --- .../components/CopyTooltip/CopyTooltip.tsx | 14 ++--- .../src/features/Linodes/AccessTable.test.tsx | 52 +++++++++++++++++++ .../src/features/Linodes/AccessTable.tsx | 6 ++- 3 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 packages/manager/src/features/Linodes/AccessTable.test.tsx diff --git a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx index 7fdb0d045da..777a1cda25e 100644 --- a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx +++ b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx @@ -16,6 +16,11 @@ export interface CopyTooltipProps { * @default false */ copyableText?: boolean; + /** + * If true, the copy button will be disabled and there will be no tooltip. + * @default false + */ + disabled?: boolean; /** * Callback to be executed when the icon is clicked. */ @@ -28,11 +33,6 @@ export interface CopyTooltipProps { * The text to be copied to the clipboard. */ text: string; - /** - * If true, the copy button will be disabled and there will be no tooltip. - * @default false - */ - disabled?: boolean; } /** @@ -46,10 +46,10 @@ export const CopyTooltip = (props: CopyTooltipProps) => { const { className, copyableText, + disabled, onClickCallback, placement, text, - disabled, } = props; const handleIconClick = () => { @@ -123,7 +123,7 @@ const StyledCopyButton = styled('button', { padding: 0, }), ...(props.disabled && { - cursor: 'default', color: theme.color.disabledText, + cursor: 'default', }), })); diff --git a/packages/manager/src/features/Linodes/AccessTable.test.tsx b/packages/manager/src/features/Linodes/AccessTable.test.tsx new file mode 100644 index 00000000000..260b93c6279 --- /dev/null +++ b/packages/manager/src/features/Linodes/AccessTable.test.tsx @@ -0,0 +1,52 @@ +import { fireEvent } from '@testing-library/react'; +import * as React from 'react'; + +import { linodeFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { AccessTable, PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from './AccessTable'; + +const linode = linodeFactory.build(); + +describe('AccessTable', () => { + it('should disable copy button and display help icon tooltip if isVPCOnlyLinode is true', async () => { + const { findByRole, getAllByRole } = renderWithTheme( + + ); + + const buttons = getAllByRole('button'); + const helpIconButton = buttons[0]; + const copyButtons = buttons.slice(1); + + fireEvent.mouseEnter(helpIconButton); + + const publicIpsUnassignedTooltip = await findByRole(/tooltip/); + expect(publicIpsUnassignedTooltip).toContainHTML( + PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT + ); + + copyButtons.forEach((copyButton) => { + expect(copyButton).toBeDisabled(); + }); + }); + + it('should not disable copy button if isVPCOnlyLinode is false', async () => { + const { getAllByRole } = renderWithTheme( + + ); + + const copyButtons = getAllByRole('button'); + + copyButtons.forEach((copyButton) => { + expect(copyButton).not.toBeDisabled(); + }); + }); +}); diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx index 810ef23f57f..245938e8bbc 100644 --- a/packages/manager/src/features/Linodes/AccessTable.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -38,6 +38,9 @@ const sxTooltipIcon = { paddingLeft: '4px', }; +export const PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT = `The Public IP Addresses have been unassigned from the +configuration profile.`; + export const AccessTable = React.memo((props: AccessTableProps) => { return ( { - The Public IP Addresses have been unassigned from the - configuration profile.{' '} + {PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT}{' '} Learn more From 243b66a40ee6e709332ab9d5e7361470ea0c828a Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 8 Nov 2023 17:43:21 -0500 Subject: [PATCH 04/17] disable public ipv4 row in linode network ip addresses table --- .../src/features/Linodes/AccessTable.tsx | 5 +- .../LinodeNetworking/LinodeIPAddresses.tsx | 66 ++++++++++++++++++- .../LinodeNetworkingActionMenu.tsx | 17 +++-- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx index 245938e8bbc..8ed1de0315a 100644 --- a/packages/manager/src/features/Linodes/AccessTable.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -75,7 +75,10 @@ export const AccessTable = React.memo((props: AccessTableProps) => { {props.rows.map((thisRow) => { return thisRow.text ? ( - + {thisRow.heading ? ( {thisRow.heading} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index f7be811bbca..dcb7fcd6d3c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -21,12 +21,17 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { Typography } from 'src/components/Typography'; +import { useAccountManagement } from 'src/hooks/useAccountManagement'; +import { useFlags } from 'src/hooks/useFlags'; +import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useAllIPsQuery, useLinodeIPsQuery, } from 'src/queries/linodes/networking'; import { useGrants } from 'src/queries/profile'; +import { useVPCsQuery } from 'src/queries/vpcs'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { getPermissionsForLinode } from 'src/utilities/linodes'; import { AddIPDrawer } from './AddIPDrawer'; @@ -48,6 +53,8 @@ import { ViewRDNSDrawer } from './ViewRDNSDrawer'; import { ViewRangeDrawer } from './ViewRangeDrawer'; import { IPTypes } from './types'; +import type { Interface } from '@linode/api-v4/lib/linodes/types'; + const useStyles = makeStyles()( (theme: Theme, _params, classes) => ({ copy: { @@ -102,6 +109,51 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const [isViewRDNSDialogOpen, setIsViewRDNSDialogOpen] = React.useState(false); const [isAddDrawerOpen, setIsAddDrawerOpen] = React.useState(false); + const flags = useFlags(); + const { account } = useAccountManagement(); + + const displayVPCSection = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + + const { data: vpcData } = useVPCsQuery({}, {}, displayVPCSection); + const vpcsList = vpcData?.data ?? []; + + const vpcLinodeIsAssignedTo = vpcsList.find((vpc) => { + const subnets = vpc.subnets; + + return Boolean( + subnets.find((subnet) => + subnet.linodes.some((linodeInfo) => linodeInfo.id === linodeID) + ) + ); + }); + + const { data: configs } = useAllLinodeConfigsQuery(linodeID); + let _configInterfaceWithVPC: Interface | undefined; + + // eslint-disable-next-line no-unused-expressions + configs?.find((config) => { + const interfaces = config.interfaces; + + const interfaceWithVPC = interfaces.find( + (_interface) => _interface.vpc_id === vpcLinodeIsAssignedTo?.id + ); + + if (interfaceWithVPC) { + _configInterfaceWithVPC = interfaceWithVPC; + } + + return interfaceWithVPC; + }); + + // A VPC-only Linode is a Linode that has at least one config interface with primary set to true and purpose vpc and no ipv4.nat_1_1 value + const isVPCOnlyLinode = Boolean( + _configInterfaceWithVPC?.primary && !_configInterfaceWithVPC.ipv4?.nat_1_1 + ); + const openRemoveIPDialog = (ip: IPAddress) => { setSelectedIP(ip); setIsDeleteIPDialogOpen(true); @@ -134,19 +186,25 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const isOnlyPublicIP = ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; + const disabled = isVPCOnlyLinode && ipDisplay.type === 'IPv4 – Public'; + return ( - - + + {!isVPCOnlyLinode || + (ipDisplay.type !== 'IPv4 – Public' && ( + + ))} {type} @@ -168,6 +226,7 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { {_ip ? ( { /> ) : _range ? ( { const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); const { + disabled, ipAddress, ipType, isOnlyPublicIP, @@ -55,13 +58,15 @@ export const LinodeNetworkingActionMenu = (props: Props) => { const actions = [ onRemove && ipAddress && !is116Range && deletableIPTypes.includes(ipType) ? { - disabled: readOnly || isOnlyPublicIP, + disabled: readOnly || isOnlyPublicIP || disabled, onClick: () => { onRemove(ipAddress); }, title: 'Delete', tooltip: readOnly ? readOnlyTooltip + : disabled + ? PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT : isOnlyPublicIP ? isOnlyPublicIPTooltip : undefined, @@ -69,12 +74,16 @@ export const LinodeNetworkingActionMenu = (props: Props) => { : null, onEdit && ipAddress && showEdit ? { - disabled: readOnly, + disabled: readOnly || disabled, onClick: () => { onEdit(ipAddress); }, title: 'Edit RDNS', - tooltip: readOnly ? readOnlyTooltip : undefined, + tooltip: readOnly + ? readOnlyTooltip + : disabled + ? PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT + : undefined, } : null, ].filter(Boolean) as Action[]; From a872099617cab9915ac614f10d6ca58e6d375baf Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 9 Nov 2023 16:09:52 -0500 Subject: [PATCH 05/17] move shared logic to custom hook --- .../features/Linodes/LinodeEntityDetail.tsx | 65 +++---------------- .../LinodeNetworking/LinodeIPAddresses.tsx | 55 +--------------- .../src/hooks/useVPCConfigInterface.ts | 61 +++++++++++++++++ 3 files changed, 73 insertions(+), 108 deletions(-) create mode 100644 packages/manager/src/hooks/useVPCConfigInterface.ts diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index 69353abfb60..ae8f7d2b341 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -18,18 +18,14 @@ import { LinodeActionMenu } from 'src/features/Linodes/LinodesLanding/LinodeActi import { ProgressDisplay } from 'src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow'; import { lishLaunch } from 'src/features/Lish/lishUtils'; import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; -import { useAccountManagement } from 'src/hooks/useAccountManagement'; -import { useFlags } from 'src/hooks/useFlags'; +import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useAllImagesQuery } from 'src/queries/images'; -import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useLinodeUpdateMutation } from 'src/queries/linodes/linodes'; import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useTypeQuery } from 'src/queries/types'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; -import { useVPCsQuery } from 'src/queries/vpcs'; import { useRecentEventForLinode } from 'src/store/selectors/recentEventForLinode'; -import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendLinodeActionMenuItemEvent } from 'src/utilities/analytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { formatDate } from 'src/utilities/formatDate'; @@ -59,11 +55,7 @@ import { isEventWithSecondaryLinodeStatus, } from './transitions'; -import type { - Interface, - Linode, - LinodeType, -} from '@linode/api-v4/lib/linodes/types'; +import type { Linode, LinodeType } from '@linode/api-v4/lib/linodes/types'; import type { Subnet } from '@linode/api-v4/lib/vpcs'; interface LinodeEntityDetailProps { @@ -343,58 +335,19 @@ export const Body = React.memo((props: BodyProps) => { const username = profile?.username ?? 'none'; const theme = useTheme(); - const flags = useFlags(); - const { account } = useAccountManagement(); - const displayVPCSection = isFeatureEnabled( - 'VPCs', - Boolean(flags.vpc), - account?.capabilities ?? [] - ); - - const { data: vpcData } = useVPCsQuery({}, {}, displayVPCSection); - const vpcsList = vpcData?.data ?? []; - - const vpcLinodeIsAssignedTo = vpcsList.find((vpc) => { - const subnets = vpc.subnets; - - return Boolean( - subnets.find((subnet) => - subnet.linodes.some((linodeInfo) => linodeInfo.id === linodeId) - ) - ); - }); + const { + configInterfaceWithVPC, + displayVPCSection, + isVPCOnlyLinode, + vpcLinodeIsAssignedTo, + } = useVPCConfigInterface(linodeId); // Filter and retrieve subnets associated with a specific Linode ID const linodeAssociatedSubnets = vpcLinodeIsAssignedTo?.subnets.filter( (subnet) => subnet.linodes.some((linode) => linode.id === linodeId) ); - const { data: configs } = useAllLinodeConfigsQuery( - linodeId, - Boolean(vpcLinodeIsAssignedTo) // only grab configs if necessary - ); - let _configInterfaceWithVPC: Interface | undefined; - - // eslint-disable-next-line no-unused-expressions - configs?.find((config) => { - const interfaces = config.interfaces; - - const interfaceWithVPC = interfaces.find( - (_interface) => _interface.vpc_id === vpcLinodeIsAssignedTo?.id - ); - - if (interfaceWithVPC) { - _configInterfaceWithVPC = interfaceWithVPC; - } - - return interfaceWithVPC; - }); - - // A VPC-only Linode is a Linode that has at least one config interface with primary set to true and purpose vpc and no ipv4.nat_1_1 value - const isVPCOnlyLinode = Boolean( - _configInterfaceWithVPC?.primary && !_configInterfaceWithVPC.ipv4?.nat_1_1 - ); const numIPAddresses = ipv4.length + (ipv6 ? 1 : 0); const firstAddress = ipv4[0]; @@ -533,7 +486,7 @@ export const Body = React.memo((props: BodyProps) => { VPC IPv4: {' '} - {_configInterfaceWithVPC?.ipv4?.vpc} + {configInterfaceWithVPC?.ipv4?.vpc} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index dcb7fcd6d3c..d4398c253a9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -21,17 +21,13 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { Typography } from 'src/components/Typography'; -import { useAccountManagement } from 'src/hooks/useAccountManagement'; -import { useFlags } from 'src/hooks/useFlags'; -import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; +import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useAllIPsQuery, useLinodeIPsQuery, } from 'src/queries/linodes/networking'; import { useGrants } from 'src/queries/profile'; -import { useVPCsQuery } from 'src/queries/vpcs'; -import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { getPermissionsForLinode } from 'src/utilities/linodes'; import { AddIPDrawer } from './AddIPDrawer'; @@ -53,8 +49,6 @@ import { ViewRDNSDrawer } from './ViewRDNSDrawer'; import { ViewRangeDrawer } from './ViewRangeDrawer'; import { IPTypes } from './types'; -import type { Interface } from '@linode/api-v4/lib/linodes/types'; - const useStyles = makeStyles()( (theme: Theme, _params, classes) => ({ copy: { @@ -88,6 +82,8 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const { data: grants } = useGrants(); const { data: ips, error, isLoading } = useLinodeIPsQuery(linodeID); + const { isVPCOnlyLinode } = useVPCConfigInterface(linodeID); + const readOnly = getPermissionsForLinode(grants, linodeID) === 'read_only'; const [selectedIP, setSelectedIP] = React.useState(); @@ -109,51 +105,6 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const [isViewRDNSDialogOpen, setIsViewRDNSDialogOpen] = React.useState(false); const [isAddDrawerOpen, setIsAddDrawerOpen] = React.useState(false); - const flags = useFlags(); - const { account } = useAccountManagement(); - - const displayVPCSection = isFeatureEnabled( - 'VPCs', - Boolean(flags.vpc), - account?.capabilities ?? [] - ); - - const { data: vpcData } = useVPCsQuery({}, {}, displayVPCSection); - const vpcsList = vpcData?.data ?? []; - - const vpcLinodeIsAssignedTo = vpcsList.find((vpc) => { - const subnets = vpc.subnets; - - return Boolean( - subnets.find((subnet) => - subnet.linodes.some((linodeInfo) => linodeInfo.id === linodeID) - ) - ); - }); - - const { data: configs } = useAllLinodeConfigsQuery(linodeID); - let _configInterfaceWithVPC: Interface | undefined; - - // eslint-disable-next-line no-unused-expressions - configs?.find((config) => { - const interfaces = config.interfaces; - - const interfaceWithVPC = interfaces.find( - (_interface) => _interface.vpc_id === vpcLinodeIsAssignedTo?.id - ); - - if (interfaceWithVPC) { - _configInterfaceWithVPC = interfaceWithVPC; - } - - return interfaceWithVPC; - }); - - // A VPC-only Linode is a Linode that has at least one config interface with primary set to true and purpose vpc and no ipv4.nat_1_1 value - const isVPCOnlyLinode = Boolean( - _configInterfaceWithVPC?.primary && !_configInterfaceWithVPC.ipv4?.nat_1_1 - ); - const openRemoveIPDialog = (ip: IPAddress) => { setSelectedIP(ip); setIsDeleteIPDialogOpen(true); diff --git a/packages/manager/src/hooks/useVPCConfigInterface.ts b/packages/manager/src/hooks/useVPCConfigInterface.ts new file mode 100644 index 00000000000..67ec0d7f601 --- /dev/null +++ b/packages/manager/src/hooks/useVPCConfigInterface.ts @@ -0,0 +1,61 @@ +import { useAccountManagement } from 'src/hooks/useAccountManagement'; +import { useFlags } from 'src/hooks/useFlags'; +import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; +import { useVPCsQuery } from 'src/queries/vpcs'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; + +import type { Interface } from '@linode/api-v4/lib/linodes/types'; + +export const useVPCConfigInterface = (linodeId: number) => { + const flags = useFlags(); + const { account } = useAccountManagement(); + + const displayVPCSection = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + + const { data: vpcData } = useVPCsQuery({}, {}, displayVPCSection); + const vpcsList = vpcData?.data ?? []; + + const vpcLinodeIsAssignedTo = vpcsList.find((vpc) => { + const subnets = vpc.subnets; + + return Boolean( + subnets.find((subnet) => + subnet.linodes.some((linodeInfo) => linodeInfo.id === linodeId) + ) + ); + }); + + const { data: configs } = useAllLinodeConfigsQuery(linodeId); + let configInterfaceWithVPC: Interface | undefined; + + // eslint-disable-next-line no-unused-expressions + configs?.find((config) => { + const interfaces = config.interfaces; + + const interfaceWithVPC = interfaces.find( + (_interface) => _interface.vpc_id === vpcLinodeIsAssignedTo?.id + ); + + if (interfaceWithVPC) { + configInterfaceWithVPC = interfaceWithVPC; + } + + return interfaceWithVPC; + }); + + // A VPC-only Linode is a Linode that has at least one config interface with primary set to true and purpose vpc and no ipv4.nat_1_1 value + const isVPCOnlyLinode = Boolean( + configInterfaceWithVPC?.primary && !configInterfaceWithVPC.ipv4?.nat_1_1 + ); + + return { + configInterfaceWithVPC, + displayVPCSection, + isVPCOnlyLinode, + vpcLinodeIsAssignedTo, + }; +}; From 2f9fbc03c2c192926e2752c6691dd0dd915a2082 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 9 Nov 2023 16:51:01 -0500 Subject: [PATCH 06/17] move renderIPRow into own component --- .../LinodeNetworking/EditRangeRDNSDrawer.tsx | 2 +- .../LinodeNetworking/LinodeIPAddressRow.tsx | 236 ++++++++++++++++++ .../LinodeIPAddresses.test.ts | 7 +- .../LinodeNetworking/LinodeIPAddresses.tsx | 221 ++-------------- .../LinodeNetworking/ViewRDNSDrawer.tsx | 2 +- 5 files changed, 260 insertions(+), 208 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx index 528a8f3aa04..eb470919aa1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx @@ -16,7 +16,7 @@ import { } from 'src/queries/linodes/networking'; import { getErrorMap } from 'src/utilities/errorUtils'; -import { listIPv6InRange } from './LinodeIPAddresses'; +import { listIPv6InRange } from './LinodeIPAddressRow'; interface Props { linodeId: number; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx new file mode 100644 index 00000000000..7f50330781e --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx @@ -0,0 +1,236 @@ +import { IPAddress, IPRange } from '@linode/api-v4/lib/networking'; +import { Theme, useTheme } from '@mui/material/styles'; +import { IPv6, parse as parseIP } from 'ipaddr.js'; +import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; + +import { CircleProgress } from 'src/components/CircleProgress'; +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { Typography } from 'src/components/Typography'; +import { IPDisplay } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses'; +import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; +import { + useAllIPsQuery, + useLinodeIPsQuery, +} from 'src/queries/linodes/networking'; + +import { StyledActionTableCell } from './LinodeIPAddresses.styles'; +import { LinodeNetworkingActionMenu } from './LinodeNetworkingActionMenu'; + +const useStyles = makeStyles()( + (theme: Theme, _params, classes) => ({ + copy: { + '& svg': { + height: `12px`, + opacity: 0, + width: `12px`, + }, + marginLeft: 4, + top: 1, + }, + row: { + [`&:hover .${classes.copy} > svg, & .${classes.copy}:focus > svg`]: { + opacity: 1, + }, + }, + }) +); + +export interface IPAddressRowHandlers { + handleOpenEditRDNS: (ip: IPAddress) => void; + handleOpenEditRDNSForRange: (range: IPRange) => void; + handleOpenIPV6Details: (range: IPRange) => void; + openRemoveIPDialog: (ip: IPAddress) => void; + openRemoveIPRangeDialog: (range: IPRange) => void; +} + +interface Props { + linodeId: number; + readOnly: boolean; +} + +type CombinedProps = IPDisplay & IPAddressRowHandlers & Props; + +export const LinodeIPAddressRow = (props: CombinedProps) => { + const { + _ip, + _range, + address, + gateway, + handleOpenEditRDNS, + handleOpenEditRDNSForRange, + handleOpenIPV6Details, + linodeId, + openRemoveIPDialog, + openRemoveIPRangeDialog, + rdns, + readOnly, + subnetMask, + type, + } = props; + + const { classes } = useStyles(); + const { isVPCOnlyLinode } = useVPCConfigInterface(linodeId); + const { data: ips } = useLinodeIPsQuery(linodeId); + + // TODO: in order to fully get rid of makeStyles for this file, may need to convert this to a functional component + // rather than function inside this component >> will look into during part 2 of this ticket + const isOnlyPublicIP = + ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; + + const disabled = isVPCOnlyLinode && type === 'IPv4 – Public'; + + return ( + + + + {!isVPCOnlyLinode || + (type !== 'IPv4 – Public' && ( + + ))} + + + {type} + + {gateway} + {subnetMask} + + {/* Ranges have special handling for RDNS. */} + {_range ? ( + handleOpenIPV6Details(_range)} + range={_range} + /> + ) : ( + rdns + )} + + + {_ip ? ( + + ) : _range ? ( + handleOpenEditRDNSForRange(_range)} + onRemove={openRemoveIPRangeDialog} + readOnly={readOnly} + /> + ) : null} + + + ); +}; + +const RangeRDNSCell = (props: { + linodeId: number; + onViewDetails: () => void; + range: IPRange; +}) => { + const { linodeId, onViewDetails, range } = props; + const theme = useTheme(); + + const { data: linode } = useLinodeQuery(linodeId); + + const { data: ipsInRegion, isLoading: ipv6Loading } = useAllIPsQuery( + {}, + { + region: linode?.region, + }, + linode !== undefined + ); + + const ipsWithRDNS = listIPv6InRange(range.range, range.prefix, ipsInRegion); + + if (ipv6Loading) { + return ; + } + + // We don't show anything if there are no addresses. + if (ipsWithRDNS.length === 0) { + return null; + } + + if (ipsWithRDNS.length === 1) { + return ( + + {ipsWithRDNS[0].address} + {ipsWithRDNS[0].rdns} + + ); + } + + return ( + + ); +}; + +// Given a range, prefix, and a list of IPs, filter out the IPs that do not fall within the IPv6 range. +export const listIPv6InRange = ( + range: string, + prefix: number, + ips: IPAddress[] = [] +) => { + return ips.filter((thisIP) => { + // Only keep addresses that: + // 1. are part of an IPv6 range or pool + // 2. have RDNS set + if ( + !['ipv6/pool', 'ipv6/range'].includes(thisIP.type) || + thisIP.rdns === null + ) { + // eslint-disable-next-line array-callback-return + return; + } + + // The ipaddr.js library throws an if it can't parse an IP address. + // We'll wrap this in a try/catch block just in case something is malformed. + try { + // We need to typecast here so that the overloaded `match()` is typed correctly. + const addr = parseIP(thisIP.address) as IPv6; + const parsedRange = parseIP(range) as IPv6; + + return addr.match(parsedRange, prefix); + } catch { + return false; + } + }); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.test.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.test.ts index 31a6cb2cf46..4eeefcb9e3e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.test.ts +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.test.ts @@ -2,11 +2,8 @@ import { LinodeIPsResponse } from '@linode/api-v4/lib/linodes'; import { ipAddressFactory } from 'src/factories/networking'; -import { - createType, - ipResponseToDisplayRows, - listIPv6InRange, -} from './LinodeIPAddresses'; +import { listIPv6InRange } from './LinodeIPAddressRow'; +import { createType, ipResponseToDisplayRows } from './LinodeIPAddresses'; describe('listIPv6InRange utility function', () => { const ipv4List = ipAddressFactory.buildList(4); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index d4398c253a9..cf7e73d95e7 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -1,15 +1,11 @@ import { LinodeIPsResponse } from '@linode/api-v4/lib/linodes'; import { IPAddress, IPRange } from '@linode/api-v4/lib/networking'; import Grid from '@mui/material/Unstable_Grid2'; -import { Theme, useTheme } from '@mui/material/styles'; -import { IPv6, parse as parseIP } from 'ipaddr.js'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import AddNewLink from 'src/components/AddNewLink'; import { Button } from 'src/components/Button/Button'; import { CircleProgress } from 'src/components/CircleProgress'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Hidden } from 'src/components/Hidden'; import OrderBy from 'src/components/OrderBy'; @@ -20,13 +16,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; -import { Typography } from 'src/components/Typography'; -import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; -import { useLinodeQuery } from 'src/queries/linodes/linodes'; -import { - useAllIPsQuery, - useLinodeIPsQuery, -} from 'src/queries/linodes/networking'; +import { useLinodeIPsQuery } from 'src/queries/linodes/networking'; import { useGrants } from 'src/queries/profile'; import { getPermissionsForLinode } from 'src/utilities/linodes'; @@ -37,37 +27,17 @@ import { EditIPRDNSDrawer } from './EditIPRDNSDrawer'; import { EditRangeRDNSDrawer } from './EditRangeRDNSDrawer'; import IPSharing from './IPSharing'; import IPTransfer from './IPTransfer'; +import { IPAddressRowHandlers, LinodeIPAddressRow } from './LinodeIPAddressRow'; import { - StyledActionTableCell, StyledRootGrid, StyledTypography, StyledWrapperGrid, } from './LinodeIPAddresses.styles'; -import { LinodeNetworkingActionMenu } from './LinodeNetworkingActionMenu'; import { ViewIPDrawer } from './ViewIPDrawer'; import { ViewRDNSDrawer } from './ViewRDNSDrawer'; import { ViewRangeDrawer } from './ViewRangeDrawer'; import { IPTypes } from './types'; -const useStyles = makeStyles()( - (theme: Theme, _params, classes) => ({ - copy: { - '& svg': { - height: `12px`, - opacity: 0, - width: `12px`, - }, - marginLeft: 4, - top: 1, - }, - row: { - [`&:hover .${classes.copy} > svg, & .${classes.copy}:focus > svg`]: { - opacity: 1, - }, - }, - }) -); - export const ipv4TableID = 'ips'; interface LinodeIPAddressesProps { @@ -77,13 +47,9 @@ interface LinodeIPAddressesProps { export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const { linodeID } = props; - const { classes } = useStyles(); - const { data: grants } = useGrants(); const { data: ips, error, isLoading } = useLinodeIPsQuery(linodeID); - const { isVPCOnlyLinode } = useVPCConfigInterface(linodeID); - const readOnly = getPermissionsForLinode(grants, linodeID) === 'read_only'; const [selectedIP, setSelectedIP] = React.useState(); @@ -130,75 +96,12 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { setIsViewRDNSDialogOpen(true); }; - const renderIPRow = (ipDisplay: IPDisplay) => { - // TODO: in order to fully get rid of makeStyles for this file, may need to convert this to a functional component - // rather than function inside this component >> will look into during part 2 of this ticket - const { _ip, _range, address, gateway, rdns, subnetMask, type } = ipDisplay; - const isOnlyPublicIP = - ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; - - const disabled = isVPCOnlyLinode && ipDisplay.type === 'IPv4 – Public'; - - return ( - - - - {!isVPCOnlyLinode || - (ipDisplay.type !== 'IPv4 – Public' && ( - - ))} - - - {type} - - {gateway} - {subnetMask} - - {/* Ranges have special handling for RDNS. */} - {_range ? ( - handleOpenIPV6Details(_range)} - range={_range} - /> - ) : ( - rdns - )} - - - {_ip ? ( - - ) : _range ? ( - handleOpenEditRDNSForRange(_range)} - onRemove={openRemoveIPRangeDialog} - readOnly={readOnly} - /> - ) : null} - - - ); + const handlers: IPAddressRowHandlers = { + handleOpenEditRDNS, + handleOpenEditRDNSForRange, + handleOpenIPV6Details, + openRemoveIPDialog, + openRemoveIPRangeDialog, }; if (isLoading) { @@ -279,7 +182,17 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { - {orderedData.map(renderIPRow)} + + {orderedData.map((ipDisplay) => ( + + ))} + ); }} @@ -356,102 +269,8 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { ); }; -const RangeRDNSCell = (props: { - linodeId: number; - onViewDetails: () => void; - range: IPRange; -}) => { - const { linodeId, onViewDetails, range } = props; - const theme = useTheme(); - - const { data: linode } = useLinodeQuery(linodeId); - - const { data: ipsInRegion, isLoading: ipv6Loading } = useAllIPsQuery( - {}, - { - region: linode?.region, - }, - linode !== undefined - ); - - const ipsWithRDNS = listIPv6InRange(range.range, range.prefix, ipsInRegion); - - if (ipv6Loading) { - return ; - } - - // We don't show anything if there are no addresses. - if (ipsWithRDNS.length === 0) { - return null; - } - - if (ipsWithRDNS.length === 1) { - return ( - - {ipsWithRDNS[0].address} - {ipsWithRDNS[0].rdns} - - ); - } - - return ( - - ); -}; - -// ============================================================================= -// Utilities -// ============================================================================= - -// Given a range, prefix, and a list of IPs, filter out the IPs that do not fall within the IPv6 range. -export const listIPv6InRange = ( - range: string, - prefix: number, - ips: IPAddress[] = [] -) => { - return ips.filter((thisIP) => { - // Only keep addresses that: - // 1. are part of an IPv6 range or pool - // 2. have RDNS set - if ( - !['ipv6/pool', 'ipv6/range'].includes(thisIP.type) || - thisIP.rdns === null - ) { - // eslint-disable-next-line array-callback-return - return; - } - - // The ipaddr.js library throws an if it can't parse an IP address. - // We'll wrap this in a try/catch block just in case something is malformed. - try { - // We need to typecast here so that the overloaded `match()` is typed correctly. - const addr = parseIP(thisIP.address) as IPv6; - const parsedRange = parseIP(range) as IPv6; - - return addr.match(parsedRange, prefix); - } catch { - return false; - } - }); -}; - // Higher-level IP address display for the IP Table. -interface IPDisplay { +export interface IPDisplay { // Not for display, but useful for lower-level components. _ip?: IPAddress; _range?: IPRange; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ViewRDNSDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ViewRDNSDrawer.tsx index 58011a3fab8..0f62eeedfd9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ViewRDNSDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ViewRDNSDrawer.tsx @@ -7,7 +7,7 @@ import { Typography } from 'src/components/Typography'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useAllIPsQuery } from 'src/queries/linodes/networking'; -import { listIPv6InRange } from './LinodeIPAddresses'; +import { listIPv6InRange } from './LinodeIPAddressRow'; interface Props { linodeId: number; From 82ff7b56eedc20de26fa097c5bb7a5353f9e7c73 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 9 Nov 2023 17:43:19 -0500 Subject: [PATCH 07/17] migrate styles --- .../LinodeNetworking/LinodeIPAddressRow.tsx | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx index 7f50330781e..18e005290cc 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx @@ -1,14 +1,14 @@ import { IPAddress, IPRange } from '@linode/api-v4/lib/networking'; -import { Theme, useTheme } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import { IPv6, parse as parseIP } from 'ipaddr.js'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import { CircleProgress } from 'src/components/CircleProgress'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { TableCell } from 'src/components/TableCell'; -import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; +import { StyledTableRow } from 'src/features/Linodes/LinodeEntityDetail.styles'; import { IPDisplay } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses'; import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; @@ -20,25 +20,6 @@ import { import { StyledActionTableCell } from './LinodeIPAddresses.styles'; import { LinodeNetworkingActionMenu } from './LinodeNetworkingActionMenu'; -const useStyles = makeStyles()( - (theme: Theme, _params, classes) => ({ - copy: { - '& svg': { - height: `12px`, - opacity: 0, - width: `12px`, - }, - marginLeft: 4, - top: 1, - }, - row: { - [`&:hover .${classes.copy} > svg, & .${classes.copy}:focus > svg`]: { - opacity: 1, - }, - }, - }) -); - export interface IPAddressRowHandlers { handleOpenEditRDNS: (ip: IPAddress) => void; handleOpenEditRDNSForRange: (range: IPRange) => void; @@ -72,7 +53,6 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { type, } = props; - const { classes } = useStyles(); const { isVPCOnlyLinode } = useVPCConfigInterface(linodeId); const { data: ips } = useLinodeIPsQuery(linodeId); @@ -84,8 +64,7 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { const disabled = isVPCOnlyLinode && type === 'IPv4 – Public'; return ( - { > {!isVPCOnlyLinode || - (type !== 'IPv4 – Public' && ( - - ))} + (type !== 'IPv4 – Public' && )} {type} @@ -141,10 +118,22 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { /> ) : null} - + ); }; +const StyledCopyToolTip = styled(CopyTooltip, { + label: 'StyledCopyToolTip', +})(() => ({ + '& svg': { + height: `12px`, + opacity: 0, + width: `12px`, + }, + marginLeft: 4, + top: 1, +})); + const RangeRDNSCell = (props: { linodeId: number; onViewDetails: () => void; From 0f207aa91b9ae3415fb82fd8a2fd7256adc90e03 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Thu, 9 Nov 2023 18:29:11 -0500 Subject: [PATCH 08/17] add unit test for LinodeIPAddressRow --- .../LinodeIPAddressRow.test.tsx | 100 ++++++++++++++++++ .../LinodeNetworking/LinodeIPAddressRow.tsx | 9 +- .../LinodeNetworking/LinodeIPAddresses.tsx | 5 + 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx new file mode 100644 index 00000000000..08fbfb084d5 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -0,0 +1,100 @@ +import { fireEvent } from '@testing-library/react'; +import * as React from 'react'; + +import { linodeIPFactory } from 'src/factories/linodes'; +import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/AccessTable'; +import { ipResponseToDisplayRows } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses'; +import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; + +import { IPAddressRowHandlers, LinodeIPAddressRow } from './LinodeIPAddressRow'; + +const ips = linodeIPFactory.build(); +const ipDisplay = ipResponseToDisplayRows(ips)[0]; + +const handlers: IPAddressRowHandlers = { + handleOpenEditRDNS: jest.fn(), + handleOpenEditRDNSForRange: jest.fn(), + handleOpenIPV6Details: jest.fn(), + openRemoveIPDialog: jest.fn(), + openRemoveIPRangeDialog: jest.fn(), +}; + +describe('LinodeIPAddressRow', () => { + it('should render a Linode IP Address row', () => { + const { getAllByText } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + getAllByText(ipDisplay.address); + getAllByText(ipDisplay.type); + getAllByText(ipDisplay.gateway); + getAllByText(ipDisplay.subnetMask); + getAllByText(ipDisplay.rdns); + // Check if actions were rendered + getAllByText('Delete'); + getAllByText('Edit RDNS'); + }); + + it('should disable the row if disabled is true and display a tooltip', async () => { + const { findByRole, getAllByRole } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + const buttons = getAllByRole('button'); + + const deleteBtn = buttons[1]; + expect(deleteBtn).toBeDisabled(); + const deleteBtnTooltip = buttons[2]; + fireEvent.mouseEnter(deleteBtnTooltip); + const publicIpsUnassignedTooltip = await findByRole(/tooltip/); + expect(publicIpsUnassignedTooltip).toContainHTML( + PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT + ); + + const editRDNSBtn = buttons[3]; + expect(editRDNSBtn).toBeDisabled(); + const editRDNSBtnTooltip = buttons[4]; + fireEvent.mouseEnter(editRDNSBtnTooltip); + const publicIpsUnassignedTooltip2 = await findByRole(/tooltip/); + expect(publicIpsUnassignedTooltip2).toContainHTML( + PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT + ); + }); + + it('should not disable the row if disabled is false', () => { + const { getAllByRole } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + const buttons = getAllByRole('button'); + + const deleteBtn = buttons[1]; + expect(deleteBtn).not.toBeDisabled(); + const editRDNSBtn = buttons[3]; + expect(editRDNSBtn).not.toBeDisabled(); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx index 18e005290cc..bd392776536 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx @@ -10,7 +10,6 @@ import { TableCell } from 'src/components/TableCell'; import { Typography } from 'src/components/Typography'; import { StyledTableRow } from 'src/features/Linodes/LinodeEntityDetail.styles'; import { IPDisplay } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses'; -import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useAllIPsQuery, @@ -29,6 +28,7 @@ export interface IPAddressRowHandlers { } interface Props { + disabled: boolean; linodeId: number; readOnly: boolean; } @@ -40,6 +40,7 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { _ip, _range, address, + disabled, gateway, handleOpenEditRDNS, handleOpenEditRDNSForRange, @@ -53,7 +54,6 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { type, } = props; - const { isVPCOnlyLinode } = useVPCConfigInterface(linodeId); const { data: ips } = useLinodeIPsQuery(linodeId); // TODO: in order to fully get rid of makeStyles for this file, may need to convert this to a functional component @@ -61,8 +61,6 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { const isOnlyPublicIP = ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; - const disabled = isVPCOnlyLinode && type === 'IPv4 – Public'; - return ( { sx={{ whiteSpace: 'nowrap' }} > - {!isVPCOnlyLinode || - (type !== 'IPv4 – Public' && )} + {!disabled && } {type} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index cf7e73d95e7..1b919d01571 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -16,6 +16,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; +import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useLinodeIPsQuery } from 'src/queries/linodes/networking'; import { useGrants } from 'src/queries/profile'; import { getPermissionsForLinode } from 'src/utilities/linodes'; @@ -51,6 +52,7 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { const { data: ips, error, isLoading } = useLinodeIPsQuery(linodeID); const readOnly = getPermissionsForLinode(grants, linodeID) === 'read_only'; + const { isVPCOnlyLinode } = useVPCConfigInterface(linodeID); const [selectedIP, setSelectedIP] = React.useState(); const [selectedRange, setSelectedRange] = React.useState(); @@ -187,6 +189,9 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { Date: Mon, 13 Nov 2023 13:54:07 -0500 Subject: [PATCH 09/17] move public ips unassigned tooltip to separate component --- .../src/features/Linodes/AccessTable.test.tsx | 3 +- .../src/features/Linodes/AccessTable.tsx | 27 ++-------------- .../LinodeIPAddressRow.test.tsx | 2 +- .../LinodeNetworkingActionMenu.tsx | 2 +- .../Linodes/PublicIpsUnassignedTooltip.tsx | 32 +++++++++++++++++++ 5 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx diff --git a/packages/manager/src/features/Linodes/AccessTable.test.tsx b/packages/manager/src/features/Linodes/AccessTable.test.tsx index 260b93c6279..c2cfda6b6d2 100644 --- a/packages/manager/src/features/Linodes/AccessTable.test.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.test.tsx @@ -2,9 +2,10 @@ import { fireEvent } from '@testing-library/react'; import * as React from 'react'; import { linodeFactory } from 'src/factories'; +import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { AccessTable, PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from './AccessTable'; +import { AccessTable } from './AccessTable'; const linode = linodeFactory.build(); diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx index 8ed1de0315a..fe1b24380a3 100644 --- a/packages/manager/src/features/Linodes/AccessTable.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -3,11 +3,9 @@ import { SxProps } from '@mui/system'; import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; -import { Link } from 'src/components/Link'; import { TableBody } from 'src/components/TableBody'; import { TableCell } from 'src/components/TableCell'; -import { TooltipIcon } from 'src/components/TooltipIcon'; -import { Typography } from 'src/components/Typography'; +import { PublicIpsUnassignedTooltip } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; import { StyledColumnLabelGrid, @@ -33,14 +31,6 @@ interface AccessTableProps { title: string; } -const sxTooltipIcon = { - padding: '0', - paddingLeft: '4px', -}; - -export const PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT = `The Public IP Addresses have been unassigned from the -configuration profile.`; - export const AccessTable = React.memo((props: AccessTableProps) => { return ( { {props.title}{' '} {props.isVPCOnlyLinode && props.title.includes('Public IP Address') && ( - - {PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT}{' '} - - Learn more - - . - - } - interactive - status="help" - sxTooltipIcon={sxTooltipIcon} - /> + )} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx index 08fbfb084d5..5aa2984a74b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -2,8 +2,8 @@ import { fireEvent } from '@testing-library/react'; import * as React from 'react'; import { linodeIPFactory } from 'src/factories/linodes'; -import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/AccessTable'; import { ipResponseToDisplayRows } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses'; +import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; import { IPAddressRowHandlers, LinodeIPAddressRow } from './LinodeIPAddressRow'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx index 73793749899..95beef8f40d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { Action, ActionMenu } from 'src/components/ActionMenu'; import { Box } from 'src/components/Box'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/AccessTable'; +import { PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; import { IPTypes } from './types'; diff --git a/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx b/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx new file mode 100644 index 00000000000..c30729c1a60 --- /dev/null +++ b/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import { Link } from 'src/components/Link'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { Typography } from 'src/components/Typography'; + +const sxTooltipIcon = { + padding: '0', + paddingLeft: '4px', +}; + +export const PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT = `The Public IP Addresses have been unassigned from the +configuration profile.`; + +export const PublicIpsUnassignedTooltip = () => { + return ( + + {PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT}{' '} + + Learn more + + . + + } + interactive + status="help" + sxTooltipIcon={sxTooltipIcon} + /> + ); +}; From ad7d286fa3f55cbdc91a774856a42fe7b32cec4d Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 13 Nov 2023 16:25:07 -0500 Subject: [PATCH 10/17] conditionally disable public ip address column in linodes landing table --- .../Linodes/LinodesLanding/IPAddress.test.tsx | 6 ++++++ .../Linodes/LinodesLanding/IPAddress.tsx | 20 +++++++++++++++++-- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 9 ++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.test.tsx b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.test.tsx index 5619a50f945..2cd5002fd6f 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.test.tsx @@ -75,4 +75,10 @@ describe('IPAddress', () => { ).toEqual([publicIP, publicIP2, privateIP, privateIP2]); }); }); + + it('should disable copy functionality if disabled is true', () => { + component.setProps({ disabled: true }); + const copyTooltip = component.find('[data-qa-copy-ip-text]'); + expect(copyTooltip.prop('disabled')).toBe(true); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx index 271f0ff9038..00728e1ec41 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { ShowMore } from 'src/components/ShowMore/ShowMore'; +import { PublicIpsUnassignedTooltip } from 'src/features/Linodes/PublicIpsUnassignedTooltip'; import { privateIPRegex } from 'src/utilities/ipUtils'; import { @@ -13,6 +14,11 @@ import { } from './IPAddress.styles'; export interface IPAddressProps { + /** + * If true, the copy button will be disabled with a tooltip explanation. + * @default false + */ + disabled?: boolean; /** * Conditional handlers to be applied to the IP wrapper div when `showTooltipOnIpHover` is true. * @default undefined @@ -53,10 +59,11 @@ export const sortIPAddress = (ip1: string, ip2: string) => export const IPAddress = (props: IPAddressProps) => { const { + disabled = false, ips, + isHovered = false, showAll, showMore, - isHovered = false, showTooltipOnIpHover = false, } = props; @@ -82,6 +89,10 @@ export const IPAddress = (props: IPAddressProps) => { const handleMouseLeave = () => setIsIpTooltipHovered(false); const renderCopyIcon = (ip: string) => { + if (disabled) { + return ; + } + return ( { {...handlers} showTooltipOnIpHover={showTooltipOnIpHover} > - + {renderCopyIcon(ip)} ); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 15d82f576ed..9917d23f94d 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -17,6 +17,7 @@ import { transitionText, } from 'src/features/Linodes/transitions'; import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; +import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; import { useTypeQuery } from 'src/queries/types'; import { useRecentEventForLinode } from 'src/store/selectors/recentEventForLinode'; import { capitalizeAllWords } from 'src/utilities/capitalize'; @@ -55,6 +56,8 @@ export const LinodeRow = (props: Props) => { const recentEvent = useRecentEventForLinode(id); + const { isVPCOnlyLinode } = useVPCConfigInterface(id); + const isBareMetalInstance = linodeType?.class === 'metal'; const loading = linodeInTransition(status, recentEvent); @@ -141,7 +144,11 @@ export const LinodeRow = (props: Props) => { {linodeType ? formatStorageUnits(linodeType.label) : type} - + From daa3996dfa6a6101eb5355553494093a16eb6e57 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 13 Nov 2023 16:36:28 -0500 Subject: [PATCH 11/17] Added changeset: Disable Public IP Address for VPC only Linode --- .../.changeset/pr-9899-upcoming-features-1699911388781.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md diff --git a/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md b/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md new file mode 100644 index 00000000000..89ed7ee4491 --- /dev/null +++ b/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Disable Public IP Address for VPC only Linode ([#9899](https://github.com/linode/manager/pull/9899)) From e9f3140f0cdb3354b884ff2f3fcca97a8ad79734 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Mon, 13 Nov 2023 17:14:40 -0500 Subject: [PATCH 12/17] clean up --- packages/manager/src/features/Linodes/AccessTable.test.tsx | 2 +- .../LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/manager/src/features/Linodes/AccessTable.test.tsx b/packages/manager/src/features/Linodes/AccessTable.test.tsx index c2cfda6b6d2..6bf75b6b5e8 100644 --- a/packages/manager/src/features/Linodes/AccessTable.test.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.test.tsx @@ -35,7 +35,7 @@ describe('AccessTable', () => { }); }); - it('should not disable copy button if isVPCOnlyLinode is false', async () => { + it('should not disable copy button if isVPCOnlyLinode is false', () => { const { getAllByRole } = renderWithTheme( { const { data: ips } = useLinodeIPsQuery(linodeId); - // TODO: in order to fully get rid of makeStyles for this file, may need to convert this to a functional component - // rather than function inside this component >> will look into during part 2 of this ticket const isOnlyPublicIP = ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; From a3fe0cf4f2df77c70e2669c36512e9822a0d1875 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 15 Nov 2023 10:57:20 -0500 Subject: [PATCH 13/17] fixed disabled styling --- .../manager/src/components/Button/StyledActionButton.ts | 9 ++++++++- .../manager/src/components/CopyTooltip/CopyTooltip.tsx | 5 ++++- .../manager/src/components/TableRow/TableRow.styles.ts | 8 ++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/components/Button/StyledActionButton.ts b/packages/manager/src/components/Button/StyledActionButton.ts index 3b4ac509137..0cb7ab13647 100644 --- a/packages/manager/src/components/Button/StyledActionButton.ts +++ b/packages/manager/src/components/Button/StyledActionButton.ts @@ -12,7 +12,7 @@ import { Button } from './Button'; */ export const StyledActionButton = styled(Button, { label: 'StyledActionButton', -})(({ theme }) => ({ +})(({ theme, ...props }) => ({ '&:hover': { backgroundColor: theme.palette.primary.main, color: theme.name === 'dark' ? theme.color.black : theme.color.white, @@ -22,4 +22,11 @@ export const StyledActionButton = styled(Button, { lineHeight: '16px', minWidth: 0, padding: '12px 10px', + ...(props.disabled && { + color: + theme.palette.mode === 'dark' + ? `${theme.color.grey6} !important` + : theme.color.disabledText, + cursor: 'default', + }), })); diff --git a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx index 777a1cda25e..0c2227dbde4 100644 --- a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx +++ b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx @@ -123,7 +123,10 @@ const StyledCopyButton = styled('button', { padding: 0, }), ...(props.disabled && { - color: theme.color.disabledText, + color: + theme.palette.mode === 'dark' + ? theme.color.grey6 + : theme.color.disabledText, cursor: 'default', }), })); diff --git a/packages/manager/src/components/TableRow/TableRow.styles.ts b/packages/manager/src/components/TableRow/TableRow.styles.ts index a4f55e38f74..7a63344b489 100644 --- a/packages/manager/src/components/TableRow/TableRow.styles.ts +++ b/packages/manager/src/components/TableRow/TableRow.styles.ts @@ -64,9 +64,13 @@ export const StyledTableRow = styled(_TableRow, { }), ...(props.disabled && { '& td': { - color: '#D2D3D4', + color: + theme.palette.mode === 'dark' + ? theme.color.grey6 + : theme.color.disabledText, }, - backgroundColor: 'rgba(247, 247, 247, 0.25)', + backgroundColor: + theme.palette.mode === 'dark' ? '#32363c' : 'rgba(247, 247, 247, 0.25)', }), })); From fa8ffecceea509c88528ef16dc01fb227f71e54f Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Wed, 15 Nov 2023 10:58:26 -0500 Subject: [PATCH 14/17] address feedback --- ...pr-9899-upcoming-features-1699911388781.md | 2 +- .../src/features/Linodes/AccessTable.tsx | 26 ++++++------- .../Linodes/LinodesLanding/IPAddress.tsx | 2 +- .../Linodes/PublicIpsUnassignedTooltip.tsx | 38 +++++++++---------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md b/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md index 89ed7ee4491..188aaf37ddc 100644 --- a/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md +++ b/packages/manager/.changeset/pr-9899-upcoming-features-1699911388781.md @@ -2,4 +2,4 @@ "@linode/manager": Upcoming Features --- -Disable Public IP Address for VPC only Linode ([#9899](https://github.com/linode/manager/pull/9899)) +Disable Public IP Address for VPC-only Linodes ([#9899](https://github.com/linode/manager/pull/9899)) diff --git a/packages/manager/src/features/Linodes/AccessTable.tsx b/packages/manager/src/features/Linodes/AccessTable.tsx index fe1b24380a3..c53812a9cbc 100644 --- a/packages/manager/src/features/Linodes/AccessTable.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.tsx @@ -32,30 +32,28 @@ interface AccessTableProps { } export const AccessTable = React.memo((props: AccessTableProps) => { + const { footer, gridProps, isVPCOnlyLinode, rows, sx, title } = props; return ( - {props.title}{' '} - {props.isVPCOnlyLinode && props.title.includes('Public IP Address') && ( - - )} + {title}{' '} + {isVPCOnlyLinode && + title.includes('Public IP Address') && + PublicIpsUnassignedTooltip} - {props.rows.map((thisRow) => { + {rows.map((thisRow) => { return thisRow.text ? ( - + {thisRow.heading ? ( {thisRow.heading} @@ -65,12 +63,12 @@ export const AccessTable = React.memo((props: AccessTableProps) => { @@ -79,7 +77,7 @@ export const AccessTable = React.memo((props: AccessTableProps) => { })} - {props.footer ? {props.footer} : null} + {footer ? {footer} : null} ); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx index 00728e1ec41..9027280e7bc 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/IPAddress.tsx @@ -90,7 +90,7 @@ export const IPAddress = (props: IPAddressProps) => { const renderCopyIcon = (ip: string) => { if (disabled) { - return ; + return PublicIpsUnassignedTooltip; } return ( diff --git a/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx b/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx index c30729c1a60..fce59111dd2 100644 --- a/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx +++ b/packages/manager/src/features/Linodes/PublicIpsUnassignedTooltip.tsx @@ -9,24 +9,22 @@ const sxTooltipIcon = { paddingLeft: '4px', }; -export const PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT = `The Public IP Addresses have been unassigned from the -configuration profile.`; +export const PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT = + 'The Public IP Addresses have been unassigned from the configuration profile.'; -export const PublicIpsUnassignedTooltip = () => { - return ( - - {PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT}{' '} - - Learn more - - . - - } - interactive - status="help" - sxTooltipIcon={sxTooltipIcon} - /> - ); -}; +export const PublicIpsUnassignedTooltip = ( + + {PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT}{' '} + + Learn more + + . + + } + interactive + status="help" + sxTooltipIcon={sxTooltipIcon} + /> +); From ca8f205c9fa185688e7b3f58925bfedf72992182 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 17 Nov 2023 10:44:39 -0500 Subject: [PATCH 15/17] address feedback --- .../smoke-linode-landing-table.spec.ts | 4 +-- .../LinodeIPAddressRow.test.tsx | 6 ++--- .../LinodeNetworking/LinodeIPAddressRow.tsx | 25 +++++++++---------- .../LinodeNetworking/LinodeIPAddresses.tsx | 2 +- .../LinodeNetworkingActionMenu.tsx | 12 ++++----- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index 160a1a77218..81ba5308122 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -393,7 +393,7 @@ describe('linode landing checks', () => { .closest('[data-qa-linode-card]') .within(() => { cy.findByText('Summary').should('be.visible'); - cy.findByText('IP Addresses').should('be.visible'); + cy.findByText('Public IP Addresses').should('be.visible'); cy.findByText('Access').should('be.visible'); cy.findByText('Plan:').should('be.visible'); @@ -407,7 +407,7 @@ describe('linode landing checks', () => { getVisible('[aria-label="Toggle display"]').should('be.enabled').click(); cy.findByText('Summary').should('not.exist'); - cy.findByText('IP Addresses').should('not.exist'); + cy.findByText('Public IP Addresses').should('not.exist'); cy.findByText('Access').should('not.exist'); cy.findByText('Plan:').should('not.exist'); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx index 5aa2984a74b..afdead98aaf 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -24,7 +24,7 @@ describe('LinodeIPAddressRow', () => { const { getAllByText } = renderWithTheme( wrapWithTableBody( { const { findByRole, getAllByRole } = renderWithTheme( wrapWithTableBody( { const { getAllByRole } = renderWithTheme( wrapWithTableBody( void; handleOpenEditRDNSForRange: (range: IPRange) => void; @@ -28,7 +28,7 @@ export interface IPAddressRowHandlers { } interface Props { - disabled: boolean; + isVPCOnlyLinode: boolean; linodeId: number; readOnly: boolean; } @@ -40,11 +40,11 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { _ip, _range, address, - disabled, gateway, handleOpenEditRDNS, handleOpenEditRDNSForRange, handleOpenIPV6Details, + isVPCOnlyLinode, linodeId, openRemoveIPDialog, openRemoveIPRangeDialog, @@ -62,7 +62,7 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { return ( { parentColumn="Address" sx={{ whiteSpace: 'nowrap' }} > - - {!disabled && } + + {!isVPCOnlyLinode && } {type} @@ -93,20 +93,20 @@ export const LinodeIPAddressRow = (props: CombinedProps) => { {_ip ? ( ) : _range ? ( handleOpenEditRDNSForRange(_range)} onRemove={openRemoveIPRangeDialog} readOnly={readOnly} @@ -168,10 +168,9 @@ const RangeRDNSCell = (props: { } return ( - + ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index 1b919d01571..fa3a1b83735 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -189,7 +189,7 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { void; onRemove?: (ip: IPAddress | IPRange) => void; readOnly: boolean; @@ -27,10 +27,10 @@ export const LinodeNetworkingActionMenu = (props: Props) => { const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); const { - disabled, ipAddress, ipType, isOnlyPublicIP, + isVPCOnlyLinode, onEdit, onRemove, readOnly, @@ -58,14 +58,14 @@ export const LinodeNetworkingActionMenu = (props: Props) => { const actions = [ onRemove && ipAddress && !is116Range && deletableIPTypes.includes(ipType) ? { - disabled: readOnly || isOnlyPublicIP || disabled, + disabled: readOnly || isOnlyPublicIP || isVPCOnlyLinode, onClick: () => { onRemove(ipAddress); }, title: 'Delete', tooltip: readOnly ? readOnlyTooltip - : disabled + : isVPCOnlyLinode ? PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT : isOnlyPublicIP ? isOnlyPublicIPTooltip @@ -74,14 +74,14 @@ export const LinodeNetworkingActionMenu = (props: Props) => { : null, onEdit && ipAddress && showEdit ? { - disabled: readOnly || disabled, + disabled: readOnly || isVPCOnlyLinode, onClick: () => { onEdit(ipAddress); }, title: 'Edit RDNS', tooltip: readOnly ? readOnlyTooltip - : disabled + : isVPCOnlyLinode ? PUBLIC_IPS_UNASSIGNED_TOOLTIP_TEXT : undefined, } From d22c268ca3c3b7e0d3acf19e2669cbb15709c251 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 17 Nov 2023 11:40:07 -0500 Subject: [PATCH 16/17] revert button change --- .../LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx index ac2005abbba..853e6e1ffe6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx @@ -4,7 +4,6 @@ import { useTheme } from '@mui/material/styles'; import { IPv6, parse as parseIP } from 'ipaddr.js'; import * as React from 'react'; -import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { CircleProgress } from 'src/components/CircleProgress'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { TableCell } from 'src/components/TableCell'; @@ -168,7 +167,7 @@ const RangeRDNSCell = (props: { } return ( - @@ -182,7 +181,7 @@ const RangeRDNSCell = (props: { > {ipsWithRDNS.length} Addresses2 - + ); }; From b34f9c235d5bda884c3797bde73875e705ebef68 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Fri, 17 Nov 2023 13:24:31 -0500 Subject: [PATCH 17/17] change jest to vi --- .../LinodeNetworking/LinodeIPAddressRow.test.tsx | 10 +++++----- .../components/PlansPanel/PlanSelection.test.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx index afdead98aaf..60989a66618 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -12,11 +12,11 @@ const ips = linodeIPFactory.build(); const ipDisplay = ipResponseToDisplayRows(ips)[0]; const handlers: IPAddressRowHandlers = { - handleOpenEditRDNS: jest.fn(), - handleOpenEditRDNSForRange: jest.fn(), - handleOpenIPV6Details: jest.fn(), - openRemoveIPDialog: jest.fn(), - openRemoveIPRangeDialog: jest.fn(), + handleOpenEditRDNS: vi.fn(), + handleOpenEditRDNSForRange: vi.fn(), + handleOpenIPV6Details: vi.fn(), + openRemoveIPDialog: vi.fn(), + openRemoveIPRangeDialog: vi.fn(), }; describe('LinodeIPAddressRow', () => { diff --git a/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx b/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx index ff175d6af50..282cb9880b9 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx @@ -62,7 +62,7 @@ describe('PlanSelection (table, desktop)', () => { jest.fn()} + onSelect={() => vi.fn()} type={mockPlan} /> )