Skip to content

Commit a996133

Browse files
upcoming: [UIE-9409] — View Shared Image Details drawer (#13558)
1 parent cdab569 commit a996133

11 files changed

Lines changed: 473 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Private Image Sharing: add View Shared Image Details drawer ([#13558](https://github.com/linode/manager/pull/13558))

packages/manager/src/features/Images/ImagesLanding/ImagesActionMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Handlers {
1515
onEdit?: (image: Image) => void;
1616
onManageRegions?: (image: Image) => void;
1717
onRebuild?: (image: Image) => void;
18+
onView?: (image: Image) => void;
1819
}
1920

2021
interface Props {
@@ -32,7 +33,8 @@ export const ImagesActionMenu = (props: Props) => {
3233

3334
const [isOpen, setIsOpen] = React.useState<boolean>(false);
3435

35-
const { onDelete, onDeploy, onEdit, onManageRegions, onRebuild } = handlers;
36+
const { onDelete, onDeploy, onEdit, onManageRegions, onRebuild, onView } =
37+
handlers;
3638

3739
const { data: imagePermissions, isLoading: isImagePermissionsLoading } =
3840
usePermissions(
@@ -81,7 +83,7 @@ export const ImagesActionMenu = (props: Props) => {
8183
return [
8284
{
8385
title: 'View Image Details',
84-
onClick: () => null,
86+
onClick: () => onView?.(image),
8587
pendoId: pendoIDs?.actionMenu.viewImageDetails,
8688
},
8789
{ ...deployAction },

packages/manager/src/features/Images/ImagesLanding/v2/ImageLibrary/ImageLibraryTabs.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import { DeleteImageDialog } from '../../DeleteImageDialog';
1515
import { EditImageDrawer } from '../../EditImageDrawer';
1616
import { ManageImageReplicasForm } from '../../ImageRegions/ManageImageRegionsForm';
1717
import { RebuildImageDrawer } from '../../RebuildImageDrawer';
18+
import { VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS } from '../constants';
1819
import { imageLibrarySubTabs as subTabs } from './imageLibraryTabsConfig';
1920
import { ImagesView } from './ImagesView';
21+
import { ViewImageDrawer } from './ViewImageDrawer';
2022

2123
import type { Handlers as ImageHandlers } from '../../ImagesActionMenu';
2224
import type { Image } from '@linode/api-v4';
@@ -58,6 +60,10 @@ export const ImageLibraryTabs = () => {
5860
});
5961
};
6062

63+
const handleView = (image: Image) => {
64+
actionHandler(image, 'view');
65+
};
66+
6167
const handleEdit = (image: Image) => {
6268
actionHandler(image, 'edit');
6369
};
@@ -106,6 +112,7 @@ export const ImageLibraryTabs = () => {
106112
onEdit: handleEdit,
107113
onManageRegions: handleManageRegions,
108114
onRebuild: handleRebuild,
115+
onView: handleView,
109116
};
110117

111118
const subTabIndex = getSubTabIndex(subTabs, imageTypeParams?.imageType);
@@ -149,6 +156,15 @@ export const ImageLibraryTabs = () => {
149156
</TabPanels>
150157
</React.Suspense>
151158
</Tabs>
159+
<ViewImageDrawer
160+
image={selectedImage}
161+
imageError={selectedImageError}
162+
isFetching={isFetchingSelectedImage}
163+
isSharedImage={imageActionParams?.imageType === 'shared-with-me'}
164+
onClose={handleCloseDialog}
165+
open={imageActionParams?.action === 'view'}
166+
pendoIDs={VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS}
167+
/>
152168
<EditImageDrawer
153169
image={selectedImage}
154170
imageError={selectedImageError}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { styled } from '@mui/material/styles';
2+
3+
import CloudInitIcon from 'src/assets/icons/cloud-init.svg';
4+
import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip';
5+
6+
export const StyledLabel = styled('span', {
7+
label: 'StyledLabel',
8+
})(({ theme }) => ({
9+
font: theme.font.bold,
10+
}));
11+
12+
export const StyledCloudInitIcon = styled(CloudInitIcon, {
13+
label: 'StyledCloudInitIcon',
14+
})(() => ({
15+
height: 16,
16+
width: 16,
17+
}));
18+
19+
export const StyledCopyIcon = styled(CopyTooltip)(({ theme }) => ({
20+
'& svg': {
21+
height: 12,
22+
top: 1,
23+
width: 12,
24+
},
25+
marginLeft: theme.spacingFunction(4),
26+
}));
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { regionFactory } from '@linode/utilities';
2+
import React from 'react';
3+
4+
import { imageFactory } from 'src/factories';
5+
import { renderWithTheme } from 'src/utilities/testHelpers';
6+
7+
import { ViewImageDrawer } from './ViewImageDrawer';
8+
9+
import type { VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS } from '../constants';
10+
11+
const mockRegions = regionFactory.buildList(2, {
12+
id: 'us-east',
13+
label: 'Newark, NJ',
14+
country: 'us',
15+
});
16+
17+
const queryMocks = vi.hoisted(() => ({
18+
useRegionsQuery: vi.fn(),
19+
}));
20+
21+
vi.mock('@linode/queries', async () => {
22+
const actual = await vi.importActual('@linode/queries');
23+
return {
24+
...actual,
25+
useRegionsQuery: queryMocks.useRegionsQuery,
26+
};
27+
});
28+
29+
beforeEach(() => {
30+
queryMocks.useRegionsQuery.mockReturnValue({ data: mockRegions });
31+
});
32+
33+
const onClose = vi.fn();
34+
35+
const baseImage = imageFactory.build({
36+
capabilities: ['distributed-sites'],
37+
created: '2024-01-15T00:00:00',
38+
description: 'A test image description',
39+
id: 'private/123',
40+
image_sharing: {
41+
shared_by: {
42+
sharegroup_id: 1,
43+
sharegroup_label: 'my-share-group',
44+
sharegroup_uuid: 'abc-uuid',
45+
source_image_id: 123,
46+
},
47+
shared_with: null,
48+
},
49+
label: 'my-test-image',
50+
regions: [{ region: 'us-east', status: 'available' }],
51+
size: 1500,
52+
total_size: 3000,
53+
});
54+
55+
const defaultProps = {
56+
image: baseImage,
57+
imageError: null,
58+
isFetching: false,
59+
isSharedImage: true,
60+
onClose,
61+
open: true,
62+
pendoIDs: {} as typeof VIEW_SHARED_IMAGE_DETAILS_DRAWER_PENDO_IDS,
63+
};
64+
65+
describe('ViewImageDrawer', () => {
66+
it('renders the drawer title', () => {
67+
const { getByText } = renderWithTheme(
68+
<ViewImageDrawer {...defaultProps} />
69+
);
70+
71+
expect(getByText('View shared image details')).toBeVisible();
72+
});
73+
74+
it('renders image label', () => {
75+
const { getByText } = renderWithTheme(
76+
<ViewImageDrawer {...defaultProps} />
77+
);
78+
79+
expect(getByText('my-test-image')).toBeVisible();
80+
});
81+
82+
it('renders the image ID', () => {
83+
const { getByText } = renderWithTheme(
84+
<ViewImageDrawer {...defaultProps} />
85+
);
86+
87+
expect(getByText('private/123')).toBeVisible();
88+
});
89+
90+
it('renders the share group label', () => {
91+
const { getByText } = renderWithTheme(
92+
<ViewImageDrawer {...defaultProps} />
93+
);
94+
95+
expect(getByText(/my-share-group/)).toBeVisible();
96+
});
97+
98+
it('renders original image size and total replica size', () => {
99+
const { getByText } = renderWithTheme(
100+
<ViewImageDrawer {...defaultProps} />
101+
);
102+
103+
expect(getByText(/1500 MB/)).toBeVisible();
104+
expect(getByText(/3000 MB/)).toBeVisible();
105+
});
106+
107+
it('renders created date', () => {
108+
const { getByText } = renderWithTheme(
109+
<ViewImageDrawer {...defaultProps} />
110+
);
111+
112+
expect(getByText('2024-01-15T00:00:00')).toBeVisible();
113+
});
114+
115+
it('renders Encrypted when image has the distributed-sites capability', () => {
116+
const { getByTestId, queryByText } = renderWithTheme(
117+
<ViewImageDrawer {...defaultProps} />
118+
);
119+
120+
expect(getByTestId('encrypted-indicator')).toBeVisible();
121+
expect(queryByText('Not Encrypted')).toBeNull();
122+
});
123+
124+
it('renders Not Encrypted when image lacks the distributed-sites capability', () => {
125+
const image = imageFactory.build({
126+
...baseImage,
127+
capabilities: [],
128+
});
129+
130+
const { getByTestId, queryByText } = renderWithTheme(
131+
<ViewImageDrawer {...defaultProps} image={image} />
132+
);
133+
134+
expect(getByTestId('not-encrypted-indicator')).toBeVisible();
135+
expect(queryByText('Encrypted')).toBeNull();
136+
});
137+
138+
it('renders the Cloud-Init metadata notice when image has the cloud-init capability', () => {
139+
const image = imageFactory.build({
140+
...baseImage,
141+
capabilities: ['cloud-init'],
142+
});
143+
144+
const { getByText } = renderWithTheme(
145+
<ViewImageDrawer {...defaultProps} image={image} />
146+
);
147+
148+
expect(getByText('Supports Metadata service via Cloud-Init')).toBeVisible();
149+
});
150+
151+
it('does not render the Cloud-Init metadata notice when image lacks the cloud-init capability', () => {
152+
const { queryByText } = renderWithTheme(
153+
<ViewImageDrawer {...defaultProps} />
154+
);
155+
156+
expect(queryByText('Supports Metadata service via Cloud-Init')).toBeNull();
157+
});
158+
159+
it('renders the description when present', () => {
160+
const { getByText } = renderWithTheme(
161+
<ViewImageDrawer {...defaultProps} />
162+
);
163+
164+
expect(getByText('A test image description')).toBeVisible();
165+
});
166+
167+
it('does not render the description section when description is absent', () => {
168+
const image = imageFactory.build({ ...baseImage, description: null });
169+
170+
const { queryByText } = renderWithTheme(
171+
<ViewImageDrawer {...defaultProps} image={image} />
172+
);
173+
174+
expect(queryByText('Description')).toBeNull();
175+
});
176+
177+
it('renders the replicated region with flag and label', () => {
178+
queryMocks.useRegionsQuery.mockReturnValue({
179+
data: [
180+
regionFactory.build({
181+
id: 'us-east',
182+
label: 'Newark, NJ',
183+
country: 'us',
184+
}),
185+
],
186+
});
187+
188+
const { getByText } = renderWithTheme(
189+
<ViewImageDrawer {...defaultProps} />
190+
);
191+
192+
expect(getByText('Newark, NJ')).toBeVisible();
193+
});
194+
195+
it('renders Unknown for unrecognized region', () => {
196+
queryMocks.useRegionsQuery.mockReturnValue({ data: [] });
197+
198+
const { getByText } = renderWithTheme(
199+
<ViewImageDrawer {...defaultProps} />
200+
);
201+
202+
expect(getByText('Unknown')).toBeVisible();
203+
});
204+
205+
it('calls onClose when the Close button is clicked', async () => {
206+
const { getByTestId } = renderWithTheme(
207+
<ViewImageDrawer {...defaultProps} />
208+
);
209+
210+
getByTestId('cancel').click();
211+
212+
expect(onClose).toHaveBeenCalled();
213+
});
214+
215+
it('renders nothing in the drawer body when no image is provided', () => {
216+
const { queryByText } = renderWithTheme(
217+
<ViewImageDrawer {...defaultProps} image={undefined} />
218+
);
219+
220+
expect(queryByText('my-test-image')).toBeNull();
221+
expect(queryByText('private/123')).toBeNull();
222+
});
223+
});

0 commit comments

Comments
 (0)