Skip to content

Commit

Permalink
upcoming: [M3-8751& M3-8610] - Image Service Gen 2 final GA tweaks (#…
Browse files Browse the repository at this point in the history
…11115)

* initial work to the table

* initial work to the table

* save progress

* small changes and unit tests

* be consistant with flags

* add changeset

* use new copy for tooltip

* put icon before label

* Revert "put icon before label"

This reverts commit fa4f886.

* put icon before label

* remove assertion that is no longer needed for GA changes

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Oct 22, 2024
1 parent dec6b50 commit 7735a37
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Image Service Gen 2 final GA tweaks ([#11115](https://github.com/linode/manager/pull/11115))
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ describe('Manage Image Replicas', () => {
// Verify total size is rendered
cy.findByText(`0.1 GB`).should('be.visible'); // 100 / 1024 = 0.09765

// Verify capabilities are rendered
cy.findByText('Distributed').should('be.visible');

// Verify the number of regions is rendered and click it
cy.findByText(`${image.regions.length} Regions`)
.should('be.visible')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const ImageOption = (props: ImageOptionProps) => {
</Tooltip>
)}
{flags.metadata && data.isCloudInitCompatible && (
<Tooltip title="This image is compatible with cloud-init.">
<Tooltip title="This image supports our Metadata service via cloud-init.">
<CloudInitIcon />
</Tooltip>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('ImageOptionv2', () => {
);

expect(
getByLabelText('This image is compatible with cloud-init.')
getByLabelText('This image supports our Metadata service via cloud-init.')
).toBeVisible();
});
it('renders a distributed icon if image has the "distributed-sites" capability', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const ImageOptionv2 = ({ image, isSelected, listItemProps }: Props) => {
</Tooltip>
)}
{flags.metadata && image.capabilities.includes('cloud-init') && (
<Tooltip title="This image is compatible with cloud-init.">
<Tooltip title="This image supports our Metadata service via cloud-init.">
<div style={{ display: 'flex' }}>
<CloudInitIcon />
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' },
{ flag: 'gecko2', label: 'Gecko' },
{ flag: 'imageServiceGen2', label: 'Image Service Gen2' },
{ flag: 'imageServiceGen2Ga', label: 'Image Service Gen2 GA' },
{ flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' },
{ flag: 'objMultiCluster', label: 'OBJ Multi-Cluster' },
{ flag: 'objectStorageGen2', label: 'OBJ Gen2' },
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface Flags {
gecko2: GeckoFeatureFlag;
gpuv2: gpuV2;
imageServiceGen2: boolean;
imageServiceGen2Ga: boolean;
ipv6Sharing: boolean;
linodeDiskEncryption: boolean;
mainContentBanner: MainContentBanner;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,40 @@ describe('CreateImageTab', () => {
);
});

it('should render a notice if the user selects a Linode in a region that does not support image storage and Image Service Gen 2 GA is enabled', async () => {
const region = regionFactory.build({ capabilities: [] });
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: { imageServiceGen2: true, imageServiceGen2Ga: true },
});

const linodeSelect = getByLabelText('Linode');

await userEvent.click(linodeSelect);

const linodeOption = await findByText(linode.label);

await userEvent.click(linodeOption);

await findByText(
'This Linode’s region doesn’t support local image storage.',
{ exact: false }
);
});

it('should render an encryption notice if disk encryption is enabled and the Linode is not in a distributed compute region', async () => {
const region = regionFactory.build({ site_type: 'core' });
const linode = linodeFactory.build({ region: region.id });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useIsDiskEncryptionFeatureEnabled } from 'src/components/Encryption/uti
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { Paper } from 'src/components/Paper';
import { getIsDistributedRegion } from 'src/components/RegionSelect/RegionSelect.utils';
import { Stack } from 'src/components/Stack';
import { TagsInput } from 'src/components/TagsInput/TagsInput';
import { TextField } from 'src/components/TextField';
Expand Down Expand Up @@ -140,11 +139,20 @@ export const CreateImageTab = () => {

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

const { data: regionsData } = useRegionsQuery();
const { data: regions } = useRegionsQuery();

const linodeIsInDistributedRegion = getIsDistributedRegion(
regionsData ?? [],
selectedLinode?.region ?? ''
const selectedLinodeRegion = regions?.find(
(r) => r.id === selectedLinode?.region
);

const linodeIsInDistributedRegion =
selectedLinodeRegion?.site_type === 'distributed';

/**
* The 'Object Storage' capability indicates a region can store images
*/
const linodeRegionSupportsImageStorage = selectedLinodeRegion?.capabilities.includes(
'Object Storage'
);

/*
Expand Down Expand Up @@ -220,7 +228,24 @@ export const CreateImageTab = () => {
required
value={selectedLinodeId}
/>
{linodeIsInDistributedRegion && (
{selectedLinode &&
!linodeRegionSupportsImageStorage &&
flags.imageServiceGen2 &&
flags.imageServiceGen2Ga && (
<Notice variant="warning">
This Linode’s region doesn’t support local image storage. This
image will be stored in the core compute region that’s{' '}
<Link to="https://techdocs.akamai.com/cloud-computing/docs/images#regions-and-captured-custom-images">
geographically closest
</Link>
. After it’s stored, you can replicate it to other{' '}
<Link to="https://www.linode.com/global-infrastructure/">
core compute regions
</Link>
.
</Notice>
)}
{linodeIsInDistributedRegion && !flags.imageServiceGen2Ga && (
<Notice variant="warning">
This Linode is in a distributed compute region. These regions
can't store images. The image is stored in the core compute
Expand Down
100 changes: 74 additions & 26 deletions packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import React from 'react';

import { imageFactory } from 'src/factories';
import {
Expand All @@ -15,16 +15,6 @@ import type { Handlers } from './ImagesActionMenu';
beforeAll(() => mockMatchMedia());

describe('Image Table Row', () => {
const image = imageFactory.build({
capabilities: ['cloud-init', 'distributed-sites'],
regions: [
{ region: 'us-east', status: 'available' },
{ region: 'us-southeast', status: 'pending' },
],
size: 300,
total_size: 600,
});

const handlers: Handlers = {
onCancelFailed: vi.fn(),
onDelete: vi.fn(),
Expand All @@ -35,36 +25,94 @@ describe('Image Table Row', () => {
onRetry: vi.fn(),
};

it('should render an image row', async () => {
const { getAllByText, getByLabelText, getByText } = renderWithTheme(
it('should render an image row with Image Service Gen2 enabled', async () => {
const image = imageFactory.build({
capabilities: ['cloud-init', 'distributed-sites'],
regions: [
{ region: 'us-east', status: 'available' },
{ region: 'us-southeast', status: 'pending' },
],
size: 300,
total_size: 600,
});

const { getByLabelText, getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />
)
);

// Check to see if the row rendered some data

expect(getByText(image.label)).toBeVisible();
expect(getByText(image.id)).toBeVisible();
expect(getByText('Ready')).toBeVisible();
expect(getByText('Cloud-init, Distributed')).toBeVisible();
expect(getByText('2 Regions')).toBeVisible();
expect(getByText('0.29 GB')).toBeVisible(); // 300 / 1024 = 0.292
expect(getByText('0.59 GB')).toBeVisible(); // 600 / 1024 = 0.585

getByText(image.label);
getAllByText('Ready');
getAllByText('Cloud-init, Distributed');
getAllByText(image.id);
expect(getByText('0.29 GB')).toBeVisible(); // Size is converted from MB to GB - 300 / 1024 = 0.292
expect(getByText('0.59 GB')).toBeVisible(); // Size is converted from MB to GB - 600 / 1024 = 0.585

// Open action menu
const actionMenu = getByLabelText(`Action menu for Image ${image.label}`);
await userEvent.click(actionMenu);

getByText('Edit');
getByText('Manage Replicas');
getByText('Deploy to New Linode');
getByText('Rebuild an Existing Linode');
getByText('Delete');
expect(getByText('Edit')).toBeVisible();
expect(getByText('Manage Replicas')).toBeVisible();
expect(getByText('Deploy to New Linode')).toBeVisible();
expect(getByText('Rebuild an Existing Linode')).toBeVisible();
expect(getByText('Delete')).toBeVisible();
});

it('should show a cloud-init icon with a tooltip when Image Service Gen 2 GA is enabled and the image supports cloud-init', () => {
const image = imageFactory.build({
capabilities: ['cloud-init'],
regions: [{ region: 'us-east', status: 'available' }],
});

const { getByLabelText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true, imageServiceGen2Ga: true } }
)
);

expect(
getByLabelText('This image supports our Metadata service via cloud-init.')
).toBeVisible();
});

it('does not show the compatibility column when Image Service Gen2 GA is enabled', () => {
const image = imageFactory.build({
capabilities: ['cloud-init', 'distributed-sites'],
});

const { queryByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true, imageServiceGen2Ga: true } }
)
);

expect(queryByText('Cloud-init, Distributed')).not.toBeInTheDocument();
});

it('should show N/A if multiRegionsEnabled is true, but the Image does not have any regions', () => {
const image = imageFactory.build({ regions: [] });

const { getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true } }
)
);

expect(getByText('N/A')).toBeVisible();
});

it('calls handlers when performing actions', async () => {
const image = imageFactory.build({
regions: [{ region: 'us-east', status: 'available' }],
});

const { getByLabelText, getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />
Expand Down
46 changes: 35 additions & 11 deletions packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as React from 'react';
import React from 'react';

import CloudInitIcon from 'src/assets/icons/cloud-init.svg';
import { Hidden } from 'src/components/Hidden';
import { LinkButton } from 'src/components/LinkButton';
import { Stack } from 'src/components/Stack';
import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';
import { Tooltip } from 'src/components/Tooltip';
import { Typography } from 'src/components/Typography';
import { useFlags } from 'src/hooks/useFlags';
import { useProfile } from 'src/queries/profile/profile';
import { capitalizeAllWords } from 'src/utilities/capitalize';
import { formatDate } from 'src/utilities/formatDate';
Expand Down Expand Up @@ -44,6 +48,7 @@ export const ImageRow = (props: Props) => {
} = image;

const { data: profile } = useProfile();
const flags = useFlags();

const isFailed = status === 'pending_upload' && event?.status === 'failed';

Expand Down Expand Up @@ -92,23 +97,42 @@ export const ImageRow = (props: Props) => {

return (
<TableRow data-qa-image-cell={id} key={id}>
<TableCell data-qa-image-label>{label}</TableCell>
<TableCell data-qa-image-label noWrap>
{capabilities.includes('cloud-init') &&
flags.imageServiceGen2 &&
flags.imageServiceGen2Ga ? (
<Stack alignItems="center" direction="row" gap={1.5}>
<Tooltip title="This image supports our Metadata service via cloud-init.">
<div style={{ display: 'flex' }}>
<CloudInitIcon />
</div>
</Tooltip>
{label}
</Stack>
) : (
label
)}
</TableCell>
<Hidden smDown>
<TableCell>{getStatusForImage(status)}</TableCell>
</Hidden>
{multiRegionsEnabled && (
<>
<Hidden smDown>
<TableCell>
<Hidden smDown>
<TableCell>
{regions.length > 0 ? (
<LinkButton onClick={() => handlers.onManageRegions?.(image)}>
{pluralize('Region', 'Regions', regions.length)}
</LinkButton>
</TableCell>
</Hidden>
<Hidden smDown>
<TableCell>{compatibilitiesList}</TableCell>
</Hidden>
</>
) : (
'N/A'
)}
</TableCell>
</Hidden>
)}
{multiRegionsEnabled && !flags.imageServiceGen2Ga && (
<Hidden smDown>
<TableCell>{compatibilitiesList}</TableCell>
</Hidden>
)}
<TableCell data-qa-image-size>
{getSizeForImage(size, status, event?.status)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,14 +450,14 @@ export const ImagesLanding = () => {
<TableCell>Status</TableCell>
</Hidden>
{multiRegionsEnabled && (
<>
<Hidden smDown>
<TableCell>Replicated in</TableCell>
</Hidden>
<Hidden smDown>
<TableCell>Compatibility</TableCell>
</Hidden>
</>
<Hidden smDown>
<TableCell>Replicated in</TableCell>
</Hidden>
)}
{multiRegionsEnabled && !flags.imageServiceGen2Ga && (
<Hidden smDown>
<TableCell>Compatibility</TableCell>
</Hidden>
)}
<TableSortCell
active={manualImagesOrderBy === 'size'}
Expand Down

0 comments on commit 7735a37

Please sign in to comment.