From 1ee88d69a1550bb70514b594e0483fcc8dc3b80a Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 3 Apr 2026 15:46:40 -0400 Subject: [PATCH 1/6] View Image Details button functionality; View shared image details drawer --- .../Images/ImagesLanding/ImagesActionMenu.tsx | 6 +- .../v2/ImageLibrary/ImageLibraryTabs.tsx | 13 ++ .../v2/ShareGroups/ViewSharedImageDrawer.tsx | 128 ++++++++++++++++++ packages/manager/src/features/Images/utils.ts | 14 +- packages/manager/src/routes/images/index.ts | 1 + 5 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.tsx diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx index 33e81f013e0..b4725314e61 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx @@ -15,6 +15,7 @@ export interface Handlers { onEdit?: (image: Image) => void; onManageRegions?: (image: Image) => void; onRebuild?: (image: Image) => void; + onView?: (image: Image) => void; } interface Props { @@ -32,7 +33,8 @@ export const ImagesActionMenu = (props: Props) => { const [isOpen, setIsOpen] = React.useState(false); - const { onDelete, onDeploy, onEdit, onManageRegions, onRebuild } = handlers; + const { onDelete, onDeploy, onEdit, onManageRegions, onRebuild, onView } = + handlers; const { data: imagePermissions, isLoading: isImagePermissionsLoading } = usePermissions( @@ -81,7 +83,7 @@ export const ImagesActionMenu = (props: Props) => { return [ { title: 'View Image Details', - onClick: () => null, + onClick: () => onView?.(image), pendoId: pendoIDs?.actionMenu.viewImageDetails, }, { ...deployAction }, diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx index 796786111e8..6a4ba406d78 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx @@ -15,6 +15,7 @@ import { DeleteImageDialog } from '../../DeleteImageDialog'; import { EditImageDrawer } from '../../EditImageDrawer'; import { ManageImageReplicasForm } from '../../ImageRegions/ManageImageRegionsForm'; import { RebuildImageDrawer } from '../../RebuildImageDrawer'; +import { ViewSharedImageDrawer } from '../ShareGroups/ViewSharedImageDrawer'; import { imageLibrarySubTabs as subTabs } from './imageLibraryTabsConfig'; import { ImagesView } from './ImagesView'; @@ -58,6 +59,10 @@ export const ImageLibraryTabs = () => { }); }; + const handleView = (image: Image) => { + actionHandler(image, 'view'); + }; + const handleEdit = (image: Image) => { actionHandler(image, 'edit'); }; @@ -106,6 +111,7 @@ export const ImageLibraryTabs = () => { onEdit: handleEdit, onManageRegions: handleManageRegions, onRebuild: handleRebuild, + onView: handleView, }; const subTabIndex = getSubTabIndex(subTabs, imageTypeParams?.imageType); @@ -149,6 +155,13 @@ export const ImageLibraryTabs = () => { + void; + open: boolean; +} + +export const ViewSharedImageDrawer = (props: Props) => { + const { imageError, isFetching, onClose, open, image } = props; + + const { data: regions } = useRegionsQuery(); + + return ( + + + + Label: {image?.label} + + + Image ID:{' '} + + + + Share group:{' '} + {image?.image_sharing?.shared_by?.sharegroup_label} + + + Original image size: {image?.size} MB + + + All replicas: {image?.total_size} MB + + + Created: {image?.created} + + {image?.capabilities?.includes('distributed-sites') ? ( + + + Encrypted + + ) : ( + + + Not Encrypted + + )} + {image?.capabilities?.includes('cloud-init') && ( + + + Supports Metadata service via Cloud-Init + + )} + {image?.description && ( + + + Description + {image.description} + + + )} + + Replicated in the following regions:{' '} + {image?.regions.map((region) => { + const countryAndLabelObject = getCountryAndLabelFromImageRegion( + regions ?? [], + region + ); + + const imageCountry = countryAndLabelObject.country ?? 'us'; + const regionLabel = countryAndLabelObject.label ?? 'Unknown'; + + return ( + + + {regionLabel} + + ); + })} + + + + + ); +}; + +const StyledLabel = styled('span', { + label: 'StyledLabel', +})(({ theme }) => ({ + font: theme.font.bold, +})); + +const StyledCloudInitIcon = styled(CloudInitIcon, { + label: 'StyledCloudInitIcon', +})(() => ({ + height: 16, + width: 16, +})); diff --git a/packages/manager/src/features/Images/utils.ts b/packages/manager/src/features/Images/utils.ts index c989aa5c21d..2065ec7d32f 100644 --- a/packages/manager/src/features/Images/utils.ts +++ b/packages/manager/src/features/Images/utils.ts @@ -3,7 +3,7 @@ import { useRegionsQuery } from '@linode/queries'; import { DISALLOWED_IMAGE_REGIONS } from 'src/constants'; import { useFlags } from 'src/hooks/useFlags'; -import type { Event, Image, Linode } from '@linode/api-v4'; +import type { Event, Image, ImageRegion, Linode, Region } from '@linode/api-v4'; export type ImageLibraryType = | 'owned-by-me' @@ -118,3 +118,15 @@ export const getImageTypeToImageLibraryType = ( return 'shared-with-me'; } }; + +export const getCountryAndLabelFromImageRegion = ( + regions: Region[], + imageRegion: ImageRegion +) => { + const matchingRegion = regions?.find((r) => r.id === imageRegion.region); + + return { + country: matchingRegion?.country, + label: matchingRegion?.label, + }; +}; diff --git a/packages/manager/src/routes/images/index.ts b/packages/manager/src/routes/images/index.ts index 0e48c5b2c2a..87d824643af 100644 --- a/packages/manager/src/routes/images/index.ts +++ b/packages/manager/src/routes/images/index.ts @@ -45,6 +45,7 @@ const imageActions = { edit: 'edit', 'manage-replicas': 'manage-replicas', rebuild: 'rebuild', + view: 'view', } as const; export type ImageAction = (typeof imageActions)[keyof typeof imageActions]; From e61937a8f84e14ec63f848f1d49279050dc59a9e Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 3 Apr 2026 18:08:50 -0400 Subject: [PATCH 2/6] Unit tests for ViewSharedImageDrawer component and getCountryAndLabelFromImageRegion() helper function --- .../ViewSharedImageDrawer.test.tsx | 219 ++++++++++++++++++ .../v2/ShareGroups/ViewSharedImageDrawer.tsx | 6 +- .../src/features/Images/utils.test.tsx | 38 ++- 3 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx new file mode 100644 index 00000000000..0ab6d9706c5 --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx @@ -0,0 +1,219 @@ +import { regionFactory } from '@linode/utilities'; +import React from 'react'; + +import { imageFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { ViewSharedImageDrawer } from './ViewSharedImageDrawer'; + +const mockRegions = regionFactory.buildList(2, { + id: 'us-east', + label: 'Newark, NJ', + country: 'us', +}); + +const queryMocks = vi.hoisted(() => ({ + useRegionsQuery: vi.fn(), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useRegionsQuery: queryMocks.useRegionsQuery, + }; +}); + +beforeEach(() => { + queryMocks.useRegionsQuery.mockReturnValue({ data: mockRegions }); +}); + +const onClose = vi.fn(); + +const baseImage = imageFactory.build({ + capabilities: ['distributed-sites'], + created: '2024-01-15T00:00:00', + description: 'A test image description', + id: 'private/123', + image_sharing: { + shared_by: { + sharegroup_id: 1, + sharegroup_label: 'my-share-group', + sharegroup_uuid: 'abc-uuid', + source_image_id: 123, + }, + shared_with: null, + }, + label: 'my-test-image', + regions: [{ region: 'us-east', status: 'available' }], + size: 1500, + total_size: 3000, +}); + +const defaultProps = { + image: baseImage, + imageError: null, + isFetching: false, + onClose, + open: true, +}; + +describe('ViewSharedImageDrawer', () => { + it('renders the drawer title', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('View shared image details')).toBeVisible(); + }); + + it('renders image label', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('my-test-image')).toBeVisible(); + }); + + it('renders the image ID', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('private/123')).toBeVisible(); + }); + + it('renders the share group label', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('my-share-group')).toBeVisible(); + }); + + it('renders original image size and total replica size', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText(/1500 MB/)).toBeVisible(); + expect(getByText(/3000 MB/)).toBeVisible(); + }); + + it('renders created date', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('2024-01-15T00:00:00')).toBeVisible(); + }); + + it('renders Encrypted when image has the distributed-sites capability', () => { + const { getByTestId, queryByText } = renderWithTheme( + + ); + + expect(getByTestId('encrypted-indicator')).toBeVisible(); + expect(queryByText('Not Encrypted')).toBeNull(); + }); + + it('renders Not Encrypted when image lacks the distributed-sites capability', () => { + const image = imageFactory.build({ + ...baseImage, + capabilities: [], + }); + + const { getByTestId, queryByText } = renderWithTheme( + + ); + + expect(getByTestId('not-encrypted-indicator')).toBeVisible(); + expect(queryByText('Encrypted')).toBeNull(); + }); + + it('renders the Cloud-Init metadata notice when image has the cloud-init capability', () => { + const image = imageFactory.build({ + ...baseImage, + capabilities: ['cloud-init'], + }); + + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Supports Metadata service via Cloud-Init')).toBeVisible(); + }); + + it('does not render the Cloud-Init metadata notice when image lacks the cloud-init capability', () => { + const { queryByText } = renderWithTheme( + + ); + + expect(queryByText('Supports Metadata service via Cloud-Init')).toBeNull(); + }); + + it('renders the description when present', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('A test image description')).toBeVisible(); + }); + + it('does not render the description section when description is absent', () => { + const image = imageFactory.build({ ...baseImage, description: null }); + + const { queryByText } = renderWithTheme( + + ); + + expect(queryByText('Description')).toBeNull(); + }); + + it('renders the replicated region with flag and label', () => { + queryMocks.useRegionsQuery.mockReturnValue({ + data: [ + regionFactory.build({ + id: 'us-east', + label: 'Newark, NJ', + country: 'us', + }), + ], + }); + + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Newark, NJ')).toBeVisible(); + }); + + it('renders Unknown for unrecognized region', () => { + queryMocks.useRegionsQuery.mockReturnValue({ data: [] }); + + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Unknown')).toBeVisible(); + }); + + it('calls onClose when the Close button is clicked', async () => { + const { getByTestId } = renderWithTheme( + + ); + + getByTestId('cancel').click(); + + expect(onClose).toHaveBeenCalled(); + }); + + it('renders nothing in the drawer body when no image is provided', () => { + const { queryByText } = renderWithTheme( + + ); + + expect(queryByText('my-test-image')).toBeNull(); + expect(queryByText('private/123')).toBeNull(); + }); +}); diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.tsx index 2c5edc0356a..ad1b1748ade 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.tsx @@ -56,12 +56,14 @@ export const ViewSharedImageDrawer = (props: Props) => { {image?.capabilities?.includes('distributed-sites') ? ( - Encrypted + Encrypted ) : ( - Not Encrypted + + Not Encrypted + )} {image?.capabilities?.includes('cloud-init') && ( diff --git a/packages/manager/src/features/Images/utils.test.tsx b/packages/manager/src/features/Images/utils.test.tsx index e7ab420a401..8be3bc05bee 100644 --- a/packages/manager/src/features/Images/utils.test.tsx +++ b/packages/manager/src/features/Images/utils.test.tsx @@ -1,10 +1,11 @@ -import { linodeFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { renderHook, waitFor } from '@testing-library/react'; import { eventFactory, imageFactory } from 'src/factories'; import { wrapWithTheme } from 'src/utilities/testHelpers'; import { + getCountryAndLabelFromImageRegion, getEventsForImages, getImageLabelForLinode, getImageTypeToImageLibraryType, @@ -121,6 +122,41 @@ describe('getSubTabIndex', () => { }); }); +describe('getCountryAndLabelFromImageRegion', () => { + it('returns the country and label when a matching region is found', () => { + const regions = regionFactory.buildList(1, { + id: 'us-east', + label: 'Newark, NJ', + country: 'us', + }); + const imageRegion = { region: 'us-east', status: 'available' as const }; + + expect(getCountryAndLabelFromImageRegion(regions, imageRegion)).toEqual({ + country: 'us', + label: 'Newark, NJ', + }); + }); + + it('returns undefined for country and label when no matching region is found', () => { + const regions = regionFactory.buildList(1, { id: 'us-west' }); + const imageRegion = { region: 'us-east', status: 'available' as const }; + + expect(getCountryAndLabelFromImageRegion(regions, imageRegion)).toEqual({ + country: undefined, + label: undefined, + }); + }); + + it('returns undefined for country and label when regions array is empty', () => { + const imageRegion = { region: 'us-east', status: 'available' as const }; + + expect(getCountryAndLabelFromImageRegion([], imageRegion)).toEqual({ + country: undefined, + label: undefined, + }); + }); +}); + describe('getImageTypeToImageLibraryType', () => { it('returns "owned-by-me" when image type is "manual"', () => { expect(getImageTypeToImageLibraryType('manual')).toBe('owned-by-me'); From dfde5cb8677e528329679e857cd0c226416d4fbc Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 3 Apr 2026 20:35:36 -0400 Subject: [PATCH 3/6] Adjustments to make View Image drawer more generic to accommodate non-shared images later --- .../v2/ImageLibrary/ImageLibraryTabs.tsx | 7 +++- ...redImageDrawer.tsx => ViewImageDrawer.tsx} | 29 ++++++++++---- .../ViewSharedImageDrawer.test.tsx | 39 ++++++++++--------- .../Images/ImagesLanding/v2/constants.ts | 6 +++ packages/ui/src/components/Drawer/Drawer.tsx | 6 +++ 5 files changed, 60 insertions(+), 27 deletions(-) rename packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/{ViewSharedImageDrawer.tsx => ViewImageDrawer.tsx} (84%) diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx index 6a4ba406d78..df1a49a8890 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx @@ -15,7 +15,8 @@ import { DeleteImageDialog } from '../../DeleteImageDialog'; import { EditImageDrawer } from '../../EditImageDrawer'; import { ManageImageReplicasForm } from '../../ImageRegions/ManageImageRegionsForm'; import { RebuildImageDrawer } from '../../RebuildImageDrawer'; -import { ViewSharedImageDrawer } from '../ShareGroups/ViewSharedImageDrawer'; +import { VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS } from '../constants'; +import { ViewImageDrawer } from '../ShareGroups/ViewImageDrawer'; import { imageLibrarySubTabs as subTabs } from './imageLibraryTabsConfig'; import { ImagesView } from './ImagesView'; @@ -155,12 +156,14 @@ export const ImageLibraryTabs = () => { - void; open: boolean; + pendoIDs: typeof VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS; } -export const ViewSharedImageDrawer = (props: Props) => { - const { imageError, isFetching, onClose, open, image } = props; +export const ViewImageDrawer = (props: Props) => { + const { + imageError, + isFetching, + isSharedImage, + onClose, + open, + pendoIDs, + image, + } = props; const { data: regions } = useRegionsQuery(); @@ -30,7 +41,8 @@ export const ViewSharedImageDrawer = (props: Props) => { isFetching={isFetching} onClose={onClose} open={open} - title="View shared image details" + pendoId={pendoIDs.xButton} + title={`View ${isSharedImage ? 'shared ' : ''}image details`} > @@ -40,10 +52,12 @@ export const ViewSharedImageDrawer = (props: Props) => { Image ID:{' '} - - Share group:{' '} - {image?.image_sharing?.shared_by?.sharegroup_label} - + {isSharedImage && ( + + Share group:{' '} + {image?.image_sharing?.shared_by?.sharegroup_label} + + )} Original image size: {image?.size} MB @@ -106,6 +120,7 @@ export const ViewSharedImageDrawer = (props: Props) => { { +describe('ViewImageDrawer', () => { it('renders the drawer title', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('View shared image details')).toBeVisible(); @@ -69,7 +72,7 @@ describe('ViewSharedImageDrawer', () => { it('renders image label', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('my-test-image')).toBeVisible(); @@ -77,7 +80,7 @@ describe('ViewSharedImageDrawer', () => { it('renders the image ID', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('private/123')).toBeVisible(); @@ -85,7 +88,7 @@ describe('ViewSharedImageDrawer', () => { it('renders the share group label', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('my-share-group')).toBeVisible(); @@ -93,7 +96,7 @@ describe('ViewSharedImageDrawer', () => { it('renders original image size and total replica size', () => { const { getByText } = renderWithTheme( - + ); expect(getByText(/1500 MB/)).toBeVisible(); @@ -102,7 +105,7 @@ describe('ViewSharedImageDrawer', () => { it('renders created date', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('2024-01-15T00:00:00')).toBeVisible(); @@ -110,7 +113,7 @@ describe('ViewSharedImageDrawer', () => { it('renders Encrypted when image has the distributed-sites capability', () => { const { getByTestId, queryByText } = renderWithTheme( - + ); expect(getByTestId('encrypted-indicator')).toBeVisible(); @@ -124,7 +127,7 @@ describe('ViewSharedImageDrawer', () => { }); const { getByTestId, queryByText } = renderWithTheme( - + ); expect(getByTestId('not-encrypted-indicator')).toBeVisible(); @@ -138,7 +141,7 @@ describe('ViewSharedImageDrawer', () => { }); const { getByText } = renderWithTheme( - + ); expect(getByText('Supports Metadata service via Cloud-Init')).toBeVisible(); @@ -146,7 +149,7 @@ describe('ViewSharedImageDrawer', () => { it('does not render the Cloud-Init metadata notice when image lacks the cloud-init capability', () => { const { queryByText } = renderWithTheme( - + ); expect(queryByText('Supports Metadata service via Cloud-Init')).toBeNull(); @@ -154,7 +157,7 @@ describe('ViewSharedImageDrawer', () => { it('renders the description when present', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('A test image description')).toBeVisible(); @@ -164,7 +167,7 @@ describe('ViewSharedImageDrawer', () => { const image = imageFactory.build({ ...baseImage, description: null }); const { queryByText } = renderWithTheme( - + ); expect(queryByText('Description')).toBeNull(); @@ -182,7 +185,7 @@ describe('ViewSharedImageDrawer', () => { }); const { getByText } = renderWithTheme( - + ); expect(getByText('Newark, NJ')).toBeVisible(); @@ -192,7 +195,7 @@ describe('ViewSharedImageDrawer', () => { queryMocks.useRegionsQuery.mockReturnValue({ data: [] }); const { getByText } = renderWithTheme( - + ); expect(getByText('Unknown')).toBeVisible(); @@ -200,7 +203,7 @@ describe('ViewSharedImageDrawer', () => { it('calls onClose when the Close button is clicked', async () => { const { getByTestId } = renderWithTheme( - + ); getByTestId('cancel').click(); @@ -210,7 +213,7 @@ describe('ViewSharedImageDrawer', () => { it('renders nothing in the drawer body when no image is provided', () => { const { queryByText } = renderWithTheme( - + ); expect(queryByText('my-test-image')).toBeNull(); diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts b/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts index 1d50213647e..1e9c94832eb 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts +++ b/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts @@ -1 +1,7 @@ export const DEFAULT_PAGE_SIZES = [25, 50, 75, 100]; + +// Shared Image drawer Pendo IDs +export const VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS = { + xButton: 'Images Library Shared View-X button', + closeButton: 'Images Library Shared View-Close', +}; diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/ui/src/components/Drawer/Drawer.tsx index c1486bf620f..c727b856bd5 100644 --- a/packages/ui/src/components/Drawer/Drawer.tsx +++ b/packages/ui/src/components/Drawer/Drawer.tsx @@ -35,6 +35,10 @@ export interface DrawerProps extends _DrawerProps { * If true, the drawer will feature a loading spinner for its content. */ isFetching?: boolean; + /** + * Optional Pendo ID for tracking clicks on the X icon that closes the drawer. + */ + pendoId?: string; /** * Title that appears at the top of the drawer */ @@ -72,6 +76,7 @@ export const Drawer = React.forwardRef( isFetching, onClose, open, + pendoId, sx, title, titleSuffix, @@ -204,6 +209,7 @@ export const Drawer = React.forwardRef( onClose?.({}, 'escapeKeyDown')} size="large" From 1939c951e43a31c02b2e1f660fa54ed84ffec808 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 3 Apr 2026 22:15:08 -0400 Subject: [PATCH 4/6] Move ViewImageDrawer to /ImageLibrary; update Pendo IDs --- .../v2/ImageLibrary/ImageLibraryTabs.tsx | 2 +- .../ViewImageDrawer.test.tsx} | 3 +- .../ViewImageDrawer.tsx | 68 +++++++++++-------- .../Images/ImagesLanding/v2/constants.ts | 1 + 4 files changed, 42 insertions(+), 32 deletions(-) rename packages/manager/src/features/Images/ImagesLanding/v2/{ShareGroups/ViewSharedImageDrawer.test.tsx => ImageLibrary/ViewImageDrawer.test.tsx} (98%) rename packages/manager/src/features/Images/ImagesLanding/v2/{ShareGroups => ImageLibrary}/ViewImageDrawer.tsx (73%) diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx index df1a49a8890..229f29ec3b1 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx @@ -16,9 +16,9 @@ import { EditImageDrawer } from '../../EditImageDrawer'; import { ManageImageReplicasForm } from '../../ImageRegions/ManageImageRegionsForm'; import { RebuildImageDrawer } from '../../RebuildImageDrawer'; import { VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS } from '../constants'; -import { ViewImageDrawer } from '../ShareGroups/ViewImageDrawer'; import { imageLibrarySubTabs as subTabs } from './imageLibraryTabsConfig'; import { ImagesView } from './ImagesView'; +import { ViewImageDrawer } from './ViewImageDrawer'; import type { Handlers as ImageHandlers } from '../../ImagesActionMenu'; import type { Image } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.test.tsx similarity index 98% rename from packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx rename to packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.test.tsx index 79628d9c0a1..e91019af1a5 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewSharedImageDrawer.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.test.tsx @@ -56,6 +56,7 @@ const defaultProps = { image: baseImage, imageError: null, isFetching: false, + isSharedImage: true, onClose, open: true, pendoIDs: {} as typeof VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS, @@ -91,7 +92,7 @@ describe('ViewImageDrawer', () => { ); - expect(getByText('my-share-group')).toBeVisible(); + expect(getByText(/my-share-group/)).toBeVisible(); }); it('renders original image size and total replica size', () => { diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx similarity index 73% rename from packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewImageDrawer.tsx rename to packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx index 11569cec89e..6b0442af77d 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ShareGroups/ViewImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx @@ -49,8 +49,11 @@ export const ViewImageDrawer = (props: Props) => { Label: {image?.label} - Image ID:{' '} - + Image ID: {image?.id} + {isSharedImage && ( @@ -87,37 +90,33 @@ export const ViewImageDrawer = (props: Props) => { )} {image?.description && ( - - - Description - {image.description} - - + + Description + {image.description} + )} - - Replicated in the following regions:{' '} - {image?.regions.map((region) => { - const countryAndLabelObject = getCountryAndLabelFromImageRegion( - regions ?? [], - region - ); + Replicated in the following regions:{' '} + {image?.regions.map((region) => { + const countryAndLabelObject = getCountryAndLabelFromImageRegion( + regions ?? [], + region + ); - const imageCountry = countryAndLabelObject.country ?? 'us'; - const regionLabel = countryAndLabelObject.label ?? 'Unknown'; + const imageCountry = countryAndLabelObject.country ?? 'us'; + const regionLabel = countryAndLabelObject.label ?? 'Unknown'; - return ( - - - {regionLabel} - - ); - })} - + return ( + + + {regionLabel} + + ); + })} ({ + '& svg': { + height: 12, + top: 1, + width: 12, + }, + marginLeft: theme.spacingFunction(4), +})); diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts b/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts index 1e9c94832eb..e18b2c1aaad 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts +++ b/packages/manager/src/features/Images/ImagesLanding/v2/constants.ts @@ -4,4 +4,5 @@ export const DEFAULT_PAGE_SIZES = [25, 50, 75, 100]; export const VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS = { xButton: 'Images Library Shared View-X button', closeButton: 'Images Library Shared View-Close', + copyImageIdIcon: 'Images Library Shared View-Copy ID', }; From 032718d0688572c1cf65210a2adfb94e150c3f9a Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Fri, 3 Apr 2026 22:17:22 -0400 Subject: [PATCH 5/6] Added changeset: Private Image Sharing: add View Shared Image Details drawer --- .../.changeset/pr-13558-upcoming-features-1775269042594.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13558-upcoming-features-1775269042594.md diff --git a/packages/manager/.changeset/pr-13558-upcoming-features-1775269042594.md b/packages/manager/.changeset/pr-13558-upcoming-features-1775269042594.md new file mode 100644 index 00000000000..e04813ea9f1 --- /dev/null +++ b/packages/manager/.changeset/pr-13558-upcoming-features-1775269042594.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Private Image Sharing: add View Shared Image Details drawer ([#13558](https://github.com/linode/manager/pull/13558)) From d74a3e65b92b1dd4e67061552070596291685bd6 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Thu, 9 Apr 2026 14:02:04 -0400 Subject: [PATCH 6/6] ViewImageDrawer.styles.ts --- .../v2/ImageLibrary/ViewImageDrawer.styles.ts | 26 +++++++++++++++ .../v2/ImageLibrary/ViewImageDrawer.tsx | 32 ++++--------------- 2 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.styles.ts diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.styles.ts b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.styles.ts new file mode 100644 index 00000000000..9af6628d388 --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.styles.ts @@ -0,0 +1,26 @@ +import { styled } from '@mui/material/styles'; + +import CloudInitIcon from 'src/assets/icons/cloud-init.svg'; +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; + +export const StyledLabel = styled('span', { + label: 'StyledLabel', +})(({ theme }) => ({ + font: theme.font.bold, +})); + +export const StyledCloudInitIcon = styled(CloudInitIcon, { + label: 'StyledCloudInitIcon', +})(() => ({ + height: 16, + width: 16, +})); + +export const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({ + '& svg': { + height: 12, + top: 1, + width: 12, + }, + marginLeft: theme.spacingFunction(4), +})); diff --git a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx index 6b0442af77d..35d31a6739b 100644 --- a/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ViewImageDrawer.tsx @@ -1,14 +1,18 @@ import { useRegionsQuery } from '@linode/queries'; -import { ActionsPanel, Drawer, Stack, styled, Typography } from '@linode/ui'; +import { ActionsPanel, Drawer, Stack, Typography } from '@linode/ui'; import React from 'react'; -import CloudInitIcon from 'src/assets/icons/cloud-init.svg'; import Lock from 'src/assets/icons/lock.svg'; import Unlock from 'src/assets/icons/unlock.svg'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { Flag } from 'src/components/Flag'; import { getCountryAndLabelFromImageRegion } from 'src/features/Images/utils'; +import { + StyledCloudInitIcon, + StyledCopyIcon, + StyledLabel, +} from './ViewImageDrawer.styles'; + import type { VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS } from '../constants'; import type { APIError, Image } from '@linode/api-v4'; @@ -129,25 +133,3 @@ export const ViewImageDrawer = (props: Props) => { ); }; - -const StyledLabel = styled('span', { - label: 'StyledLabel', -})(({ theme }) => ({ - font: theme.font.bold, -})); - -const StyledCloudInitIcon = styled(CloudInitIcon, { - label: 'StyledCloudInitIcon', -})(() => ({ - height: 16, - width: 16, -})); - -const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({ - '& svg': { - height: 12, - top: 1, - width: 12, - }, - marginLeft: theme.spacingFunction(4), -}));