Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-7345] – Unrecommended Linode configuration indicators on VPC Detail page #9914

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 it’s not critical information
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes this issue at design.linode.com:

Screenshot 2023-11-15 at 9 16 44 PM


## Types of Notices:

Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autoformatting from the linter

Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ export const onUploadProgressFactory = (
) => (progressEvent: AxiosProgressEvent) => {
dispatch({
data: {
percentComplete: (progressEvent.loaded / (progressEvent.total ?? 1)) * 100,
percentComplete:
(progressEvent.loaded / (progressEvent.total ?? 1)) * 100,
},
filesToUpdate: [fileName],
type: 'UPDATE_FILES',
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();
});
});
});
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
5 changes: 2 additions & 3 deletions packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import { useParams } from 'react-router-dom';
import { Box } from 'src/components/Box';
import { StyledLinkButton } from 'src/components/Button/StyledLinkButton';
import { CircleProgress } from 'src/components/CircleProgress/CircleProgress';
import { DismissibleBanner } from 'src/components/DismissibleBanner';
import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner';
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 @@ -30,3 +30,12 @@ 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';
Loading