Skip to content

Commit 6383a6f

Browse files
upcoming: [UIE-9502] - Add Recovery Images tab (v2) (#13432)
* Split PR, and add more changes * Added changeset: Private Image Sharing tabs new layout (v2) * Few changes * Update few remaining references * Save progress.. * Update filename * Some refactoring and add reuseable images component * Change custom to owned (query params) * Update sharegroups to share-groups * Add more tests * Update remaining share-groups references * Few updates * More changes * Add ImagesView tests for custom images * Minor changes * Added changeset: Add reusable `ImagesView` and Integrate It for the `Owned by me` Images tab (v2) * Added changeset: Add `shared` to ImageType * Added changeset: Add `ZeroStateSearchNarrowIcon` to UI package * Update changeset * Clean up old query param from ImagesSearchParams * Update custom images desc and move styles to dedicated file * Add recovery images tab v2 * Minor change * Add tests in ImagesView for Recovery Images * Add unit tests in ImageLibraryTabs for recovery images * Few minor changes to tests * Update docsLink (for owned by me images) after latest UX update * Few changes to emptyMessage.instruction type * Add some changes related to docLink * Few updates * Simplify table col config for sorting * Update Recovery Images to Recovery images * Added changeset:  Add Recovery images tab (v2) * Prevent old action routes when flag is enabled and update table styles per UX
1 parent 4e922c0 commit 6383a6f

File tree

9 files changed

+451
-108
lines changed

9 files changed

+451
-108
lines changed
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+
 Add Recovery images tab (v2) ([#13432](https://github.com/linode/manager/pull/13432))

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

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const queryMocks = vi.hoisted(() => ({
1616
useQueryWithPermissions: vi.fn().mockReturnValue({}),
1717
useLinodesPermissionsCheck: vi.fn().mockReturnValue({}),
1818
useSearch: vi.fn().mockReturnValue({}),
19+
useParams: vi.fn(),
1920
}));
2021

2122
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
@@ -29,6 +30,7 @@ vi.mock('@tanstack/react-router', async () => {
2930
...actual,
3031
useLocation: queryMocks.useLocation,
3132
useSearch: queryMocks.useSearch,
33+
useParams: queryMocks.useParams,
3234
};
3335
});
3436

@@ -61,8 +63,12 @@ describe('ImageLibraryTabs', () => {
6163
});
6264
});
6365

