Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-12428-fixed-1750852226481.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Fix console error in Create NodeBalancer page and columns misalignment in Subnet NodeBalancers Table ([#12428](https://github.com/linode/manager/pull/12428))
9 changes: 6 additions & 3 deletions packages/manager/src/features/NodeBalancers/VPCPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,12 @@ export const VPCPanel = (props: Props) => {
placeholder="Subnet"
textFieldProps={{
helperText: (
<Typography mb={2}>
The VPC subnet for this NodeBalancer.
</Typography>
<Box component="span" mb={2} sx={{ display: 'block' }}>
<Typography component="span" variant="body1">
Select a subnet in which to allocate the VPC CIDR for
the NodeBalancer.
</Typography>
</Box>
),
helperTextPosition: 'top',
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { linodeFactory } from '@linode/utilities';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

import { subnetFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { SubnetLinodeActionMenu } from './SubnetLinodeActionMenu';

const props = {
handlePowerActionsLinode: vi.fn(),
handleUnassignLinode: vi.fn(),
isVPCLKEEnterpriseCluster: false,
linode: linodeFactory.build({ label: 'linode-1' }),
subnet: subnetFactory.build({ label: 'subnet-1' }),
isOffline: false,
isRebootNeeded: false,
showPowerButton: true,
};

describe('SubnetActionMenu', () => {
it('should render the subnet action menu', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<SubnetLinodeActionMenu {...props} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);
getByText('Power Off');
getByText('Unassign Linode');
});

it('should allow the reboot button to be clicked', async () => {
const { getByLabelText, getByText, queryByLabelText } = renderWithTheme(
<SubnetLinodeActionMenu {...props} isRebootNeeded={true} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);

const rebootButton = getByText('Reboot');
await userEvent.click(rebootButton);
expect(props.handlePowerActionsLinode).toHaveBeenCalled();
const tooltipText = queryByLabelText(
'Linodes assigned to a subnet must be unassigned before the subnet can be deleted.'
);
expect(tooltipText).not.toBeInTheDocument();
});

it('should allow the Power Off button to be clicked', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<SubnetLinodeActionMenu {...props} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);

const powerOffButton = getByText('Power Off');
await userEvent.click(powerOffButton);
expect(props.handlePowerActionsLinode).toHaveBeenCalled();
});

it('should allow the Power On button to be clicked', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<SubnetLinodeActionMenu {...props} isOffline={true} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);

const powerOnButton = getByText('Power On');
await userEvent.click(powerOnButton);
expect(props.handlePowerActionsLinode).toHaveBeenCalled();
});

it('should allow the Unassign Linode button to be clicked', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<SubnetLinodeActionMenu {...props} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);

const unassignButton = getByText('Unassign Linode');
await userEvent.click(unassignButton);
expect(props.handleUnassignLinode).toHaveBeenCalled();
});

