Skip to content

Commit

Permalink
feat: [M3-7345] – Unrecommended Linode configuration indicators on VP…
Browse files Browse the repository at this point in the history
…C Detail page (#9914)
  • Loading branch information
dwiley-akamai authored Nov 28, 2023
1 parent 8b8fd8b commit 9f1f4dd
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Indicate unrecommended Linode configurations on VPC Detail page ([#9914](https://github.com/linode/manager/pull/9914))
2 changes: 1 addition & 1 deletion packages/manager/src/components/Notice/Notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface NoticeProps extends Grid2Props {
- Appear within the page or modal
- Might be triggered by user action
- Typically used to alert the user to a new service, limited availability, or a potential consequence of the action being taken
- Consider using a [Dismissible Banner](/docs/components-notifications-dismissible-banners--beta-banner) if it’s not critical information
- Consider using a [Dismissible Banner](/docs/components-notifications-dismissible-banners--beta-banner) if its not critical information
## Types of Notices:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { styled } from '@mui/material/styles';

import Warning from 'src/assets/icons/warning.svg';
import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';

Expand Down Expand Up @@ -38,3 +39,9 @@ export const StyledTableHeadCell = styled(TableCell, {
borderBottom: `1px solid ${theme.borderColors.borderTable} !important`,
borderTop: 'none !important',
}));

export const StyledWarningIcon = styled(Warning, {
label: 'StyledWarningIcon',
})(({ theme }) => ({
fill: theme.color.yellow,
}));
123 changes: 106 additions & 17 deletions packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { fireEvent } from '@testing-library/react';
import { waitForElementToBeRemoved } from '@testing-library/react';
import { waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import * as React from 'react';
import { QueryClient } from 'react-query';

import { firewallFactory } from 'src/factories';
import { LinodeConfigInterfaceFactoryWithVPC } from 'src/factories/linodeConfigInterfaceFactory';
import {
LinodeConfigInterfaceFactory,
LinodeConfigInterfaceFactoryWithVPC,
firewallFactory,
subnetAssignedLinodeDataFactory,
subnetFactory,
} from 'src/factories';
import { linodeConfigFactory } from 'src/factories/linodeConfigs';
import { linodeFactory } from 'src/factories/linodes';
import { makeResourcePage } from 'src/mocks/serverHandlers';
Expand All @@ -15,6 +20,7 @@ import {
wrapWithTableBody,
} from 'src/utilities/testHelpers';

import { WARNING_ICON_UNRECOMMENDED_CONFIG } from '../constants';
import { SubnetLinodeRow } from './SubnetLinodeRow';

const queryClient = new QueryClient();
Expand All @@ -25,23 +31,33 @@ afterEach(() => {
});

const loadingTestId = 'circle-progress';
const mockFirewall0 = 'mock-firewall-0';

describe('SubnetLinodeRow', () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });

server.use(
rest.get('*/linodes/instances/:linodeId', (req, res, ctx) => {
return res(ctx.json(linodeFactory1));
}),
rest.get('*/linode/instances/:id/firewalls', (req, res, ctx) => {
return res(
ctx.json(
makeResourcePage(
firewallFactory.buildList(1, { label: mockFirewall0 })
)
)
);
})
);

const linodeFactory2 = linodeFactory.build({ id: 2, label: 'linode-2' });

const handleUnassignLinode = vi.fn();

it('should display linode label, reboot status, VPC IPv4 address, associated firewalls, and Reboot and Unassign buttons', async () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });
server.use(
rest.get('*/linodes/instances/:linodeId', (req, res, ctx) => {
return res(ctx.json(linodeFactory1));
}),
rest.get('*/linode/instances/:id/firewalls', (req, res, ctx) => {
return res(
ctx.json(
makeResourcePage(
firewallFactory.buildList(1, { label: 'mock-firewall-0' })
)
)
);
}),
rest.get('*/instances/*/configs', async (req, res, ctx) => {
const configs = linodeConfigFactory.buildList(3);
return res(ctx.json(makeResourcePage(configs)));
Expand Down Expand Up @@ -82,7 +98,7 @@ describe('SubnetLinodeRow', () => {
);

getAllByText('10.0.0.0');
getByText('mock-firewall-0');
getByText(mockFirewall0);

const rebootLinodeButton = getAllByRole('button')[1];
expect(rebootLinodeButton).toHaveTextContent('Reboot');
Expand All @@ -97,6 +113,7 @@ describe('SubnetLinodeRow', () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });
const vpcInterface = LinodeConfigInterfaceFactoryWithVPC.build({
active: true,
primary: true,
});
server.use(
rest.get('*/linodes/instances/:linodeId', (req, res, ctx) => {
Expand All @@ -106,7 +123,7 @@ describe('SubnetLinodeRow', () => {
return res(
ctx.json(
makeResourcePage(
firewallFactory.buildList(1, { label: 'mock-firewall-0' })
firewallFactory.buildList(1, { label: mockFirewall0 })
)
)
);
Expand Down Expand Up @@ -158,4 +175,76 @@ describe('SubnetLinodeRow', () => {
fireEvent.click(unassignLinodeButton);
expect(handleUnassignLinode).toHaveBeenCalled();
});

it('should display a warning icon for Linodes using unrecommended configuration profiles', async () => {
const publicInterface = LinodeConfigInterfaceFactory.build({
active: true,
id: 5,
ipam_address: null,
primary: true,
purpose: 'public',
});

const vpcInterface = LinodeConfigInterfaceFactory.build({
active: true,
id: 10,
ipam_address: null,
purpose: 'vpc',
subnet_id: 1,
});

const configurationProfile = linodeConfigFactory.build({
interfaces: [publicInterface, vpcInterface],
});

const subnet = subnetFactory.build({
id: 1,
linodes: [
subnetAssignedLinodeDataFactory.build({
id: 1,
interfaces: [
{
active: true,
id: 5,
},
{
active: true,
id: 10,
},
],
}),
],
});

server.use(
rest.get('*/instances/*/configs', async (req, res, ctx) => {
return res(ctx.json(makeResourcePage([configurationProfile])));
})
);

const { getByTestId } = renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={vi.fn()}
handleUnassignLinode={handleUnassignLinode}
linodeId={linodeFactory2.id}
subnet={subnet}
subnetId={subnet.id}
/>
),
{
queryClient,
}
);

// Loading state should render
expect(getByTestId(loadingTestId)).toBeInTheDocument();
await waitForElementToBeRemoved(getByTestId(loadingTestId));

const warningIcon = getByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG);

await waitFor(() => {
expect(warningIcon).toBeInTheDocument();
});
});
});
50 changes: 47 additions & 3 deletions packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ import {
} from 'src/queries/linodes/linodes';
import { capitalizeAllWords } from 'src/utilities/capitalize';

import { VPC_REBOOT_MESSAGE } from '../constants';
import { getSubnetInterfaceFromConfigs } from '../utils';
import {
NETWORK_INTERFACES_GUIDE_URL,
VPC_REBOOT_MESSAGE,
WARNING_ICON_UNRECOMMENDED_CONFIG,
} from '../constants';
import {
hasUnrecommendedConfiguration as _hasUnrecommendedConfiguration,
getSubnetInterfaceFromConfigs,
} from '../utils';
import {
StyledActionTableCell,
StyledTableCell,
StyledTableHeadCell,
StyledTableRow,
StyledWarningIcon,
} from './SubnetLinodeRow.styles';

import type { Subnet } from '@linode/api-v4/lib/vpcs/types';
Expand Down Expand Up @@ -71,6 +79,11 @@ export const SubnetLinodeRow = (props: Props) => {
isLoading: configsLoading,
} = useAllLinodeConfigsQuery(linodeId);

const hasUnrecommendedConfiguration = _hasUnrecommendedConfiguration(
configs ?? [],
subnet?.id ?? -1
);

// If the Linode's status is running, we want to check if its interfaces associated with this subnet have become active so
// that we can determine if it needs a reboot or not. So, we need to invalidate the linode configs query to get the most up to date information.
React.useEffect(() => {
Expand Down Expand Up @@ -113,6 +126,37 @@ export const SubnetLinodeRow = (props: Props) => {
);
}

const linkifiedLinodeLabel = (
<Link to={`/linodes/${linode.id}`}>{linode.label}</Link>
);

const labelCell = hasUnrecommendedConfiguration ? (
<Box
data-testid={WARNING_ICON_UNRECOMMENDED_CONFIG}
sx={{ alignItems: 'center', display: 'flex' }}
>
<TooltipIcon
text={
<Typography>
This Linode is using an unrecommended configuration profile. Update
its configuration profile to avoid connectivity issues. Read our{' '}
<Link to={NETWORK_INTERFACES_GUIDE_URL}>
Configuration Profiles
</Link>{' '}
guide for more information.
</Typography>
}
icon={<StyledWarningIcon />}
interactive
status="other"
sxTooltipIcon={{ paddingLeft: 0 }}
/>
{linkifiedLinodeLabel}
</Box>
) : (
linkifiedLinodeLabel
);

const iconStatus = getLinodeIconStatus(linode.status);
const isRunning = linode.status === 'running';
const isOffline = linode.status === 'stopped' || linode.status === 'offline';
Expand All @@ -130,7 +174,7 @@ export const SubnetLinodeRow = (props: Props) => {
return (
<StyledTableRow>
<StyledTableCell component="th" scope="row" sx={{ paddingLeft: 6 }}>
<Link to={`/linodes/${linode.id}`}>{linode.label}</Link>
{labelCell}
</StyledTableCell>
<StyledTableCell statusCell>
<StatusIcon
Expand Down
3 changes: 1 addition & 2 deletions packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { EntityHeader } from 'src/components/EntityHeader/EntityHeader';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { LandingHeader } from 'src/components/LandingHeader';
import { VPC_FEEDBACK_FORM_URL } from 'src/features/VPCs/constants';
import { VPC_LABEL } from 'src/features/VPCs/constants';
import { VPC_FEEDBACK_FORM_URL, VPC_LABEL } from 'src/features/VPCs/constants';
import { useRegionsQuery } from 'src/queries/regions';
import { useVPCQuery } from 'src/queries/vpcs';
import { truncate } from 'src/utilities/truncate';
Expand Down
9 changes: 9 additions & 0 deletions packages/manager/src/features/VPCs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ export const VPC_FEEDBACK_FORM_URL =
export const VPC_REBOOT_MESSAGE =
'The VPC configuration has been updated and the Linode needs to be rebooted.';

export const NETWORK_INTERFACES_GUIDE_URL =
'https://www.linode.com/docs/products/compute/compute-instances/guides/configuration-profiles/';

export const UNRECOMMENDED_CONFIGURATION_PREFERENCE_KEY =
'not-recommended-configuration';

export const WARNING_ICON_UNRECOMMENDED_CONFIG =
'warning-icon-for-unrecommended-config';

// Linode Config dialog helper text for unrecommended configurations
export const LINODE_UNREACHABLE_HELPER_TEXT =
'This network configuration is not recommended. The Linode will not be reachable or be able to reach Linodes in the other subnets of the VPC. We recommend selecting VPC as the primary interface and checking the “Assign a public IPv4 address for this Linode” checkbox.';
Expand Down
Loading

0 comments on commit 9f1f4dd

Please sign in to comment.