Skip to content

Commit badc6f9

Browse files
upcoming: [UIE-9502] - Add Reusable ImagesView and Integrate It for the Custom (Owned by Me) Images Tab (v2) (#13418)
* 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 * Minor change * Update docsLink (for owned by me images) after latest UX update * Few updates
1 parent 0664a55 commit badc6f9

20 files changed

+1275
-38
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Upcoming Features
3+
---
4+
5+
Add `shared` to ImageType ([#13418](https://github.com/linode/manager/pull/13418))

packages/api-v4/src/images/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export type ImageStatus = 'available' | 'creating' | 'pending_upload';
22

33
export type ImageCapabilities = 'cloud-init' | 'distributed-sites';
44

5-
type ImageType = 'automatic' | 'manual';
5+
type ImageType = 'automatic' | 'manual' | 'shared';
66

77
type SharegroupMemberStatus = 'active' | 'revoked';
88

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 reusable `ImagesView` and `ImagesTable` components, and integrated them for the `Owned by me` Images tab (v2) ([#13418](https://github.com/linode/manager/pull/13418))
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { linodeFactory } from '@linode/utilities';
2+
import { waitForElementToBeRemoved } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import * as React from 'react';
5+
6+
import { imageFactory } from 'src/factories';
7+
import { makeResourcePage } from 'src/mocks/serverHandlers';
8+
import { http, HttpResponse, server } from 'src/mocks/testServer';
9+
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
10+
11+
import { ImageLibraryTabs } from './ImageLibraryTabs';
12+
13+
const queryMocks = vi.hoisted(() => ({
14+
useLocation: vi.fn(),
15+
usePermissions: vi.fn().mockReturnValue({ data: { create_image: false } }),
16+
useQueryWithPermissions: vi.fn().mockReturnValue({}),
17+
useLinodesPermissionsCheck: vi.fn().mockReturnValue({}),
18+
useSearch: vi.fn().mockReturnValue({}),
19+
}));
20+
21+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
22+
usePermissions: queryMocks.usePermissions,
23+
useQueryWithPermissions: queryMocks.useQueryWithPermissions,
24+
}));
25+
26+
vi.mock('@tanstack/react-router', async () => {
27+
const actual = await vi.importActual('@tanstack/react-router');
28+
return {
29+
...actual,
30+
useLocation: queryMocks.useLocation,
31+
useSearch: queryMocks.useSearch,
32+
};
33+
});
34+
35+
vi.mock('../utils.ts', async () => {
36+
const actual = await vi.importActual('../utils');
37+
return {
38+
...actual,
39+
useLinodesPermissionsCheck: queryMocks.useLinodesPermissionsCheck,
40+
};
41+
});
42+
43+
beforeAll(() => mockMatchMedia());
44+
45+
const loadingTestId = 'circle-progress';
46+
47+
describe('ImageLibraryTabs', () => {
48+
beforeEach(() => {
49+
queryMocks.usePermissions.mockReturnValue({
50+
data: {
51+
update_image: true,
52+
delete_image: true,
53+
rebuild_linode: true,
54+
create_linode: true,
55+
replicate_image: true,
56+
},
57+
});
58+
59+
queryMocks.useLocation.mockReturnValue({
60+
pathname: '/images/image-library',
61+
});
62+
});
63+
64+
// For Custom Images (Owned by me)
65+
describe('For Custom Images (Owned by me)', () => {
66+
it("should render 'Owned by me' tab", async () => {
67+
const { getByText } = renderWithTheme(<ImageLibraryTabs />, {
68+
initialRoute: '/images/image-library/owned-by-me',
69+
});
70+
71+
expect(getByText('Owned by me')).toBeVisible();
72+
});
73+
74+
// Test Image action navigations for CUSTOM IMAGES (Owned by me)
75+
it('should allow opening the Edit Image drawer', async () => {
76+
const image = imageFactory.build();
77+
78+
server.use(
79+
http.get('*/images', ({ request }) => {
80+
const filter = request.headers.get('x-filter');
81+
82+
if (filter?.includes('manual')) {
83+
return HttpResponse.json(makeResourcePage([image]));
84+
}
85+
return HttpResponse.json(makeResourcePage([]));
86+
})
87+
);
88+
89+
const { getByText, findByLabelText, router } = renderWithTheme(
90+
<ImageLibraryTabs />,
91+
{
92+
initialRoute: '/images/image-library/owned-by-me/',
93+
}
94+
);
95+
96+
const actionMenu = await findByLabelText(
97+
`Action menu for Image ${image.label}`
98+
);
99+
await userEvent.click(actionMenu);
100+
await userEvent.click(getByText('Edit'));
101+
102+
expect(router.state.location.pathname).toBe(
103+
`/images/image-library/owned-by-me/${encodeURIComponent(image.id)}/edit`
104+
);
105+
});
106+
107+
it('should allow opening the Restore Image drawer', async () => {
108+
const image = imageFactory.build();
109+
110+
server.use(
111+
http.get('*/images', ({ request }) => {
112+
const filter = request.headers.get('x-filter');
113+
114+
if (filter?.includes('manual')) {
115+
return HttpResponse.json(makeResourcePage([image]));
116+
}
117+
return HttpResponse.json(makeResourcePage([]));
118+
})
119+
);
120+
121+
const { router, getByText, findByLabelText } = renderWithTheme(
122+
<ImageLibraryTabs />,
123+
{
124+
initialRoute: '/images/image-library/owned-by-me/',
125+
}
126+
);
127+
128+
const actionMenu = await findByLabelText(
129+
`Action menu for Image ${image.label}`
130+
);
131+
await userEvent.click(actionMenu);
132+
await userEvent.click(getByText('Rebuild an Existing Linode'));
133+
134+
expect(router.state.location.pathname).toBe(
135+
`/images/image-library/owned-by-me/${encodeURIComponent(image.id)}/rebuild`
136+
);
137+
});
138+
139+
it('should allow deploying to a new Linode', async () => {
140+
const image = imageFactory.build();
141+
queryMocks.useLinodesPermissionsCheck.mockReturnValue({
142+
availableLinodes: [linodeFactory.build()],
143+
});
144+
145+
server.use(
146+
http.get('*/images', ({ request }) => {
147+
const filter = request.headers.get('x-filter');
148+
149+
if (filter?.includes('manual')) {
150+
return HttpResponse.json(makeResourcePage([image]));
151+
}
152+
return HttpResponse.json(makeResourcePage([]));
153+
})
154+
);
155+
156+
const { findByLabelText, getByText, queryAllByTestId, router } =
157+
renderWithTheme(<ImageLibraryTabs />, {
158+
initialRoute: '/images/image-library/owned-by-me/',
159+
});
160+
161+
const loadingElement = queryAllByTestId(loadingTestId);
162+
await waitForElementToBeRemoved(loadingElement);
163+
164+
const actionMenu = await findByLabelText(
165+
`Action menu for Image ${image.label}`
166+
);
167+
await userEvent.click(actionMenu);
168+
await userEvent.click(getByText('Deploy to New Linode'));
169+
170+
expect(router.state.location.pathname).toBe('/linodes/create/images');
171+
172+
expect(router.state.location.search).toStrictEqual({
173+
imageID: image.id,
174+
});
175+
});
176+
177+
it('should allow deleting an image', async () => {
178+
const image = imageFactory.build();
179+
180+
server.use(
181+
http.get('*/images', ({ request }) => {
182+
const filter = request.headers.get('x-filter');
183+
184+
if (filter?.includes('manual')) {
185+
return HttpResponse.json(makeResourcePage([image]));
186+
}
187+
return HttpResponse.json(makeResourcePage([]));
188+
})
189+
);
190+
191+
const { router, findByLabelText, getByText } = renderWithTheme(
192+
<ImageLibraryTabs />,
193+
{
194+
initialRoute: '/images/image-library/owned-by-me/',
195+
}
196+
);
197+
198+
const actionMenu = await findByLabelText(
199+
`Action menu for Image ${image.label}`
200+
);
201+
await userEvent.click(actionMenu);
202+
await userEvent.click(getByText('Delete'));
203+
204+
expect(router.state.location.pathname).toBe(
205+
`/images/image-library/owned-by-me/${encodeURIComponent(image.id)}/delete`
206+
);
207+
});
208+
});
209+
210+
it('should render Owned (custom), Shared and Recovery tabs under Images Library Tab', async () => {
211+
const { getByText } = renderWithTheme(<ImageLibraryTabs />, {
212+
initialRoute: '/images/image-library',
213+
});
214+
215+
expect(getByText('Owned by me', { selector: 'button' })).toBeVisible();
216+
expect(getByText('Shared with me', { selector: 'button' })).toBeVisible();
217+
expect(getByText('Recovery images', { selector: 'button' })).toBeVisible();
218+
});
219+
});

0 commit comments

Comments
 (0)