it('should disable action buttons if isVPCLKEEnterpriseCluster is true', async () => {
const updatedProps = { ...props, isVPCLKEEnterpriseCluster: true };
const { getByLabelText, getAllByRole } = renderWithTheme(
<SubnetLinodeActionMenu {...updatedProps} />
);
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet subnet-1`
);
await userEvent.click(actionMenu);

const actionButtons = getAllByRole('menuitem');
actionButtons.forEach((button) =>
expect(button).toHaveAttribute('aria-disabled', 'true')
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as React from 'react';

import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';

import type { Linode, Subnet } from '@linode/api-v4';
import type { Action as ActionMenuAction } from 'src/components/ActionMenu/ActionMenu';
import type { Action as PowerAction } from 'src/features/Linodes/PowerActionsDialogOrDrawer';

interface SubnetLinodeActionHandlers {
handlePowerActionsLinode: (
linode: Linode,
action: PowerAction,
subnet: Subnet
) => void;
handleUnassignLinode: (linode: Linode, subnet?: Subnet) => void;
}

interface Props extends SubnetLinodeActionHandlers {
isOffline: boolean;
isRebootNeeded: boolean;
isVPCLKEEnterpriseCluster: boolean;
linode: Linode;
showPowerButton: boolean;
subnet: Subnet;
}

export const SubnetLinodeActionMenu = (props: Props) => {
const {
handlePowerActionsLinode,
handleUnassignLinode,
isVPCLKEEnterpriseCluster,
isOffline,
isRebootNeeded,
subnet,
linode,
showPowerButton,
} = props;

const actions: ActionMenuAction[] = [];
if (isRebootNeeded) {
actions.push({
disabled: isVPCLKEEnterpriseCluster,
onClick: () => {
handlePowerActionsLinode(linode, 'Reboot', subnet);
},
title: 'Reboot',
});
}

if (showPowerButton) {
actions.push({
disabled: isVPCLKEEnterpriseCluster,
onClick: () => {
handlePowerActionsLinode(
linode,
isOffline ? 'Power On' : 'Power Off',
subnet
);
},
title: isOffline ? 'Power On' : 'Power Off',
});
}

actions.push({
disabled: isVPCLKEEnterpriseCluster,
onClick: () => {
handleUnassignLinode(linode, subnet);
},
title: 'Unassign Linode',
});

return (
<ActionMenu
actionsList={actions}
ariaLabel={`Action menu for Linodes in Subnet ${subnet.label}`}
/>
);
};
103 changes: 56 additions & 47 deletions packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('SubnetLinodeRow', () => {

it('should display linode label, reboot status, VPC IPv4 address, associated firewalls, IPv4 chip, and Reboot and Unassign buttons', async () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });
const subnetFactory1 = subnetFactory.build({ id: 1, label: 'subnet-1' });
const config = linodeConfigFactory.build({
interfaces: [linodeConfigInterfaceFactoryWithVPC.build({ id: 1 })],
});
Expand All @@ -82,20 +83,26 @@ describe('SubnetLinodeRow', () => {
const handlePowerActionsLinode = vi.fn();
const handleUnassignLinode = vi.fn();

const { getAllByRole, getAllByText, getByTestId, findByText } =
renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={handlePowerActionsLinode}
handleUnassignLinode={handleUnassignLinode}
isVPCLKEEnterpriseCluster={false}
linodeId={linodeFactory1.id}
subnet={subnetFactory.build()}
subnetId={1}
subnetInterfaces={[{ active: true, config_id: config.id, id: 1 }]}
/>
)
);
const {
getAllByRole,
getAllByText,
getByLabelText,
getByTestId,
getByText,
findByText,
} = renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={handlePowerActionsLinode}
handleUnassignLinode={handleUnassignLinode}
isVPCLKEEnterpriseCluster={false}
linodeId={linodeFactory1.id}
subnet={subnetFactory1}
subnetId={1}
subnetInterfaces={[{ active: true, config_id: config.id, id: 1 }]}
/>
)
);

// Loading states should render
expect(getByTestId(loadingTestId)).toBeInTheDocument();
Expand All @@ -113,13 +120,16 @@ describe('SubnetLinodeRow', () => {
const plusChipButton = getAllByRole('button')[1];
expect(plusChipButton).toHaveTextContent('+1');

const rebootLinodeButton = getAllByRole('button')[2];
expect(rebootLinodeButton).toHaveTextContent('Reboot');
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet ${subnetFactory1.label}`
);
await userEvent.click(actionMenu);

const rebootLinodeButton = getByText('Reboot');
await userEvent.click(rebootLinodeButton);
expect(handlePowerActionsLinode).toHaveBeenCalled();

const unassignLinodeButton = getAllByRole('button')[3];
expect(unassignLinodeButton).toHaveTextContent('Unassign Linode');
const unassignLinodeButton = getByText('Unassign Linode');
await userEvent.click(unassignLinodeButton);
expect(handleUnassignLinode).toHaveBeenCalled();
const firewall = await findByText(mockFirewall0);
Expand Down Expand Up @@ -179,6 +189,7 @@ describe('SubnetLinodeRow', () => {

it('should not display reboot linode button if the linode has all active interfaces', async () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });
const subnetFactory1 = subnetFactory.build({ id: 1, label: 'subnet-1' });
const vpcInterface = linodeConfigInterfaceFactoryWithVPC.build({
active: true,
ip_ranges: [],
Expand Down Expand Up @@ -206,21 +217,22 @@ describe('SubnetLinodeRow', () => {
const handleUnassignLinode = vi.fn();
const handlePowerActionsLinode = vi.fn();

const { getAllByRole, getByTestId } = renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={handlePowerActionsLinode}
handleUnassignLinode={handleUnassignLinode}
isVPCLKEEnterpriseCluster={false}
linodeId={linodeFactory1.id}
subnet={subnetFactory.build()}
subnetId={0}
subnetInterfaces={[
{ active: true, config_id: config.id, id: vpcInterface.id },
]}
/>
)
);
const { getAllByRole, getByTestId, getByLabelText, getByText } =
renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={handlePowerActionsLinode}
handleUnassignLinode={handleUnassignLinode}
isVPCLKEEnterpriseCluster={false}
linodeId={linodeFactory1.id}
subnet={subnetFactory1}
subnetId={0}
subnetInterfaces={[
{ active: true, config_id: config.id, id: vpcInterface.id },
]}
/>
)
);

// Loading state should render
expect(getByTestId(loadingTestId)).toBeInTheDocument();
Expand All @@ -233,14 +245,15 @@ describe('SubnetLinodeRow', () => {
`/linodes/${linodeFactory1.id}`
);

const buttons = getAllByRole('button');
expect(buttons.length).toEqual(2);
const powerOffButton = buttons[0];
expect(powerOffButton).toHaveTextContent('Power Off');
const actionMenu = getByLabelText(
`Action menu for Linodes in Subnet ${subnetFactory1.label}`
);
await userEvent.click(actionMenu);

const powerOffButton = getByText('Power Off');
await userEvent.click(powerOffButton);
expect(handlePowerActionsLinode).toHaveBeenCalled();
const unassignLinodeButton = buttons[1];
expect(unassignLinodeButton).toHaveTextContent('Unassign Linode');
const unassignLinodeButton = getByText('Unassign Linode');
await userEvent.click(unassignLinodeButton);
expect(handleUnassignLinode).toHaveBeenCalled();
});
Expand Down Expand Up @@ -296,7 +309,7 @@ describe('SubnetLinodeRow', () => {
});
});

