-
Notifications
You must be signed in to change notification settings - Fork 358
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
Changes from 6 commits
c7ed12d
a50aa80
a915553
2a104de
e0d14d1
ba513a7
8427988
87cbd10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)) |
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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unit test for @dwiley-akamai 's change in #10521 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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}> | ||
|
@@ -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. | ||
|
@@ -153,6 +174,12 @@ export const CreateImageTab = () => { | |
created from a raw disk or a disk that’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> | ||
)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! ποΈ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good, thanks! |
||
<LinodeSelect | ||
getOptionDisabled={ | ||
grants | ||
|
@@ -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 | ||
|
@@ -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 }) => ( | ||
|
There was a problem hiding this comment.
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! ππ½