Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Apply UX and user feedback for linode interfaces feature in Create linode page ([#13281](https://github.com/linode/manager/pull/13281))
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useRegionsQuery } from '@linode/queries';
import { Divider, Notice, Paper, Stack, Typography } from '@linode/ui';
import React, { useMemo } from 'react';
import { useWatch } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';

import { Backups } from './Backups';
import { PrivateIP } from './PrivateIP';

import type { CreateLinodeRequest } from '@linode/api-v4';

export const Addons = () => {
const { setValue } = useFormContext<CreateLinodeRequest>();
const [regionId, interfaceGeneration] = useWatch<
CreateLinodeRequest,
['region', 'interface_generation']
Expand All @@ -26,6 +27,11 @@ export const Addons = () => {

const shouldShowPrivateIP = interfaceGeneration !== 'linode';

// Clean up private IP value when the option is hidden
if (!shouldShowPrivateIP) {
setValue('private_ip', false);
}

return (
<Paper data-qa-add-ons>
<Stack spacing={2}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const Backups = () => {
<React.Fragment>
Three backup slots are executed and rotated automatically: a
daily backup, a 2-7 day old backup, and an 8-14 day old backup.
Plans are priced according to the Linode plan selected above.
Pricing is based on the selected Linode plan.
</React.Fragment>
)}
</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useRegionsQuery } from '@linode/queries';
import {
Checkbox,
FormControlLabel,
NewFeatureChip,
Notice,
Stack,
Typography,
Expand Down Expand Up @@ -59,14 +58,12 @@ export const PrivateIP = () => {
Lets you connect with other Linodes in the same region over the data
center&apos;s private network, without using a public IPv4 address.
</Typography>
<Notice variant="tip">
<Stack alignItems="center" direction="row" spacing={1}>
<NewFeatureChip />
<Typography>
You can use VPC for private networking instead. NodeBalancers
now connect to backend nodes without a private IPv4 address.
</Typography>
</Stack>
<Notice marginTop="12px !important" variant="tip">
<Typography>
You can now establish network isolation and connections to
NodeBalancer backends through VPC. We recommend using VPC instead
of Private IPs.
</Typography>
</Notice>
</Stack>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

Expand All @@ -7,36 +8,34 @@ import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { InterfaceGeneration } from './InterfaceGeneration';

const getAccountSettingsAPI = '*/v4*/account/settings';

describe('InterfaceGeneration', () => {
it('disables the radios if the account setting enforces linode_only interfaces', async () => {
const accountSettings = accountSettingsFactory.build({
interfaces_for_new_linodes: 'linode_only',
});

server.use(
http.get('*/v4*/account/settings', () => {
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const { findByText, getAllByRole, getByText } =
renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});

const button = getByText('Network Interface Type');

// Expand the "Show More"
await userEvent.click(button);
const { getAllByRole, findByRole } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});

// Hover to check for the tooltip
await userEvent.hover(getByText('Network Interface Type'));
// Wait for the tooltip icon to appear (indicating the disabled state)
await findByRole('button', {
name: 'Your account administrator has enforced that all new Linodes are created with Linode interfaces.',
});

await findByText(
'Your account administrator has enforced that all new Linodes are created with Linode interfaces.'
);
// Verify both radio buttons are disabled
const radios = getAllByRole('radio');
expect(radios).toHaveLength(2);

for (const radio of getAllByRole('radio')) {
for (const radio of radios) {
expect(radio).toBeDisabled();
}
});
Expand All @@ -47,34 +46,140 @@ describe('InterfaceGeneration', () => {
});

server.use(
http.get('*/v4*/account/settings', () => {
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const { findByText, getAllByRole, getByText } =
renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});
const { getAllByRole, findByRole } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});

// Wait for the tooltip icon to appear (indicating the disabled state)
await findByRole('button', {
name: 'Your account administrator has enforced that all new Linodes are created with legacy configuration interfaces.',
});

const button = getByText('Network Interface Type');
// Verify both radio buttons are disabled
const radios = getAllByRole('radio');
expect(radios).toHaveLength(2);

// Expand the "Show More"
await userEvent.click(button);
for (const radio of radios) {
expect(radio).toBeDisabled();
}
});

// Hover to check for the tooltip
await userEvent.hover(getByText('Network Interface Type'));
it('enables the radios when account settings allow both interface types', async () => {
const accountSettings = accountSettingsFactory.build({
interfaces_for_new_linodes: 'linode_default_but_legacy_config_allowed',
});

await findByText(
'Your account administrator has enforced that all new Linodes are created with legacy configuration interfaces.'
server.use(
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const radios = getAllByRole('radio');
const { getAllByRole, queryByRole } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});

// Wait for radios to render
await waitFor(() => {
// Verify no disabled tooltip appears
expect(
queryByRole('button', {
name: /Your account administrator has enforced/,
})
).toBeNull();
});

// Verify both radio buttons are enabled
const radios = getAllByRole('radio');
expect(radios).toHaveLength(2);

for (const radio of radios) {
expect(radio).toBeDisabled();
expect(radio).toBeEnabled();
}
});

it('defaults to linode interface when value is not set', async () => {
const accountSettings = accountSettingsFactory.build({
interfaces_for_new_linodes: 'linode_default_but_legacy_config_allowed',
});

server.use(
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const { getByDisplayValue } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
useFormOptions: {
defaultValues: {
interface_generation: null,
},
},
});

// Wait for component to render
await waitFor(() => {
// Verify linode radio is selected by default
expect(getByDisplayValue('linode')).toBeChecked();
});
});

it('allows user to select legacy config interface when enabled', async () => {
const accountSettings = accountSettingsFactory.build({
interfaces_for_new_linodes: 'legacy_config_default_but_linode_allowed',
});

server.use(
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const { getByDisplayValue } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
useFormOptions: {
defaultValues: {
interface_generation: 'linode',
},
},
});

const legacyConfigRadio = getByDisplayValue('legacy_config');

// Click on legacy config radio
await userEvent.click(legacyConfigRadio);

// Verify legacy config is now selected
expect(legacyConfigRadio).toBeChecked();
expect(getByDisplayValue('linode')).not.toBeChecked();
});

it('displays correct labels for both interface types', () => {
const accountSettings = accountSettingsFactory.build({
interfaces_for_new_linodes: 'linode_default_but_legacy_config_allowed',
});

server.use(
http.get(getAccountSettingsAPI, () => {
return HttpResponse.json(accountSettings);
})
);

const { getByText } = renderWithThemeAndHookFormContext({
component: <InterfaceGeneration />,
});

// Verify interface type labels
expect(getByText('Linode Interfaces (Recommended)')).toBeVisible();
expect(
getByText('Configuration Profile Interfaces (Legacy)')
).toBeVisible();
expect(getByText('Network Interface Type')).toBeVisible();
});
});
Loading