it('should hide in-line action buttons for LKE-E Linodes', async () => {
it('should hide action-menu buttons for LKE-E Linodes', async () => {
const linodeFactory1 = linodeFactory.build({ id: 1, label: 'linode-1' });

server.use(
Expand All @@ -313,7 +326,7 @@ describe('SubnetLinodeRow', () => {
const handleUnassignLinode = vi.fn();
const handlePowerActionsLinode = vi.fn();

const { getByTestId, queryByRole } = renderWithTheme(
const { getByTestId, queryByText } = renderWithTheme(
wrapWithTableBody(
<SubnetLinodeRow
handlePowerActionsLinode={handlePowerActionsLinode}
Expand All @@ -331,13 +344,9 @@ describe('SubnetLinodeRow', () => {
expect(getByTestId(loadingTestId)).toBeInTheDocument();
await waitForElementToBeRemoved(getByTestId(loadingTestId));

const powerOffButton = queryByRole('button', {
name: 'Power Off',
});
const powerOffButton = queryByText('Power Off');
expect(powerOffButton).not.toBeInTheDocument();
const unassignLinodeButton = queryByRole('button', {
name: 'Unassign Linode',
});
const unassignLinodeButton = queryByText('Unassign Linode');
expect(unassignLinodeButton).not.toBeInTheDocument();
});

Expand Down
Loading