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-8017] - Support Linodes in Distributed Compute Regions on Image Create #10544

Merged
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10544-added-1717530161965.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Informational notice about capturing an image from a Linode in a distributed compute region ([#10544](https://github.com/linode/manager/pull/10544))
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding unit tests for this component! πŸ™ŒπŸ½

Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { userEvent } from '@testing-library/user-event';
import React from 'react';

import {
imageFactory,
linodeDiskFactory,
linodeFactory,
regionFactory,
} from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { CreateImageTab } from './CreateImageTab';

describe('CreateImageTab', () => {
it('should render fields, titles, and buttons in their default state', () => {
const { getByLabelText, getByText } = renderWithTheme(<CreateImageTab />);

expect(getByText('Select Linode & Disk')).toBeVisible();

expect(getByLabelText('Linode')).toBeVisible();

const diskSelect = getByLabelText('Disk');

expect(diskSelect).toBeVisible();
expect(diskSelect).toBeDisabled();

expect(getByText('Select a Linode to see available disks')).toBeVisible();

expect(getByText('Image Details')).toBeVisible();

expect(getByLabelText('Label')).toBeVisible();
expect(getByLabelText('Add Tags')).toBeVisible();
expect(getByLabelText('Description')).toBeVisible();

const submitButton = getByText('Create Image').closest('button');

expect(submitButton).toBeVisible();
expect(submitButton).toBeEnabled();
});

it('should render client side validation errors', async () => {
const { getByText } = renderWithTheme(<CreateImageTab />);

const submitButton = getByText('Create Image').closest('button');

await userEvent.click(submitButton!);

expect(getByText('Disk is required.')).toBeVisible();
});

it('should allow the user to select a disk and submit the form', async () => {
const linode = linodeFactory.build();
const disk = linodeDiskFactory.build();
const image = imageFactory.build();

server.use(
http.get('*/v4/linode/instances', () => {
return HttpResponse.json(makeResourcePage([linode]));
}),
http.get('*/v4/linode/instances/:id/disks', () => {
return HttpResponse.json(makeResourcePage([disk]));
}),
http.post('*/v4/images', () => {
return HttpResponse.json(image);
})
);

const {
findByText,
getByLabelText,
getByText,
queryByText,
} = renderWithTheme(<CreateImageTab />);

const linodeSelect = getByLabelText('Linode');

await userEvent.click(linodeSelect);

const linodeOption = await findByText(linode.label);

await userEvent.click(linodeOption);

const diskSelect = getByLabelText('Disk');

// Once a Linode is selected, the Disk select should become enabled
expect(diskSelect).toBeEnabled();
expect(queryByText('Select a Linode to see available disks')).toBeNull();

await userEvent.click(diskSelect);

const diskOption = await findByText(disk.label);

await userEvent.click(diskOption);

const submitButton = getByText('Create Image').closest('button');

await userEvent.click(submitButton!);

// Verify success toast shows
await findByText('Image scheduled for creation.');
});

it('should render a notice if the user selects a Linode in a distributed compute region', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Unit test for this PR's addition

const region = regionFactory.build({ site_type: 'distributed' });
const linode = linodeFactory.build({ region: region.id });

server.use(
http.get('*/v4/linode/instances', () => {
return HttpResponse.json(makeResourcePage([linode]));
}),
http.get('*/v4/linode/instances/:id', () => {
return HttpResponse.json(linode);
}),
http.get('*/v4/regions', () => {
return HttpResponse.json(makeResourcePage([region]));
})
);

const { findByText, getByLabelText } = renderWithTheme(<CreateImageTab />);

const linodeSelect = getByLabelText('Linode');

await userEvent.click(linodeSelect);

const linodeOption = await findByText(linode.label);

await userEvent.click(linodeOption);

// Verify distributed compute region notice renders
await findByText(
'This Linode is in a distributed compute region. Images captured from this Linode will be stored in the closest core site.'
);

// Verify helper text renders
await findByText(
`Image will be stored in the closest core site to (${linode.region})`
);
});

it('should render an encryption notice if disk encryption is enabled and the Linode is not in a distributed compute region', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Unit test for @dwiley-akamai 's change in #10521

Copy link
Member Author

@bnussman-akamai bnussman-akamai Jun 4, 2024

Choose a reason for hiding this comment

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

Oops, I just realized you covered this in a cypress test already. I'll keep it here anyway unless there are any objections.

const region = regionFactory.build({ site_type: 'core' });
const linode = linodeFactory.build({ region: region.id });

server.use(
http.get('*/v4/linode/instances', () => {
return HttpResponse.json(makeResourcePage([linode]));
}),
http.get('*/v4/linode/instances/:id', () => {
return HttpResponse.json(linode);
}),
http.get('*/v4/regions', () => {
return HttpResponse.json(makeResourcePage([region]));
})
);

const { findByText, getByLabelText } = renderWithTheme(<CreateImageTab />, {
flags: { linodeDiskEncryption: true },
});

const linodeSelect = getByLabelText('Linode');

await userEvent.click(linodeSelect);

const linodeOption = await findByText(linode.label);

await userEvent.click(linodeOption);

// Verify encryption notice renders
await findByText('Virtual Machine Images are not encrypted.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,44 @@ export const CreateImageTab = () => {

const isRawDisk = selectedDisk?.filesystem === 'raw';

/*
We only want to display the notice about disk encryption if:
1. the Disk Encryption feature is enabled
2. the selected linode is not in an Edge region
*/
const { data: regionsData } = useRegionsQuery();

const { data: linode } = useLinodeQuery(
selectedLinodeId ?? -1,
Boolean(selectedLinodeId) && isDiskEncryptionFeatureEnabled
selectedLinodeId !== null
);

const linodeIsInDistributedRegion = getIsDistributedRegion(
regionsData ?? [],
linode?.region ?? ''
);

/*
We only want to display the notice about disk encryption if:
1. the Disk Encryption feature is enabled
2. a linode is selected
2. the selected linode is not in an Edge region
*/
const showDiskEncryptionWarning =
isDiskEncryptionFeatureEnabled &&
selectedLinodeId !== null &&
!linodeIsInDistributedRegion;

const linodeSelectHelperText = [
{
show: linodeIsInDistributedRegion,
text: `Image will be stored in the closest core site to (${linode?.region})`,
},
{
show: grants?.linode.some((grant) => grant.permissions === 'read_only'),
text:
'You can only create Images from Linodes you have read/write access to.',
},
]
.filter((item) => item.show)
.map((item) => item.text)
.join('. ');

return (
<form onSubmit={onSubmit}>
<Stack spacing={2}>
Expand All @@ -135,7 +156,7 @@ export const CreateImageTab = () => {
variant="error"
/>
)}
<Stack spacing={1}>
<Stack spacing={2}>
<Typography variant="h2">Select Linode & Disk</Typography>
<Typography sx={{ maxWidth: { md: '80%', sm: '100%' } }}>
By default, Linode images are limited to 6144 MB of data per disk.
Expand All @@ -153,6 +174,12 @@ export const CreateImageTab = () => {
created from a raw disk or a disk that&rsquo;s formatted using a
custom file system.
</Typography>
{linodeIsInDistributedRegion && (
<Notice variant="info">
This Linode is in a distributed compute region. Images captured
from this Linode will be stored in the closest core site.
</Notice>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we increase the margins above and below this notice to match the mockups?

This PR Figma mockup
Screenshot 2024-06-06 at 5 02 30β€―PM Screenshot 2024-06-06 at 5 02 07β€―PM

Copy link
Member Author

Choose a reason for hiding this comment

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

I increased overall spacing in ba513a7, let me know if that looks okay to you! πŸ‘οΈ

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good, thanks!

<LinodeSelect
getOptionDisabled={
grants
Expand All @@ -164,35 +191,25 @@ export const CreateImageTab = () => {
)
: undefined
}
helperText={
grants?.linode.some(
(grant) => grant.permissions === 'read_only'
)
? 'You can only create Images from Linodes you have read/write access to.'
: undefined
}
onSelectionChange={(linode) => {
setSelectedLinodeId(linode?.id ?? null);
if (linode === null) {
resetField('disk_id');
}
}}
disabled={isImageCreateRestricted}
helperText={linodeSelectHelperText}
noMarginTop
required
value={selectedLinodeId}
/>
{isDiskEncryptionFeatureEnabled &&
!linodeIsInDistributedRegion &&
selectedLinodeId !== null && (
<Notice variant="warning">
<Typography
sx={(theme) => ({ fontFamily: theme.font.normal })}
>
{DISK_ENCRYPTION_IMAGES_CAVEAT_COPY}
</Typography>
</Notice>
)}
{showDiskEncryptionWarning && (
<Notice variant="warning">
<Typography sx={(theme) => ({ fontFamily: theme.font.normal })}>
{DISK_ENCRYPTION_IMAGES_CAVEAT_COPY}
</Typography>
</Notice>
)}
<Controller
render={({ field, fieldState }) => (
<Autocomplete
Expand Down Expand Up @@ -235,7 +252,7 @@ export const CreateImageTab = () => {
</Stack>
</Paper>
<Paper>
<Stack spacing={1}>
<Stack spacing={2}>
<Typography variant="h2">Image Details</Typography>
<Controller
render={({ field, fieldState }) => (
Expand Down
Loading