From 6346f2243846500b776e955dbfbebd8ba8b76b4d Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 4 Jun 2024 16:36:59 -0400 Subject: [PATCH 01/25] Saving progress... --- .../manager/src/features/Images/ImageRow.tsx | 29 +++++++++++++++++++ .../src/features/Images/ImagesLanding.tsx | 19 ++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index e3eee26d2fc..aaece52af7e 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -2,11 +2,13 @@ import { Event } from '@linode/api-v4/lib/account'; import { Image } from '@linode/api-v4/lib/images'; import * as React from 'react'; +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { useProfile } from 'src/queries/profile'; +import { useRegionsQuery } from 'src/queries/regions/regions'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatDate } from 'src/utilities/formatDate'; @@ -28,6 +30,7 @@ const ImageRow = (props: Props) => { label, onCancelFailed, onRetry, + regions, size, status, ...rest @@ -35,8 +38,27 @@ const ImageRow = (props: Props) => { const { data: profile } = useProfile(); + const { data: regionsData } = useRegionsQuery(); + const isFailed = status === 'pending_upload' && event?.status === 'failed'; + // TODO Image Service v2: delete after GA + const multiRegionsEnabled = regions.length > 0; + + const regionsList = multiRegionsEnabled && ( + <> + {regionsData?.find((region) => region.id == regions[0].region)?.label} + {regions.length > 1 && ( + <> + ,{' '} + + +{regions.length - 1} + + + )} + + ); + const getStatusForImage = (status: string) => { switch (status) { case 'creating': @@ -74,6 +96,13 @@ const ImageRow = (props: Props) => { {label} {status ? {getStatusForImage(status)} : null} + + {regionsList && ( + + {regionsList} + + )} + {formatDate(created, { timezone: profile?.timezone, diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index d52729e5923..91f6fa1518f 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -194,6 +194,19 @@ export const ImagesLanding: React.FC = () => { const manualImagesData = getImagesWithEvents( manualImages?.data ?? [], imageEvents + ) + // TODO: delete + .map((image) => ({ + ...image, + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'available' }, + ], + })); + + // TODO Image Service v2: delete after GA + const multiRegionsEnabled = manualImagesData.some( + (image) => image.regions.length ); // Automatic images with the associated events tied in. @@ -452,6 +465,7 @@ export const ImagesLanding: React.FC = () => { return renderEmpty(); } + // TODO update colSpan const noManualImages = ( ); @@ -491,6 +505,11 @@ export const ImagesLanding: React.FC = () => { Status + {multiRegionsEnabled && ( + + Region(s) + + )} Date: Wed, 12 Jun 2024 17:03:19 -0400 Subject: [PATCH 02/25] Add compatibility, total size and image id columns --- packages/api-v4/src/images/types.ts | 2 +- .../manager/src/features/Images/ImageRow.tsx | 46 ++++++++++++++++--- .../src/features/Images/ImagesLanding.tsx | 27 ++++++++--- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/packages/api-v4/src/images/types.ts b/packages/api-v4/src/images/types.ts index 48be8aff12a..e25fb28f9a2 100644 --- a/packages/api-v4/src/images/types.ts +++ b/packages/api-v4/src/images/types.ts @@ -4,7 +4,7 @@ export type ImageStatus = | 'deleted' | 'pending_upload'; -type ImageCapabilities = 'cloud-init' | 'distributed-images'; +export type ImageCapabilities = 'cloud-init' | 'distributed-images'; type ImageType = 'manual' | 'automatic'; diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index 64c60330c8d..413c9daf40a 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -13,7 +13,12 @@ import { formatDate } from 'src/utilities/formatDate'; import { ImagesActionMenu } from './ImagesActionMenu'; import type { Handlers } from './ImagesActionMenu'; -import type { Event, Image } from '@linode/api-v4'; +import type { Event, Image, ImageCapabilities } from '@linode/api-v4'; + +const capabilityMap: Record = { + 'cloud-init': 'Cloud-init', + 'distributed-images': 'Distributed', +}; interface Props { event?: Event; @@ -24,7 +29,17 @@ interface Props { const ImageRow = (props: Props) => { const { event, image } = props; - const { created, expiry, id, label, regions, size, status } = image; + const { + capabilities, + created, + expiry, + id, + label, + regions, + size, + status, + total_size, + } = image; const { data: profile } = useProfile(); @@ -49,6 +64,10 @@ const ImageRow = (props: Props) => { ); + const compatibilitiesList = + multiRegionsEnabled && + capabilities.map((capability) => capabilityMap[capability]).join(', '); + const getStatusForImage = (status: string) => { switch (status) { case 'creating': @@ -87,9 +106,24 @@ const ImageRow = (props: Props) => { {status ? {getStatusForImage(status)} : null} + {regionsList && ( + <> + + {regionsList} + + + {compatibilitiesList} + + + )} + + {getSizeForImage(size, status, event?.status)} + {regionsList && ( - {regionsList} + + {getSizeForImage(total_size, status, event?.status)} + )} @@ -99,9 +133,6 @@ const ImageRow = (props: Props) => { })} - - {getSizeForImage(size, status, event?.status)} - {expiry ? ( @@ -111,6 +142,9 @@ const ImageRow = (props: Props) => { ) : null} + + {id} + diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index f225a8ec488..02651157ada 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -360,9 +360,8 @@ export const ImagesLanding = () => { return renderEmpty(); } - // TODO update colSpan const noManualImages = ( - + ); const noAutomaticImages = ( @@ -401,10 +400,24 @@ export const ImagesLanding = () => { Status {multiRegionsEnabled && ( + <> Region(s) + + Compatibility + + )} + + Size + + Total Size { Created + {}} // TODO: sorting on id currently broken + label="id" > - Size + Image Id + From a7afb39618749aa806512bb7253cd24d73320471 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 13 Jun 2024 15:42:20 -0400 Subject: [PATCH 03/25] Add onManageRegions to image action menu --- .../src/features/Images/EditImageDrawer.tsx | 22 ++++-- .../manager/src/features/Images/ImageRow.tsx | 12 ++-- .../src/features/Images/ImagesActionMenu.tsx | 14 +++- .../src/features/Images/ImagesLanding.tsx | 72 +++++++++---------- .../features/Images/RebuildImageDrawer.tsx | 8 +-- 5 files changed, 71 insertions(+), 57 deletions(-) diff --git a/packages/manager/src/features/Images/EditImageDrawer.tsx b/packages/manager/src/features/Images/EditImageDrawer.tsx index eaa00d8f4f1..11c7e00b212 100644 --- a/packages/manager/src/features/Images/EditImageDrawer.tsx +++ b/packages/manager/src/features/Images/EditImageDrawer.tsx @@ -1,5 +1,4 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { APIError, Image, UpdateImagePayload } from '@linode/api-v4'; import { updateImageSchema } from '@linode/validation'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -9,24 +8,28 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; +import { usePrevious } from 'src/hooks/usePrevious'; import { useUpdateImageMutation } from 'src/queries/images'; import { useImageAndLinodeGrantCheck } from './utils'; +import type { APIError, Image, UpdateImagePayload } from '@linode/api-v4'; + interface Props { image: Image | undefined; onClose: () => void; - open: boolean; } export const EditImageDrawer = (props: Props) => { - const { image, onClose, open } = props; + const { image, onClose } = props; const { canCreateImage } = useImageAndLinodeGrantCheck(); + // Prevent content from disappearing when closing drawer + const prevImage = usePrevious(image); const defaultValues = { - description: image?.description ?? undefined, - label: image?.label, - tags: image?.tags, + description: image?.description ?? prevImage?.description ?? undefined, + label: image?.label ?? prevImage?.label, + tags: image?.tags ?? prevImage?.tags, }; const { @@ -75,7 +78,12 @@ export const EditImageDrawer = (props: Props) => { }); return ( - + {!canCreateImage && ( { - const { event, image } = props; + const { event, handlers, image } = props; const { capabilities, @@ -56,7 +56,7 @@ const ImageRow = (props: Props) => { {regions.length > 1 && ( <> ,{' '} - + handlers.onManageRegions(image)}> +{regions.length - 1} @@ -142,9 +142,11 @@ const ImageRow = (props: Props) => { ) : null} - - {id} - + {regionsList && ( + + {id} + + )} diff --git a/packages/manager/src/features/Images/ImagesActionMenu.tsx b/packages/manager/src/features/Images/ImagesActionMenu.tsx index d50b994a04f..db506adc442 100644 --- a/packages/manager/src/features/Images/ImagesActionMenu.tsx +++ b/packages/manager/src/features/Images/ImagesActionMenu.tsx @@ -1,13 +1,16 @@ -import { Event, Image, ImageStatus } from '@linode/api-v4'; import * as React from 'react'; -import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; + +import type { Event, Image, ImageStatus } from '@linode/api-v4'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; export interface Handlers { onCancelFailed?: (imageID: string) => void; onDelete?: (label: string, imageID: string, status?: ImageStatus) => void; onDeploy?: (imageID: string) => void; onEdit?: (image: Image) => void; + onManageRegions: (image: Image) => void; onRestore?: (image: Image) => void; onRetry?: ( imageID: string, @@ -32,6 +35,7 @@ export const ImagesActionMenu = (props: Props) => { onDelete, onDeploy, onEdit, + onManageRegions, onRestore, onRetry, } = handlers; @@ -60,6 +64,11 @@ export const ImagesActionMenu = (props: Props) => { ? 'Image is not yet available for use.' : undefined, }, + { + disabled: isDisabled, + onClick: () => onManageRegions?.(image), + title: 'Manage Regions', + }, { disabled: isDisabled, onClick: () => onDeploy?.(id), @@ -91,6 +100,7 @@ export const ImagesActionMenu = (props: Props) => { onCancelFailed, onEdit, image, + onManageRegions, onDeploy, onRestore, onDelete, diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 02651157ada..72fcf3e8f41 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -188,13 +188,12 @@ export const ImagesLanding = () => { imageEvents ); - const [selectedImage, setSelectedImage] = React.useState(); - - const [editDrawerOpen, setEditDrawerOpen] = React.useState(false); - - const [rebuildDrawerOpen, setRebuildDrawerOpen] = React.useState( - false - ); + const [ + manageRegionsDrawerImage, + setManageRegionsDrawerImage, + ] = React.useState(); + const [editDrawerImage, setEditDrawerImage] = React.useState(); + const [rebuildDrawerImage, setRebuildDrawerImage] = React.useState(); const [dialog, setDialogState] = React.useState( defaultDialogState @@ -280,16 +279,6 @@ export const ImagesLanding = () => { queryClient.invalidateQueries(imageQueries.paginated._def); }; - const openForEdit = (image: Image) => { - setSelectedImage(image); - setEditDrawerOpen(true); - }; - - const openForRestore = (image: Image) => { - setSelectedImage(image); - setRebuildDrawerOpen(true); - }; - const deployNewLinode = (imageID: string) => { history.push({ pathname: `/linodes/create/`, @@ -320,8 +309,9 @@ export const ImagesLanding = () => { onCancelFailed: onCancelFailedClick, onDelete: openDialog, onDeploy: deployNewLinode, - onEdit: openForEdit, - onRestore: openForRestore, + onEdit: setEditDrawerImage, + onManageRegions: setManageRegionsDrawerImage, + onRestore: setRebuildDrawerImage, onRetry: onRetryClick, }; @@ -401,9 +391,9 @@ export const ImagesLanding = () => { {multiRegionsEnabled && ( <> - - Region(s) - + + Region(s) + Compatibility @@ -417,7 +407,11 @@ export const ImagesLanding = () => { > Size - Total Size + {multiRegionsEnabled && ( + + Total Size + + )} { Created - - {}} // TODO: sorting on id currently broken - label="id" - > - Image Id - - + {multiRegionsEnabled && ( + + {}} // TODO: sorting on id currently broken + label="id" + > + Image Id + + + )} @@ -532,14 +528,12 @@ export const ImagesLanding = () => { /> setEditDrawerOpen(false)} - open={editDrawerOpen} + image={editDrawerImage} + onClose={() => setEditDrawerImage(undefined)} /> setRebuildDrawerOpen(false)} - open={rebuildDrawerOpen} + image={rebuildDrawerImage} + onClose={() => setRebuildDrawerImage(undefined)} /> void; - open?: boolean; } export const RebuildImageDrawer = (props: Props) => { - const { image, onClose, open } = props; + const { image, onClose } = props; const history = useHistory(); const { @@ -51,7 +51,7 @@ export const RebuildImageDrawer = (props: Props) => { {formState.errors.root?.message && ( From 6f0efc64d642a20dabfc20e3807c979d82482d34 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 12:12:07 -0400 Subject: [PATCH 04/25] Add unit tests for ImagesLanding --- .../features/Images/ImagesLanding.test.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 packages/manager/src/features/Images/ImagesLanding.test.tsx diff --git a/packages/manager/src/features/Images/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding.test.tsx new file mode 100644 index 00000000000..fe710ffe9da --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding.test.tsx @@ -0,0 +1,104 @@ +import { waitForElementToBeRemoved } from '@testing-library/react'; +import * as React from 'react'; + +import { imageFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; + +import ImagesLanding from './ImagesLanding'; + +beforeAll(() => mockMatchMedia()); + +const loadingTestId = 'circle-progress'; + +describe('Images Landing Table', () => { + it('should render images landing table with items', async () => { + server.resetHandlers(); + server.use( + http.get('*/images', () => { + const images = imageFactory.buildList(3, { + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { getAllByText, getByTestId } = renderWithTheme(); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + // Two tables should render + getAllByText('Custom Images'); + getAllByText('Recovery Images'); + + // Static text and table column headers + expect(getAllByText('Image').length).toBe(2); + expect(getAllByText('Status').length).toBe(2); + expect(getAllByText('Region(s)').length).toBe(1); + expect(getAllByText('Compatibility').length).toBe(1); + expect(getAllByText('Size').length).toBe(2); + expect(getAllByText('Total Size').length).toBe(1); + expect(getAllByText('Created').length).toBe(2); + expect(getAllByText('Image Id').length).toBe(1); + }); + + it('should render custom images empty state', async () => { + server.use( + http.get('*/images', ({ request }) => { + return HttpResponse.json( + makeResourcePage( + request.headers.get('x-filter')?.includes('automatic') + ? [imageFactory.build({ type: 'automatic' })] + : [] + ) + ); + }) + ); + + const { getByTestId, getByText } = renderWithTheme(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect(getByText('No Custom Images to display.')).toBeInTheDocument(); + }); + + it('should render automatic images empty state', async () => { + server.use( + http.get('*/images', ({ request }) => { + return HttpResponse.json( + makeResourcePage( + request.headers.get('x-filter')?.includes('manual') + ? [imageFactory.build({ type: 'manual' })] + : [] + ) + ); + }) + ); + + const { getByTestId, getByText } = renderWithTheme(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect(getByText('No Recovery Images to display.')).toBeInTheDocument(); + }); + + it('should render images landing empty state', async () => { + server.use( + http.get('*/images', () => { + return HttpResponse.json(makeResourcePage([])); + }) + ); + + const { getByTestId, getByText } = renderWithTheme(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + expect( + getByText((text) => text.includes('Store your own custom Linux images')) + ).toBeInTheDocument(); + }); +}); From b79e71468552fb5ccc28adf78a2c487c66dcedde Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 12:38:24 -0400 Subject: [PATCH 05/25] Add mock regions image and improve conditional column rendering --- packages/manager/src/factories/images.ts | 3 +- .../manager/src/features/Images/ImageRow.tsx | 48 ++++++++++--------- .../src/features/Images/ImagesActionMenu.tsx | 16 ++++--- .../src/features/Images/ImagesLanding.tsx | 10 ++-- packages/manager/src/mocks/serverHandlers.ts | 28 +++++++---- 5 files changed, 63 insertions(+), 42 deletions(-) diff --git a/packages/manager/src/factories/images.ts b/packages/manager/src/factories/images.ts index 18a6246e599..86c6edefa27 100644 --- a/packages/manager/src/factories/images.ts +++ b/packages/manager/src/factories/images.ts @@ -1,6 +1,7 @@ -import { Image } from '@linode/api-v4/lib/images/types'; import * as Factory from 'factory.ts'; +import type { Image } from '@linode/api-v4/lib/images/types'; + export const imageFactory = Factory.Sync.makeFactory({ capabilities: [], created: new Date().toISOString(), diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index 6c11e73dccf..778f513e342 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -24,10 +24,11 @@ interface Props { event?: Event; handlers: Handlers; image: Image; + multiRegionsEnabled?: boolean; // TODO Image Service v2: delete after GA } const ImageRow = (props: Props) => { - const { event, handlers, image } = props; + const { event, handlers, image, multiRegionsEnabled } = props; const { capabilities, @@ -47,26 +48,27 @@ const ImageRow = (props: Props) => { const isFailed = status === 'pending_upload' && event?.status === 'failed'; - // TODO Image Service v2: delete after GA - const multiRegionsEnabled = regions.length > 0; - - const regionsList = multiRegionsEnabled && ( - <> - {regionsData?.find((region) => region.id == regions[0].region)?.label} - {regions.length > 1 && ( - <> - ,{' '} - handlers.onManageRegions(image)}> - +{regions.length - 1} - - - )} - - ); - - const compatibilitiesList = + const regionsList = multiRegionsEnabled && - capabilities.map((capability) => capabilityMap[capability]).join(', '); + (regions.length > 0 ? ( + <> + {regionsData?.find((region) => region.id == regions[0].region)?.label} + {regions.length > 1 && ( + <> + ,{' '} + handlers.onManageRegions?.(image)}> + +{regions.length - 1} + + + )} + + ) : ( + '' + )); + + const compatibilitiesList = multiRegionsEnabled + ? capabilities.map((capability) => capabilityMap[capability]).join(', ') + : ''; const getStatusForImage = (status: string) => { switch (status) { @@ -106,7 +108,7 @@ const ImageRow = (props: Props) => { {status ? {getStatusForImage(status)} : null} - {regionsList && ( + {multiRegionsEnabled && ( <> {regionsList} @@ -119,7 +121,7 @@ const ImageRow = (props: Props) => { {getSizeForImage(size, status, event?.status)} - {regionsList && ( + {multiRegionsEnabled && ( {getSizeForImage(total_size, status, event?.status)} @@ -142,7 +144,7 @@ const ImageRow = (props: Props) => { ) : null} - {regionsList && ( + {multiRegionsEnabled && ( {id} diff --git a/packages/manager/src/features/Images/ImagesActionMenu.tsx b/packages/manager/src/features/Images/ImagesActionMenu.tsx index db506adc442..41ea6d1b519 100644 --- a/packages/manager/src/features/Images/ImagesActionMenu.tsx +++ b/packages/manager/src/features/Images/ImagesActionMenu.tsx @@ -10,7 +10,7 @@ export interface Handlers { onDelete?: (label: string, imageID: string, status?: ImageStatus) => void; onDeploy?: (imageID: string) => void; onEdit?: (image: Image) => void; - onManageRegions: (image: Image) => void; + onManageRegions?: (image: Image) => void; onRestore?: (image: Image) => void; onRetry?: ( imageID: string, @@ -64,11 +64,15 @@ export const ImagesActionMenu = (props: Props) => { ? 'Image is not yet available for use.' : undefined, }, - { - disabled: isDisabled, - onClick: () => onManageRegions?.(image), - title: 'Manage Regions', - }, + ...(onManageRegions + ? [ + { + disabled: isDisabled, + onClick: () => onManageRegions(image), + title: 'Manage Regions', + }, + ] + : []), { disabled: isDisabled, onClick: () => onDeploy?.(id), diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 72fcf3e8f41..6df9dba3f93 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -178,9 +178,8 @@ export const ImagesLanding = () => { ); // TODO Image Service V2: delete after GA - const multiRegionsEnabled = manualImages?.data.some( - (image) => image.regions.length - ); + const multiRegionsEnabled = + manualImages?.data.some((image) => image.regions.length) ?? false; // Automatic images with the associated events tied in. const automaticImagesEvents = getEventsForImages( @@ -310,7 +309,9 @@ export const ImagesLanding = () => { onDelete: openDialog, onDeploy: deployNewLinode, onEdit: setEditDrawerImage, - onManageRegions: setManageRegionsDrawerImage, + onManageRegions: multiRegionsEnabled + ? setManageRegionsDrawerImage + : undefined, onRestore: setRebuildDrawerImage, onRetry: onRetryClick, }; @@ -445,6 +446,7 @@ export const ImagesLanding = () => { handlers={handlers} image={manualImage} key={manualImage.id} + multiRegionsEnabled={multiRegionsEnabled} /> )) : noManualImages} diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index f9511523f12..92dd80c2258 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -1,11 +1,3 @@ -import { - NotificationType, - ObjectStorageKeyRequest, - SecurityQuestionsPayload, - TokenRequest, - User, - VolumeStatus, -} from '@linode/api-v4'; import { DateTime } from 'luxon'; import { HttpResponse, http } from 'msw'; @@ -108,6 +100,15 @@ import { grantFactory, grantsFactory } from 'src/factories/grants'; import { pickRandom } from 'src/utilities/random'; import { getStorage } from 'src/utilities/storage'; +import type { + NotificationType, + ObjectStorageKeyRequest, + SecurityQuestionsPayload, + TokenRequest, + User, + VolumeStatus, +} from '@linode/api-v4'; + export const makeResourcePage = ( e: T[], override: { page: number; pages: number; results?: number } = { @@ -624,6 +625,16 @@ export const handlers = [ status: 'available', type: 'manual', }); + const multiRegionsImage = imageFactory.build({ + id: 'multi-regions-test-image', + label: 'multi-regions-test-image', + regions: [ + { region: 'us-southeast', status: 'available' }, + { region: 'us-east', status: 'pending' }, + ], + status: 'available', + type: 'manual', + }); const creatingImages = imageFactory.buildList(2, { status: 'creating', type: 'manual', @@ -640,6 +651,7 @@ export const handlers = [ const images = [ cloudinitCompatableDistro, cloudinitCompatableImage, + multiRegionsImage, ...automaticImages, ...privateImages, ...publicImages, From 2be202b178b693a9b9a54d02fe13591b75d1a760 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 12:41:22 -0400 Subject: [PATCH 06/25] remove extraneous changes --- packages/manager/src/factories/images.ts | 3 +-- packages/manager/src/features/Events/factories/tax.tsx | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/factories/images.ts b/packages/manager/src/factories/images.ts index 86c6edefa27..18a6246e599 100644 --- a/packages/manager/src/factories/images.ts +++ b/packages/manager/src/factories/images.ts @@ -1,7 +1,6 @@ +import { Image } from '@linode/api-v4/lib/images/types'; import * as Factory from 'factory.ts'; -import type { Image } from '@linode/api-v4/lib/images/types'; - export const imageFactory = Factory.Sync.makeFactory({ capabilities: [], created: new Date().toISOString(), diff --git a/packages/manager/src/features/Events/factories/tax.tsx b/packages/manager/src/features/Events/factories/tax.tsx index 5ac7cb45211..f107718a8e1 100644 --- a/packages/manager/src/features/Events/factories/tax.tsx +++ b/packages/manager/src/features/Events/factories/tax.tsx @@ -4,10 +4,6 @@ import type { PartialEventMap } from '../types'; export const tax: PartialEventMap<'tax'> = { tax_id_invalid: { - notification: () => ( - <> - Tax Identification Number format is invalid. - - ), + notification: () => <>Tax Identification Number format is invalid., }, }; From 329318dd08c82193ee858d3650337265fcb0c07d Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 18:02:32 -0400 Subject: [PATCH 07/25] Add more unit tests --- .../src/features/Images/ImageRow.test.tsx | 93 +++++++++++ .../manager/src/features/Images/ImageRow.tsx | 3 +- .../features/Images/ImagesLanding.test.tsx | 151 +++++++++++++++++- 3 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 packages/manager/src/features/Images/ImageRow.test.tsx diff --git a/packages/manager/src/features/Images/ImageRow.test.tsx b/packages/manager/src/features/Images/ImageRow.test.tsx new file mode 100644 index 00000000000..c6fa1646bda --- /dev/null +++ b/packages/manager/src/features/Images/ImageRow.test.tsx @@ -0,0 +1,93 @@ +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; + +import { imageFactory } from 'src/factories'; +import { + mockMatchMedia, + renderWithTheme, + wrapWithTableBody, +} from 'src/utilities/testHelpers'; + +import ImageRow from './ImageRow'; + +import type { Handlers } from './ImagesActionMenu'; + +beforeAll(() => mockMatchMedia()); + +describe('Image Table Row', () => { + const image = imageFactory.build({ + capabilities: ['cloud-init', 'distributed-images'], + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + + const handlers: Handlers = { + onCancelFailed: vi.fn(), + onDelete: vi.fn(), + onDeploy: vi.fn(), + onEdit: vi.fn(), + onManageRegions: vi.fn(), + onRestore: vi.fn(), + onRetry: vi.fn(), + }; + + it('should render an image row', async () => { + const { getAllByText, getByLabelText, getByText } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + // Check to see if the row rendered some data + getByText(image.label); + getAllByText('Ready'); + getAllByText((text) => text.includes(image.regions[0].region)); + getAllByText('+1'); + getAllByText('Cloud-init, Distributed'); + expect(getAllByText('1500 MB').length).toBe(2); + getAllByText(image.id); + + // Open action menu + const actionMenu = getByLabelText(`Action menu for Image ${image.label}`); + await userEvent.click(actionMenu); + + getByText('Edit'); + getByText('Manage Regions'); + getByText('Deploy to New Linode'); + getByText('Rebuild an Existing Linode'); + getByText('Delete'); + }); + + it('calls handlers when performing actions', async () => { + const { getByLabelText, getByText } = renderWithTheme( + wrapWithTableBody( + + ) + ); + + // Open action menu + const actionMenu = getByLabelText(`Action menu for Image ${image.label}`); + await userEvent.click(actionMenu); + + await userEvent.click(getByText('Edit')); + expect(handlers.onEdit).toBeCalledWith(image); + + await userEvent.click(getByText('Manage Regions')); + expect(handlers.onManageRegions).toBeCalledWith(image); + + await userEvent.click(getByText('Deploy to New Linode')); + expect(handlers.onDeploy).toBeCalledWith(image.id); + + await userEvent.click(getByText('Rebuild an Existing Linode')); + expect(handlers.onRestore).toBeCalledWith(image); + + await userEvent.click(getByText('Delete')); + expect(handlers.onDelete).toBeCalledWith( + image.label, + image.id, + image.status + ); + }); +}); diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index 778f513e342..212b5853a4c 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -52,7 +52,8 @@ const ImageRow = (props: Props) => { multiRegionsEnabled && (regions.length > 0 ? ( <> - {regionsData?.find((region) => region.id == regions[0].region)?.label} + {regionsData?.find((region) => region.id == regions[0].region)?.label ?? + regions[0].region} {regions.length > 1 && ( <> ,{' '} diff --git a/packages/manager/src/features/Images/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding.test.tsx index fe710ffe9da..b1209c60c70 100644 --- a/packages/manager/src/features/Images/ImagesLanding.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.test.tsx @@ -1,4 +1,5 @@ import { waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { imageFactory } from 'src/factories'; @@ -8,13 +9,26 @@ import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import ImagesLanding from './ImagesLanding'; +const mockHistory = { + push: vi.fn(), + replace: vi.fn(), +}; + +// Mock useHistory +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useHistory: vi.fn(() => mockHistory), + }; +}); + beforeAll(() => mockMatchMedia()); const loadingTestId = 'circle-progress'; describe('Images Landing Table', () => { it('should render images landing table with items', async () => { - server.resetHandlers(); server.use( http.get('*/images', () => { const images = imageFactory.buildList(3, { @@ -101,4 +115,139 @@ describe('Images Landing Table', () => { getByText((text) => text.includes('Store your own custom Linux images')) ).toBeInTheDocument(); }); + + it('should allow opening the Edit Image drawer', async () => { + const images = imageFactory.buildList(3, { + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + server.use( + http.get('*/images', () => { + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { getAllByLabelText, getByTestId, getByText } = renderWithTheme( + + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + // Open action menu + const actionMenu = getAllByLabelText( + `Action menu for Image ${images[0].label}` + )[0]; + await userEvent.click(actionMenu); + + await userEvent.click(getByText('Edit')); + + getByText('Edit Image'); + }); + + it('should allow opening the Restore Image drawer', async () => { + const images = imageFactory.buildList(3, { + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + server.use( + http.get('*/images', () => { + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { getAllByLabelText, getByTestId, getByText } = renderWithTheme( + + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + // Open action menu + const actionMenu = getAllByLabelText( + `Action menu for Image ${images[0].label}` + )[0]; + await userEvent.click(actionMenu); + + await userEvent.click(getByText('Rebuild an Existing Linode')); + + getByText('Restore from Image'); + }); + + it('should allow deploying to a new Linode', async () => { + const images = imageFactory.buildList(3, { + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + server.use( + http.get('*/images', () => { + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { getAllByLabelText, getByTestId, getByText } = renderWithTheme( + + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + // Open action menu + const actionMenu = getAllByLabelText( + `Action menu for Image ${images[0].label}` + )[0]; + await userEvent.click(actionMenu); + + await userEvent.click(getByText('Deploy to New Linode')); + expect(mockHistory.push).toBeCalledWith({ + pathname: '/linodes/create/', + search: `?type=Images&imageID=${images[0].id}`, + state: { selectedImageId: images[0].id }, + }); + }); + + it('should allow deleting an image', async () => { + const images = imageFactory.buildList(3, { + regions: [ + { region: 'us-east', status: 'available' }, + { region: 'us-southeast', status: 'pending' }, + ], + }); + server.use( + http.get('*/images', () => { + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { getAllByLabelText, getByTestId, getByText } = renderWithTheme( + + ); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + // Open action menu + const actionMenu = getAllByLabelText( + `Action menu for Image ${images[0].label}` + )[0]; + await userEvent.click(actionMenu); + + await userEvent.click(getByText('Delete')); + + getByText(`Delete Image ${images[0].label}`); + }); }); From cb353947e20665684803c992952ea58f08b2c979 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 18:03:34 -0400 Subject: [PATCH 08/25] Added changeset: Update Images Landing table --- .../.changeset/pr-10545-upcoming-features-1718402597165.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10545-upcoming-features-1718402597165.md diff --git a/packages/manager/.changeset/pr-10545-upcoming-features-1718402597165.md b/packages/manager/.changeset/pr-10545-upcoming-features-1718402597165.md new file mode 100644 index 00000000000..3651a31e1e8 --- /dev/null +++ b/packages/manager/.changeset/pr-10545-upcoming-features-1718402597165.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Images Landing table ([#10545](https://github.com/linode/manager/pull/10545)) From 371f66c810cd15ea1cb91a1838e45558f18dbd72 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 14 Jun 2024 18:10:32 -0400 Subject: [PATCH 09/25] Suppress typechecker warnings --- packages/manager/src/features/Images/ImagesLanding.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 6df9dba3f93..50c9be41ff5 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -188,6 +188,7 @@ export const ImagesLanding = () => { ); const [ + // @ts-expect-error This will be unused until the regions drawer is implemented manageRegionsDrawerImage, setManageRegionsDrawerImage, ] = React.useState(); @@ -428,6 +429,7 @@ export const ImagesLanding = () => { {}} // TODO: sorting on id currently broken label="id" > From 44d54cb85b7d0fedec1a3489ebb0bcbd79a9dc5a Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 17 Jun 2024 12:44:00 -0400 Subject: [PATCH 10/25] Prevent crashing when `.regions` field is not present --- packages/manager/src/features/Images/ImageRow.tsx | 2 +- packages/manager/src/features/Images/ImagesLanding.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index 212b5853a4c..484c49d30c1 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -50,7 +50,7 @@ const ImageRow = (props: Props) => { const regionsList = multiRegionsEnabled && - (regions.length > 0 ? ( + (regions && regions.length > 0 ? ( <> {regionsData?.find((region) => region.id == regions[0].region)?.label ?? regions[0].region} diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 50c9be41ff5..8016660e7c1 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -179,7 +179,7 @@ export const ImagesLanding = () => { // TODO Image Service V2: delete after GA const multiRegionsEnabled = - manualImages?.data.some((image) => image.regions.length) ?? false; + manualImages?.data.some((image) => image.regions?.length) ?? false; // Automatic images with the associated events tied in. const automaticImagesEvents = getEventsForImages( From ee1c857365c51e628ca6b425a27e600993e38428 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 17 Jun 2024 12:44:27 -0400 Subject: [PATCH 11/25] Remove sorting on image ID --- packages/manager/src/features/Images/ImagesLanding.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 8016660e7c1..0d0ca1f7112 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -426,15 +426,7 @@ export const ImagesLanding = () => { {multiRegionsEnabled && ( - {}} // TODO: sorting on id currently broken - label="id" - > - Image Id - + Image Id )} From 174217cd7a11f4347e7af10827049c3b9b92f4f1 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 17 Jun 2024 12:50:46 -0400 Subject: [PATCH 12/25] MSW: respect image type filter --- packages/manager/src/mocks/serverHandlers.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 92dd80c2258..73991ab75ca 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -605,7 +605,7 @@ export const handlers = [ http.get('*/regions', async () => { return HttpResponse.json(makeResourcePage(regions)); }), - http.get('*/images', async () => { + http.get('*/images', async ({ request }) => { const privateImages = imageFactory.buildList(5, { status: 'available', type: 'manual', @@ -658,7 +658,15 @@ export const handlers = [ ...pendingImages, ...creatingImages, ]; - return HttpResponse.json(makeResourcePage(images)); + return HttpResponse.json( + makeResourcePage( + images.filter((image) => + request.headers.get('x-filter')?.includes('manual') + ? image.type == 'manual' + : image.type == 'automatic' + ) + ) + ); }), http.get('*/linode/types', () => { From 9d01d4661881d168a505880f2c8dd16d77481470 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Mon, 17 Jun 2024 13:18:11 -0400 Subject: [PATCH 13/25] New RegionsList component and tests --- .../manager/src/features/Images/ImageRow.tsx | 33 ++++---------- .../src/features/Images/RegionsList.test.tsx | 43 +++++++++++++++++++ .../src/features/Images/RegionsList.tsx | 31 +++++++++++++ 3 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 packages/manager/src/features/Images/RegionsList.test.tsx create mode 100644 packages/manager/src/features/Images/RegionsList.tsx diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index 484c49d30c1..d6d2c44a197 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { useProfile } from 'src/queries/profile/profile'; -import { useRegionsQuery } from 'src/queries/regions/regions'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatDate } from 'src/utilities/formatDate'; import { ImagesActionMenu } from './ImagesActionMenu'; +import { RegionsList } from './RegionsList'; import type { Handlers } from './ImagesActionMenu'; import type { Event, Image, ImageCapabilities } from '@linode/api-v4'; @@ -44,29 +43,8 @@ const ImageRow = (props: Props) => { const { data: profile } = useProfile(); - const { data: regionsData } = useRegionsQuery(); - const isFailed = status === 'pending_upload' && event?.status === 'failed'; - const regionsList = - multiRegionsEnabled && - (regions && regions.length > 0 ? ( - <> - {regionsData?.find((region) => region.id == regions[0].region)?.label ?? - regions[0].region} - {regions.length > 1 && ( - <> - ,{' '} - handlers.onManageRegions?.(image)}> - +{regions.length - 1} - - - )} - - ) : ( - '' - )); - const compatibilitiesList = multiRegionsEnabled ? capabilities.map((capability) => capabilityMap[capability]).join(', ') : ''; @@ -112,7 +90,14 @@ const ImageRow = (props: Props) => { {multiRegionsEnabled && ( <> - {regionsList} + + {regions && regions.length > 0 && ( + handlers.onManageRegions?.(image)} + regions={regions} + /> + )} + {compatibilitiesList} diff --git a/packages/manager/src/features/Images/RegionsList.test.tsx b/packages/manager/src/features/Images/RegionsList.test.tsx new file mode 100644 index 00000000000..ea58d15f6dc --- /dev/null +++ b/packages/manager/src/features/Images/RegionsList.test.tsx @@ -0,0 +1,43 @@ +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; + +import 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { RegionsList } from './RegionsList'; + +describe('RegionsList', () => { + it('should render a single region', async () => { + const { findByText } = renderWithTheme( + + ); + + // Should initially fallback to region id + await findByText('us-east'); + await findByText('Newark, NJ'); + }); + + it('should allow expanding to view multiple regions', async () => { + const manageRegions = vi.fn(); + + const { findByRole, findByText } = renderWithTheme( + + ); + + await findByText((text) => text.includes('Newark, NJ')); + const expand = await findByRole('button'); + expect(expand).toHaveTextContent('+1'); + + await userEvent.click(expand); + expect(manageRegions).toBeCalled(); + }); +}); diff --git a/packages/manager/src/features/Images/RegionsList.tsx b/packages/manager/src/features/Images/RegionsList.tsx new file mode 100644 index 00000000000..e17785ea634 --- /dev/null +++ b/packages/manager/src/features/Images/RegionsList.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; +import { Typography } from 'src/components/Typography'; +import { useRegionsQuery } from 'src/queries/regions/regions'; + +import type { ImageRegion } from '@linode/api-v4'; + +interface Props { + onManageRegions: () => void; + regions: ImageRegion[]; +} + +export const RegionsList = ({ onManageRegions, regions }: Props) => { + const { data: regionsData } = useRegionsQuery(); + + return ( + + {regionsData?.find((region) => region.id == regions[0].region)?.label ?? + regions[0].region} + {regions.length > 1 && ( + <> + ,{' '} + + +{regions.length - 1} + + + )} + + ); +}; From 1df8d243f11c763c9bc164c8c199c888f6fcea4d Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 15:04:53 -0400 Subject: [PATCH 14/25] Image Id -> Image ID --- packages/manager/src/features/Images/ImagesLanding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 97ab421e891..9d666211864 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -490,7 +490,7 @@ export const ImagesLanding = () => { {multiRegionsEnabled && ( - Image Id + Image ID )} From 46a99f32b66ddbf2d0fec888ec772e7801d55b99 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 15:18:00 -0400 Subject: [PATCH 15/25] Hide columns in md breakpoint --- packages/manager/src/features/Images/ImageRow.tsx | 6 +++--- packages/manager/src/features/Images/ImagesLanding.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImageRow.tsx index d6d2c44a197..bd3e50581e2 100644 --- a/packages/manager/src/features/Images/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImageRow.tsx @@ -108,13 +108,13 @@ const ImageRow = (props: Props) => { {getSizeForImage(size, status, event?.status)} {multiRegionsEnabled && ( - + {getSizeForImage(total_size, status, event?.status)} )} - + {formatDate(created, { timezone: profile?.timezone, @@ -131,7 +131,7 @@ const ImageRow = (props: Props) => { ) : null} {multiRegionsEnabled && ( - + {id} )} diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index 9d666211864..4ae95c48872 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -474,11 +474,11 @@ export const ImagesLanding = () => { Size {multiRegionsEnabled && ( - + Total Size )} - + { {multiRegionsEnabled && ( - + Image ID )} From 9eba1fa700d8f4e09aec0629df01162db523c8aa Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 16:24:14 -0400 Subject: [PATCH 16/25] Re-organize Images directory --- .../src/features/Images/ImagesCreate/ImageCreate.tsx | 2 +- .../src/features/Images/{ => ImagesCreate}/ImageUpload.tsx | 6 +++--- .../features/Images/{ => ImagesCreate}/ImageUpload.utils.ts | 0 .../Images/{ => ImagesCreate}/ImageUploadCLIDialog.test.tsx | 0 .../Images/{ => ImagesCreate}/ImageUploadCLIDialog.tsx | 0 .../Images/{ => ImagesLanding}/EditImageDrawer.test.tsx | 0 .../features/Images/{ => ImagesLanding}/EditImageDrawer.tsx | 2 +- .../features/Images/{ => ImagesLanding}/ImageRow.test.tsx | 0 .../src/features/Images/{ => ImagesLanding}/ImageRow.tsx | 0 .../Images/{ => ImagesLanding}/ImagesActionMenu.tsx | 0 .../Images/{ => ImagesLanding}/ImagesLanding.test.tsx | 0 .../features/Images/{ => ImagesLanding}/ImagesLanding.tsx | 2 +- .../Images/{ => ImagesLanding}/ImagesLandingEmptyState.tsx | 0 .../{ => ImagesLanding}/ImagesLandingEmptyStateData.ts | 0 .../Images/{ => ImagesLanding}/RebuildImageDrawer.test.tsx | 0 .../Images/{ => ImagesLanding}/RebuildImageDrawer.tsx | 4 ++-- .../Images/{ => ImagesLanding}/RegionsList.test.tsx | 0 .../src/features/Images/{ => ImagesLanding}/RegionsList.tsx | 0 packages/manager/src/features/Images/index.tsx | 2 +- 19 files changed, 9 insertions(+), 9 deletions(-) rename packages/manager/src/features/Images/{ => ImagesCreate}/ImageUpload.tsx (98%) rename packages/manager/src/features/Images/{ => ImagesCreate}/ImageUpload.utils.ts (100%) rename packages/manager/src/features/Images/{ => ImagesCreate}/ImageUploadCLIDialog.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesCreate}/ImageUploadCLIDialog.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/EditImageDrawer.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/EditImageDrawer.tsx (98%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImageRow.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImageRow.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImagesActionMenu.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImagesLanding.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImagesLanding.tsx (99%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImagesLandingEmptyState.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/ImagesLandingEmptyStateData.ts (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/RebuildImageDrawer.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/RebuildImageDrawer.tsx (93%) rename packages/manager/src/features/Images/{ => ImagesLanding}/RegionsList.test.tsx (100%) rename packages/manager/src/features/Images/{ => ImagesLanding}/RegionsList.tsx (100%) diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageCreate.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageCreate.tsx index fafc8614c04..d1bf1d2b064 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageCreate.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageCreate.tsx @@ -6,7 +6,7 @@ import { NavTab, NavTabs } from 'src/components/NavTabs/NavTabs'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; const ImageUpload = React.lazy(() => - import('../ImageUpload').then((module) => ({ default: module.ImageUpload })) + import('./ImageUpload').then((module) => ({ default: module.ImageUpload })) ); const CreateImageTab = React.lazy(() => diff --git a/packages/manager/src/features/Images/ImageUpload.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx similarity index 98% rename from packages/manager/src/features/Images/ImageUpload.tsx rename to packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx index 0d64ef0b88c..bfef25f2a31 100644 --- a/packages/manager/src/features/Images/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx @@ -38,15 +38,15 @@ import { setPendingUpload } from 'src/store/pendingUpload'; import { getGDPRDetails } from 'src/utilities/formatRegion'; import { readableBytes } from 'src/utilities/unitConversions'; -import { EUAgreementCheckbox } from '../Account/Agreements/EUAgreementCheckbox'; -import { getRestrictedResourceText } from '../Account/utils'; +import { EUAgreementCheckbox } from '../../Account/Agreements/EUAgreementCheckbox'; +import { getRestrictedResourceText } from '../../Account/utils'; import { ImageUploadSchema, recordImageAnalytics } from './ImageUpload.utils'; import { ImageUploadFormData, ImageUploadNavigationState, } from './ImageUpload.utils'; import { ImageUploadCLIDialog } from './ImageUploadCLIDialog'; -import { uploadImageFile } from './requests'; +import { uploadImageFile } from '../requests'; import type { AxiosError, AxiosProgressEvent } from 'axios'; diff --git a/packages/manager/src/features/Images/ImageUpload.utils.ts b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.utils.ts similarity index 100% rename from packages/manager/src/features/Images/ImageUpload.utils.ts rename to packages/manager/src/features/Images/ImagesCreate/ImageUpload.utils.ts diff --git a/packages/manager/src/features/Images/ImageUploadCLIDialog.test.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUploadCLIDialog.test.tsx similarity index 100% rename from packages/manager/src/features/Images/ImageUploadCLIDialog.test.tsx rename to packages/manager/src/features/Images/ImagesCreate/ImageUploadCLIDialog.test.tsx diff --git a/packages/manager/src/features/Images/ImageUploadCLIDialog.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUploadCLIDialog.tsx similarity index 100% rename from packages/manager/src/features/Images/ImageUploadCLIDialog.tsx rename to packages/manager/src/features/Images/ImagesCreate/ImageUploadCLIDialog.tsx diff --git a/packages/manager/src/features/Images/EditImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Images/EditImageDrawer.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.test.tsx diff --git a/packages/manager/src/features/Images/EditImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx similarity index 98% rename from packages/manager/src/features/Images/EditImageDrawer.tsx rename to packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx index 11c7e00b212..582a7738462 100644 --- a/packages/manager/src/features/Images/EditImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx @@ -11,7 +11,7 @@ import { TextField } from 'src/components/TextField'; import { usePrevious } from 'src/hooks/usePrevious'; import { useUpdateImageMutation } from 'src/queries/images'; -import { useImageAndLinodeGrantCheck } from './utils'; +import { useImageAndLinodeGrantCheck } from '../utils'; import type { APIError, Image, UpdateImagePayload } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/ImageRow.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx similarity index 100% rename from packages/manager/src/features/Images/ImageRow.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx diff --git a/packages/manager/src/features/Images/ImageRow.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx similarity index 100% rename from packages/manager/src/features/Images/ImageRow.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx diff --git a/packages/manager/src/features/Images/ImagesActionMenu.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx similarity index 100% rename from packages/manager/src/features/Images/ImagesActionMenu.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx diff --git a/packages/manager/src/features/Images/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx similarity index 100% rename from packages/manager/src/features/Images/ImagesLanding.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx similarity index 99% rename from packages/manager/src/features/Images/ImagesLanding.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index 4ae95c48872..762d3abf7c1 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -46,7 +46,7 @@ import { EditImageDrawer } from './EditImageDrawer'; import ImageRow from './ImageRow'; import { ImagesLandingEmptyState } from './ImagesLandingEmptyState'; import { RebuildImageDrawer } from './RebuildImageDrawer'; -import { getEventsForImages } from './utils'; +import { getEventsForImages } from '../utils'; import type { Handlers as ImageHandlers } from './ImagesActionMenu'; import type { Image, ImageStatus } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/ImagesLandingEmptyState.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLandingEmptyState.tsx similarity index 100% rename from packages/manager/src/features/Images/ImagesLandingEmptyState.tsx rename to packages/manager/src/features/Images/ImagesLanding/ImagesLandingEmptyState.tsx diff --git a/packages/manager/src/features/Images/ImagesLandingEmptyStateData.ts b/packages/manager/src/features/Images/ImagesLanding/ImagesLandingEmptyStateData.ts similarity index 100% rename from packages/manager/src/features/Images/ImagesLandingEmptyStateData.ts rename to packages/manager/src/features/Images/ImagesLanding/ImagesLandingEmptyStateData.ts diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx similarity index 100% rename from packages/manager/src/features/Images/RebuildImageDrawer.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx similarity index 93% rename from packages/manager/src/features/Images/RebuildImageDrawer.tsx rename to packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx index 16766330c40..2c7685bc32d 100644 --- a/packages/manager/src/features/Images/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx @@ -7,8 +7,8 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; -import { REBUILD_LINODE_IMAGE_PARAM_NAME } from '../Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage'; -import { useImageAndLinodeGrantCheck } from './utils'; +import { REBUILD_LINODE_IMAGE_PARAM_NAME } from '../../Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage'; +import { useImageAndLinodeGrantCheck } from '../utils'; import type { Image } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/RegionsList.test.tsx b/packages/manager/src/features/Images/ImagesLanding/RegionsList.test.tsx similarity index 100% rename from packages/manager/src/features/Images/RegionsList.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/RegionsList.test.tsx diff --git a/packages/manager/src/features/Images/RegionsList.tsx b/packages/manager/src/features/Images/ImagesLanding/RegionsList.tsx similarity index 100% rename from packages/manager/src/features/Images/RegionsList.tsx rename to packages/manager/src/features/Images/ImagesLanding/RegionsList.tsx diff --git a/packages/manager/src/features/Images/index.tsx b/packages/manager/src/features/Images/index.tsx index 4f294a76b29..91767da9302 100644 --- a/packages/manager/src/features/Images/index.tsx +++ b/packages/manager/src/features/Images/index.tsx @@ -4,7 +4,7 @@ import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; -const ImagesLanding = React.lazy(() => import('./ImagesLanding')); +const ImagesLanding = React.lazy(() => import('./ImagesLanding/ImagesLanding')); const ImageCreate = React.lazy( () => import('./ImagesCreate/ImageCreateContainer') ); From 3d31d008a9860e142156a3bd62a6b2bbcda889bc Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 16:42:07 -0400 Subject: [PATCH 17/25] Fix unit test --- .../src/features/Images/ImagesLanding/ImagesLanding.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx index b1209c60c70..7536d29ec39 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx @@ -60,7 +60,7 @@ describe('Images Landing Table', () => { expect(getAllByText('Size').length).toBe(2); expect(getAllByText('Total Size').length).toBe(1); expect(getAllByText('Created').length).toBe(2); - expect(getAllByText('Image Id').length).toBe(1); + expect(getAllByText('Image ID').length).toBe(1); }); it('should render custom images empty state', async () => { From 9c102c55b2184dfef562f3a4dd1f6d8202360040 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 17:46:43 -0400 Subject: [PATCH 18/25] Update drawer and unit tests --- .../Images/RebuildImageDrawer.test.tsx | 4 +- .../features/Images/RebuildImageDrawer.tsx | 101 ++++++++++-------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.test.tsx b/packages/manager/src/features/Images/RebuildImageDrawer.test.tsx index 1214868b31d..d44b084c81b 100644 --- a/packages/manager/src/features/Images/RebuildImageDrawer.test.tsx +++ b/packages/manager/src/features/Images/RebuildImageDrawer.test.tsx @@ -30,7 +30,7 @@ describe('RebuildImageDrawer', () => { const { getByText } = renderWithTheme(); // Verify title renders - getByText('Restore from Image'); + getByText('Rebuild an Existing Linode from an Image'); }); it('should allow selecting a Linode to rebuild', async () => { @@ -46,7 +46,7 @@ describe('RebuildImageDrawer', () => { await userEvent.click(getByRole('combobox')); await userEvent.click(await findByText('linode-1')); - await userEvent.click(getByText('Restore Image')); + await userEvent.click(getByText('Rebuild Linode')); expect(mockHistoryPush).toBeCalledWith({ pathname: '/linodes/1/rebuild', diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/RebuildImageDrawer.tsx index 390a7d8e24c..78f263d22c0 100644 --- a/packages/manager/src/features/Images/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/RebuildImageDrawer.tsx @@ -1,16 +1,20 @@ -import { Image } from '@linode/api-v4'; +import { Typography } from '@mui/material'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useHistory } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Divider } from 'src/components/Divider'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; +import { Stack } from 'src/components/Stack'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { REBUILD_LINODE_IMAGE_PARAM_NAME } from '../Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage'; import { useImageAndLinodeGrantCheck } from './utils'; +import type { Image } from '@linode/api-v4'; + interface Props { image: Image | undefined; onClose: () => void; @@ -52,53 +56,62 @@ export const RebuildImageDrawer = (props: Props) => { onClose={onClose} onExited={reset} open={open} - title="Restore from Image" + title="Rebuild an Existing Linode from an Image" > - {formState.errors.root?.message && ( - - )} - - ( - { - field.onChange(linode?.id); - }} - optionsFilter={(linode) => - availableLinodes ? availableLinodes.includes(linode.id) : true - } - clearable={true} - errorText={fieldState.error?.message} - onBlur={field.onBlur} - value={field.value} + + {formState.errors.root?.message && ( + )} - rules={{ - required: { - message: 'Select a Linode to restore.', - value: true, - }, - }} - control={control} - name="linodeId" - /> - + + Image {image?.label} + + + + + ( + { + field.onChange(linode?.id); + }} + optionsFilter={(linode) => + availableLinodes ? availableLinodes.includes(linode.id) : true + } + clearable={true} + errorText={fieldState.error?.message} + onBlur={field.onBlur} + placeholder="Select Linode or Type to Search" + value={field.value} + /> + )} + rules={{ + required: { + message: 'Select a Linode to restore.', + value: true, + }, + }} + control={control} + name="linodeId" + /> + + + ); }; From 08d6403519f7a013f128148c48e483e34dc70280 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 17:54:00 -0400 Subject: [PATCH 19/25] Use DescriptionList --- .../src/features/Images/RebuildImageDrawer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/RebuildImageDrawer.tsx index 78f263d22c0..575bd0d4590 100644 --- a/packages/manager/src/features/Images/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/RebuildImageDrawer.tsx @@ -1,9 +1,9 @@ -import { Typography } from '@mui/material'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useHistory } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { DescriptionList } from 'src/components/DescriptionList/DescriptionList'; import { Divider } from 'src/components/Divider'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; @@ -51,7 +51,7 @@ export const RebuildImageDrawer = (props: Props) => { }); }); - return ( + return image ? ( { /> )} - - Image {image?.label} - + @@ -113,5 +113,5 @@ export const RebuildImageDrawer = (props: Props) => { /> - ); + ) : null; }; From 814003909a67a8e11df9d1fe54bb229371d3cb94 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Tue, 18 Jun 2024 18:07:45 -0400 Subject: [PATCH 20/25] Added changeset: Rebuild Linode drawer --- .../manager/.changeset/pr-10594-changed-1718748465150.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10594-changed-1718748465150.md diff --git a/packages/manager/.changeset/pr-10594-changed-1718748465150.md b/packages/manager/.changeset/pr-10594-changed-1718748465150.md new file mode 100644 index 00000000000..4f2629f6b4f --- /dev/null +++ b/packages/manager/.changeset/pr-10594-changed-1718748465150.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Rebuild Linode drawer ([#10594](https://github.com/linode/manager/pull/10594)) From 9e6871e75f2cb35565b7f25f88a543d9c7468012 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 20 Jun 2024 13:15:38 -0400 Subject: [PATCH 21/25] Add `imageServiceGen2`feature flag --- packages/manager/src/dev-tools/FeatureFlagTool.tsx | 6 ++++-- packages/manager/src/featureFlags.ts | 1 + .../features/Images/ImagesLanding/ImagesLanding.test.tsx | 4 +++- .../src/features/Images/ImagesLanding/ImagesLanding.tsx | 8 ++++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index dfc7908ec46..74986c22644 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -4,11 +4,12 @@ import * as React from 'react'; import { useDispatch } from 'react-redux'; import withFeatureFlagProvider from 'src/containers/withFeatureFlagProvider.container'; -import { FlagSet, Flags } from 'src/featureFlags'; -import { Dispatch } from 'src/hooks/types'; import { useFlags } from 'src/hooks/useFlags'; import { setMockFeatureFlags } from 'src/store/mockFeatureFlags'; import { getStorage, setStorage } from 'src/utilities/storage'; + +import type { FlagSet, Flags } from 'src/featureFlags'; +import type { Dispatch } from 'src/hooks/types'; const MOCK_FEATURE_FLAGS_STORAGE_KEY = 'devTools/mock-feature-flags'; /** @@ -24,6 +25,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' }, { flag: 'eventMessagesV2', label: 'Event Messages V2' }, { flag: 'gecko2', label: 'Gecko' }, + { flag: 'imageServiceGen2', label: 'Image Service Gen2' }, { flag: 'linodeCreateRefactor', label: 'Linode Create v2' }, { flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' }, { flag: 'objMultiCluster', label: 'OBJ Multi-Cluster' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 811f3b66097..07a945affe9 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -73,6 +73,7 @@ export interface Flags { gecko: boolean; // @TODO gecko: delete this after next release gecko2: GaFeatureFlag; gpuv2: gpuV2; + imageServiceGen2: boolean; ipv6Sharing: boolean; linodeCreateRefactor: boolean; linodeCreateWithFirewall: boolean; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx index 7536d29ec39..1a9601dcfc6 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx @@ -41,7 +41,9 @@ describe('Images Landing Table', () => { }) ); - const { getAllByText, getByTestId } = renderWithTheme(); + const { getAllByText, getByTestId } = renderWithTheme(, { + flags: { imageServiceGen2: true }, + }); // Loading state should render expect(getByTestId(loadingTestId)).toBeInTheDocument(); diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index 762d3abf7c1..c3cc58de087 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -28,6 +28,7 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' import { TableSortCell } from 'src/components/TableSortCell'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; +import { useFlags } from 'src/hooks/useFlags'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { @@ -42,11 +43,11 @@ import { } from 'src/queries/images'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; +import { getEventsForImages } from '../utils'; import { EditImageDrawer } from './EditImageDrawer'; import ImageRow from './ImageRow'; import { ImagesLandingEmptyState } from './ImagesLandingEmptyState'; import { RebuildImageDrawer } from './RebuildImageDrawer'; -import { getEventsForImages } from '../utils'; import type { Handlers as ImageHandlers } from './ImagesActionMenu'; import type { Image, ImageStatus } from '@linode/api-v4'; @@ -90,6 +91,7 @@ export const ImagesLanding = () => { const { classes } = useStyles(); const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); + const flags = useFlags(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const imageLabelFromParam = queryParams.get(searchQueryKey) ?? ''; @@ -200,7 +202,9 @@ export const ImagesLanding = () => { // TODO Image Service V2: delete after GA const multiRegionsEnabled = - manualImages?.data.some((image) => image.regions?.length) ?? false; + (flags.imageServiceGen2 && + manualImages?.data.some((image) => image.regions?.length)) ?? + false; // Automatic images with the associated events tied in. const automaticImagesEvents = getEventsForImages( From 4c3a6f84c81b6f8335f6ad3754e904ce9df72854 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 20 Jun 2024 13:23:39 -0400 Subject: [PATCH 22/25] Remove conditional render for drawer --- packages/manager/src/features/Images/RebuildImageDrawer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Images/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/RebuildImageDrawer.tsx index 575bd0d4590..4aa5cbfc49e 100644 --- a/packages/manager/src/features/Images/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/RebuildImageDrawer.tsx @@ -51,7 +51,7 @@ export const RebuildImageDrawer = (props: Props) => { }); }); - return image ? ( + return ( { )} @@ -113,5 +113,5 @@ export const RebuildImageDrawer = (props: Props) => { /> - ) : null; + ); }; From a965ce202215d2e31039706babcd46ff5bbb0980 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 21 Jun 2024 16:58:52 -0400 Subject: [PATCH 23/25] Convert ImageRow to named export --- .../src/features/Images/ImagesLanding/ImageRow.test.tsx | 2 +- .../manager/src/features/Images/ImagesLanding/ImageRow.tsx | 4 +--- .../src/features/Images/ImagesLanding/ImagesLanding.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx index c6fa1646bda..2d09bb8cbbc 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx @@ -8,7 +8,7 @@ import { wrapWithTableBody, } from 'src/utilities/testHelpers'; -import ImageRow from './ImageRow'; +import { ImageRow } from './ImageRow'; import type { Handlers } from './ImagesActionMenu'; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx index bd3e50581e2..1c3c07dacc2 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx @@ -26,7 +26,7 @@ interface Props { multiRegionsEnabled?: boolean; // TODO Image Service v2: delete after GA } -const ImageRow = (props: Props) => { +export const ImageRow = (props: Props) => { const { event, handlers, image, multiRegionsEnabled } = props; const { @@ -171,5 +171,3 @@ const ProgressDisplay: React.FC<{ ); }; - -export default React.memo(ImageRow); diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index c3cc58de087..039a71711c7 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -45,7 +45,7 @@ import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { getEventsForImages } from '../utils'; import { EditImageDrawer } from './EditImageDrawer'; -import ImageRow from './ImageRow'; +import { ImageRow } from './ImageRow'; import { ImagesLandingEmptyState } from './ImagesLandingEmptyState'; import { RebuildImageDrawer } from './RebuildImageDrawer'; From 4dba3d680cab2897a8bfeabc115facd342afdafc Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 21 Jun 2024 16:59:12 -0400 Subject: [PATCH 24/25] Fix unit test --- .../src/features/Images/ImagesLanding/ImagesLanding.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx index 1a9601dcfc6..f0e753fe8b1 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.test.tsx @@ -181,7 +181,7 @@ describe('Images Landing Table', () => { await userEvent.click(getByText('Rebuild an Existing Linode')); - getByText('Restore from Image'); + getByText('Rebuild an Existing Linode from an Image'); }); it('should allow deploying to a new Linode', async () => { From 68750dc1cde448436e6d7a73d13da0c963a03e50 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Fri, 21 Jun 2024 17:22:08 -0400 Subject: [PATCH 25/25] Update unit tests to check for image label render --- .../features/Images/ImagesLanding/RebuildImageDrawer.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx index d44b084c81b..25892bbe05e 100644 --- a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx @@ -31,6 +31,9 @@ describe('RebuildImageDrawer', () => { // Verify title renders getByText('Rebuild an Existing Linode from an Image'); + + // Verify image label is displayed + getByText(props.image.label); }); it('should allow selecting a Linode to rebuild', async () => {