64-
// For Custom Images (Owned by me)
65-
describe('For Custom Images (Owned by me)', () => {
66+
// For Custom images (Owned by me)
67+
describe('For Custom images (Owned by me)', () => {
68+
beforeEach(() => {
69+
queryMocks.useParams.mockReturnValue({ imageType: 'owned-by-me' });
70+
});
71+
6672
it("should render 'Owned by me' tab", async () => {
6773
const { getByText } = renderWithTheme(<ImageLibraryTabs />, {
6874
initialRoute: '/images/image-library/owned-by-me',
@@ -71,7 +77,7 @@ describe('ImageLibraryTabs', () => {
7177
expect(getByText('Owned by me')).toBeVisible();
7278
});
7379

74-
// Test Image action navigations for CUSTOM IMAGES (Owned by me)
80+
// Test Image action navigations for CUSTOM images (Owned by me)
7581
it('should allow opening the Edit Image drawer', async () => {
7682
const image = imageFactory.build();
7783

@@ -207,6 +213,156 @@ describe('ImageLibraryTabs', () => {
207213
});
208214
});
209215

216+
// For Recovery images
217+
describe('For Recovery images', () => {
218+
beforeEach(() => {
219+
queryMocks.useParams.mockReturnValue({ imageType: 'recovery-images' });
220+
});
221+
222+
it("should render 'Recovery images' tab", async () => {
223+
const { getByText } = renderWithTheme(<ImageLibraryTabs />, {
224+
initialRoute: '/images/image-library/recovery-images',
225+
});
226+
227+
expect(getByText('Recovery images')).toBeVisible();
228+
});
229+
230+
// Test Images Action navigations for RECOVERY images
231+
it('should allow opening the Edit Image drawer', async () => {
232+
const image = imageFactory.build({ type: 'automatic' });
233+
234+
server.use(
235+
http.get('*/images', ({ request }) => {
236+
const filter = request.headers.get('x-filter');
237+
238+
if (filter?.includes('automatic')) {
239+
return HttpResponse.json(makeResourcePage([image]));
240+
}
241+
return HttpResponse.json(makeResourcePage([]));
242+
})
243+
);
244+
245+
const { getByText, findByLabelText, router } = renderWithTheme(
246+
<ImageLibraryTabs />,
247+
{
248+
initialRoute: '/images/image-library/recovery-images',
249+
}
250+
);
251+
252+
const actionMenu = await findByLabelText(
253+
`Action menu for Image ${image.label}`
254+
);
255+
await userEvent.click(actionMenu);
256+
await userEvent.click(getByText('Edit'));
257+
258+
expect(router.state.location.pathname).toBe(
259+
`/images/image-library/recovery-images/${encodeURIComponent(image.id)}/edit`
260+
);
261+
});
262+
263+
it('should allow opening the Restore Image drawer', async () => {
264+
const image = imageFactory.build();
265+
266+
server.use(
267+
http.get('*/images', ({ request }) => {
268+
const filter = request.headers.get('x-filter');
269+
270+
if (filter?.includes('automatic')) {
271+
return HttpResponse.json(makeResourcePage([image]));
272+
}
273+
return HttpResponse.json(makeResourcePage([]));
274+
})
275+
);
276+
277+
const { router, getByText, findByLabelText } = renderWithTheme(
278+
<ImageLibraryTabs />,
279+
{
280+
initialRoute: '/images/image-library/recovery-images',
281+
}
282+
);
283+
284+
const actionMenu = await findByLabelText(
285+
`Action menu for Image ${image.label}`
286+
);
287+
await userEvent.click(actionMenu);
288+
await userEvent.click(getByText('Rebuild an Existing Linode'));
289+
290+
expect(router.state.location.pathname).toBe(
291+
`/images/image-library/recovery-images/${encodeURIComponent(image.id)}/rebuild`
292+
);
293+
});
294+
295+
it('should allow deploying to a new Linode', async () => {
296+
const image = imageFactory.build();
297+
queryMocks.useLinodesPermissionsCheck.mockReturnValue({
298+
availableLinodes: [linodeFactory.build()],
299+
});
300+
301+
server.use(
302+
http.get('*/images', ({ request }) => {
303+
const filter = request.headers.get('x-filter');
304+
305+
if (filter?.includes('automatic')) {
306+
return HttpResponse.json(makeResourcePage([image]));
307+
}
308+
return HttpResponse.json(makeResourcePage([]));
309+
})
310+
);
311+
312+
const { findByLabelText, getByText, queryAllByTestId, router } =
313+
renderWithTheme(<ImageLibraryTabs />, {
314+
initialRoute: '/images/image-library/recovery-images',
315+
});
316+
317+
const loadingElement = queryAllByTestId(loadingTestId);
318+
await waitForElementToBeRemoved(loadingElement);
319+
320+
const actionMenu = await findByLabelText(
321+
`Action menu for Image ${image.label}`
322+
);
323+
await userEvent.click(actionMenu);
324+
await userEvent.click(getByText('Deploy to New Linode'));
325+
326+
expect(router.state.location.pathname).toBe('/linodes/create/images');
327+
328+
expect(router.state.location.search).toStrictEqual({
329+
imageID: image.id,
330+
});
331+
});
332+
333+
it('should allow deleting an image', async () => {
334+
const image = imageFactory.build();
335+
336+
server.use(
337+
http.get('*/images', ({ request }) => {
338+
const filter = request.headers.get('x-filter');
339+
340+
if (filter?.includes('automatic')) {
341+
return HttpResponse.json(makeResourcePage([image]));
342+
}
343+
return HttpResponse.json(makeResourcePage([]));
344+
})
345+
);
346+
347+
const { router, findByLabelText, getByText } = renderWithTheme(
348+
<ImageLibraryTabs />,
349+
{
350+
initialRoute: '/images/image-library/recovery-images',
351+
}
352+
);
353+
354+
const actionMenu = await findByLabelText(
355+
`Action menu for Image ${image.label}`
356+
);
357+
await userEvent.click(actionMenu);
358+
await userEvent.click(getByText('Delete'));
359+
360+
expect(router.state.location.pathname).toBe(
361+
`/images/image-library/recovery-images/${encodeURIComponent(image.id)}/delete`
362+
);
363+
});
364+
});
365+
210366
it('should render Owned (custom), Shared and Recovery tabs under Images Library Tab', async () => {
211367
const { getByText } = renderWithTheme(<ImageLibraryTabs />, {
212368
initialRoute: '/images/image-library',

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,7 @@ export const ImageLibraryTabs = () => {
147147
</Notice>
148148
)}
149149
{tab.type === 'recovery-images' && (
150-
// <ImagesView handlers={handlers} type="recovery-images" />
151-
<Notice variant="info">Recovery Images</Notice>
150+
<ImagesView handlers={handlers} type="recovery-images" />
152151
)}
153152
</SafeTabPanel>
154153
))}
Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
1-
import { Paper, Typography } from '@linode/ui';
1+
import { Box, Paper, Typography } from '@linode/ui';
22
import { styled } from '@mui/material/styles';
33

4-
export const StyledImageTable = styled(Paper, { label: 'StyledImageTable' })(
5-
({ theme }) => ({
6-
marginBottom: theme.spacingFunction(24),
7-
padding: 0,
8-
})
9-
);
4+
export const StyledImageContainer = styled(Paper, {
5+
label: 'StyledImageContainer',
6+
})(({ theme }) => ({
7+
border: `1px solid ${theme.tokens.alias.Border.Normal}`,
8+
marginBottom: theme.spacingFunction(24),
9+
padding: 0,
10+
}));
1011

11-
export const StyledImageTableHeader = styled('div', {
12+
export const StyledImageTableHeader = styled(Box, {
1213
label: 'StyledImageTableHeader',
1314
})(({ theme }) => ({
14-
border: `1px solid ${theme.tokens.alias.Border.Normal}`,
15-
borderBottom: 0,
16-
padding: theme.spacingFunction(8),
17-
paddingLeft: theme.spacingFunction(12),
15+
padding: `${theme.spacingFunction(16)} ${theme.spacingFunction(24)} 0`,
1816
}));
1917

2018
export const StyledImageTableSubheader = styled(Typography, {
2119
label: 'StyledImageTableSubheader',
2220
})(({ theme }) => ({
2321
marginTop: theme.spacingFunction(8),
2422
}));
23+
24+
export const StyledImageTableContainer = styled(Box, {
25+
label: 'StyledImageTableContainer',
26+
})(({ theme }) => ({
27+
padding: `${theme.spacingFunction(16)} ${theme.spacingFunction(24)} ${theme.spacingFunction(24)}`,
28+
'& .MuiTable-root': {
29+
border: 'none',
30+
},
31+
'& [data-qa-table-pagination]': {
32+
border: 'none',
33+
},
34+
}));

0 commit comments

Comments
 (0)