Skip to content

Commit

Permalink
feat: [M3-7272] - Disable Public IP Address for VPC only Linode (#9899)
Browse files Browse the repository at this point in the history
## Description 📝
Visually disable Public IP addresses for VPC-only Linodes and add tooltip explanation.

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.

## Changes  🔄
- Disable the Public IP Address column in the Linodes landing table
- Disable the Public IP Addresses and Access sections in the Linode's details page
- Disable the Public IPv4 row in the Linode's details -> Network tab -> IP Addresses table
- Refactored components into their own file and added unit testing
- Created custom hook `useVPCConfigInterface` to share logic

## How to test 🧪

### Prerequisites
- Ensure your account has vpc customer tags
- Have a VPC-only Linode
  - You can either create a Linode and add a VPC or edit a Linode's config so that the primary interface is VPC. (make sure the `assign a public IPv4 address` checkbox is unchecked)

### Verification steps 
For the VPC-only Linode
- The Public IP Address column in the Linodes landing table should be disabled and there should be a tooltip
- The Public IP Addresses and Access sections in the Linode's details page should be disabled and there should be a tooltip
- The Public IPv4 row in the Linode's details -> Network tab -> IP Addresses table should be disabled and there should be a tooltip
  • Loading branch information
hana-akamai authored Nov 17, 2023
1 parent 1e0ea08 commit f465b7d
Show file tree
Hide file tree
Showing 21 changed files with 697 additions and 347 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Disable Public IP Address for VPC-only Linodes ([#9899](https://github.com/linode/manager/pull/9899))
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
9 changes: 8 additions & 1 deletion packages/manager/src/components/Button/StyledActionButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
}),
}));
55 changes: 41 additions & 14 deletions packages/manager/src/components/CopyTooltip/CopyTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -38,7 +43,14 @@ export interface CopyTooltipProps {

export const CopyTooltip = (props: CopyTooltipProps) => {
const [copied, setCopied] = React.useState<boolean>(false);
const { className, copyableText, onClickCallback, placement, text } = props;
const {
className,
copyableText,
disabled,
onClickCallback,
placement,
text,
} = props;

const handleIconClick = () => {
setCopied(true);
Expand All @@ -49,30 +61,38 @@ export const CopyTooltip = (props: CopyTooltipProps) => {
}
};

const CopyButton = (
<StyledCopyButton
aria-label={`Copy ${text} to clipboard`}
className={className}
data-qa-copy-btn
name={text}
onClick={handleIconClick}
type="button"
{...props}
>
{copyableText ? text : <FileCopy />}
</StyledCopyButton>
);

if (disabled) {
return CopyButton;
}

return (
<Tooltip
className="copy-tooltip"
data-qa-copied
placement={placement ?? 'top'}
title={copied ? 'Copied!' : 'Copy'}
>
<StyledCopyTooltipButton
aria-label={`Copy ${text} to clipboard`}
className={className}
data-qa-copy-btn
name={text}
onClick={handleIconClick}
type="button"
{...props}
>
{copyableText ? text : <FileCopy />}
</StyledCopyTooltipButton>
{CopyButton}
</Tooltip>
);
};

const StyledCopyTooltipButton = styled('button', {
label: 'StyledCopyTooltipButton',
const StyledCopyButton = styled('button', {
label: 'StyledCopyButton',
shouldForwardProp: omittedProps(['copyableText', 'text']),
})<Omit<CopyTooltipProps, 'text'>>(({ theme, ...props }) => ({
'& svg': {
Expand Down Expand Up @@ -102,4 +122,11 @@ const StyledCopyTooltipButton = styled('button', {
font: 'inherit',
padding: 0,
}),
...(props.disabled && {
color:
theme.palette.mode === 'dark'
? theme.color.grey6
: theme.color.disabledText,
cursor: 'default',
}),
}));
8 changes: 6 additions & 2 deletions packages/manager/src/components/TableRow/TableRow.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
}),
}));

Expand Down
53 changes: 53 additions & 0 deletions packages/manager/src/features/Linodes/AccessTable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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 } 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(
<AccessTable
isVPCOnlyLinode={true}
rows={[{ text: linode.ipv4[0] }, { text: linode.ipv4[1] }]}
title={'Public IP Addresses'}
/>
);

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', () => {
const { getAllByRole } = renderWithTheme(
<AccessTable
isVPCOnlyLinode={false}
rows={[{ text: linode.ipv4[0] }, { text: linode.ipv4[1] }]}
title={'Public IP Addresses'}
/>
);

const copyButtons = getAllByRole('button');

copyButtons.forEach((copyButton) => {
expect(copyButton).not.toBeDisabled();
});
});
});
84 changes: 84 additions & 0 deletions packages/manager/src/features/Linodes/AccessTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { PublicIpsUnassignedTooltip } from 'src/features/Linodes/PublicIpsUnassignedTooltip';

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;
}

export const AccessTable = React.memo((props: AccessTableProps) => {
const { footer, gridProps, isVPCOnlyLinode, rows, sx, title } = props;
return (
<Grid
container
direction="column"
md={6}
spacing={1}
sx={sx}
{...gridProps}
>
<StyledColumnLabelGrid>
{title}{' '}
{isVPCOnlyLinode &&
title.includes('Public IP Address') &&
PublicIpsUnassignedTooltip}
</StyledColumnLabelGrid>
<StyledTableGrid>
<StyledTable>
<TableBody>
{rows.map((thisRow) => {
return thisRow.text ? (
<StyledTableRow disabled={isVPCOnlyLinode} key={thisRow.text}>
{thisRow.heading ? (
<TableCell component="th" scope="row">
{thisRow.heading}
</TableCell>
) : null}
<StyledTableCell>
<StyledGradientDiv>
<CopyTooltip
copyableText
disabled={isVPCOnlyLinode}
text={thisRow.text}
/>
</StyledGradientDiv>
<StyledCopyTooltip
disabled={isVPCOnlyLinode}
text={thisRow.text}
/>
</StyledTableCell>
</StyledTableRow>
) : null;
})}
</TableBody>
</StyledTable>
{footer ? <Grid sx={{ padding: 0 }}>{footer}</Grid> : null}
</StyledTableGrid>
</Grid>
);
});
Loading

0 comments on commit f465b7d

Please sign in to comment.