Skip to content

Commit 6d8b9c4

Browse files
fix: [UIE-9731] - IAM Permissions performance improvements: Create from Backup & Clone (#13143)
* utilize new endpoint to avoid parallel queries * add loading * handle cards as well * Added changeset: IAM Permissions performance improvements: Create from Backup & Clone
1 parent 915b170 commit 6d8b9c4

File tree

6 files changed

+189
-117
lines changed

6 files changed

+189
-117
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
IAM Permissions performance improvements: Create from Backup & Clone ([#13143](https://github.com/linode/manager/pull/13143))

packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
1919
import { TableRowError } from 'src/components/TableRowError/TableRowError';
2020
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
2121
import { TableSortCell } from 'src/components/TableSortCell';
22+
import { useGetAllUserEntitiesByPermission } from 'src/features/IAM/hooks/useGetAllUserEntitiesByPermission';
23+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
2224
import {
2325
linodesCreateTypesMap,
2426
useGetLinodeCreateType,
@@ -56,6 +58,26 @@ export const LinodeSelectTable = (props: Props) => {
5658
theme.breakpoints.up('md')
5759
);
5860

61+
const { data: accountPermissions, isLoading: isLoadingAccountPermissions } =
62+
usePermissions('account', ['create_linode']);
63+
64+
const {
65+
data: shutdownableLinodes = [],
66+
isLoading: isLoadingShutdownableLinodes,
67+
error: shutdownableLinodesError,
68+
} = useGetAllUserEntitiesByPermission({
69+
entityType: 'linode',
70+
permission: 'shutdown_linode',
71+
});
72+
const {
73+
data: cloneableLinodes = [],
74+
isLoading: isLoadingCloneableLinodes,
75+
error: cloneableLinodesError,
76+
} = useGetAllUserEntitiesByPermission({
77+
entityType: 'linode',
78+
permission: 'clone_linode',
79+
});
80+
5981
const {
6082
control,
6183
formState: {
@@ -102,7 +124,12 @@ export const LinodeSelectTable = (props: Props) => {
102124

103125
const { filter, filterError } = getLinodeXFilter(query, order, orderBy);
104126

105-
const { data, error, isFetching, isLoading } = useLinodesQuery(
127+
const {
128+
data,
129+
error: linodesError,
130+
isFetching,
131+
isLoading: isLoadingLinodes,
132+
} = useLinodesQuery(
106133
{
107134
page: pagination.page,
108135
page_size: pagination.pageSize,
@@ -144,6 +171,14 @@ export const LinodeSelectTable = (props: Props) => {
144171

145172
const columns = enablePowerOff ? 6 : 5;
146173

174+
const isLoading =
175+
isLoadingAccountPermissions ||
176+
isLoadingShutdownableLinodes ||
177+
isLoadingCloneableLinodes ||
178+
isLoadingLinodes;
179+
const error =
180+
shutdownableLinodesError || cloneableLinodesError || linodesError;
181+
147182
return (
148183
<Stack pt={1} spacing={2}>
149184
{fieldState.error?.message && (
@@ -195,27 +230,38 @@ export const LinodeSelectTable = (props: Props) => {
195230
</TableRow>
196231
</TableHead>
197232
<TableBody>
198-
{isLoading && <TableRowLoading columns={columns} rows={10} />}
233+
{isLoading && (
234+
<TableRowLoading columns={columns} rows={pagination.pageSize} />
235+
)}
199236
{error && (
200237
<TableRowError colSpan={columns} message={error[0].reason} />
201238
)}
202239
{data?.results === 0 && <TableRowEmpty colSpan={columns} />}
203-
{data?.data.map((linode) => (
204-
<LinodeSelectTableRow
205-
key={linode.id}
206-
linode={linode}
207-
onPowerOff={
208-
enablePowerOff
209-
? () => {
210-
setLinodeToPowerOff(linode);
211-
sendLinodePowerOffEvent('Clone Linode');
212-
}
213-
: undefined
214-
}
215-
onSelect={() => handleSelect(linode)}
216-
selected={linode.id === field.value?.id}
217-
/>
218-
))}
240+
{!isLoading &&
241+
!error &&
242+
data?.data.map((linode) => (
243+
<LinodeSelectTableRow
244+
disabled={!accountPermissions?.create_linode}
245+
isCloneable={cloneableLinodes?.some(
246+
(l) => l.id === linode.id
247+
)}
248+
isShutdownable={shutdownableLinodes?.some(
249+
(l) => l.id === linode.id
250+
)}
251+
key={linode.id}
252+
linode={linode}
253+
onPowerOff={
254+
enablePowerOff
255+
? () => {
256+
setLinodeToPowerOff(linode);
257+
sendLinodePowerOffEvent('Clone Linode');
258+
}
259+
: undefined
260+
}
261+
onSelect={() => handleSelect(linode)}
262+
selected={linode.id === field.value?.id}
263+
/>
264+
))}
219265
</TableBody>
220266
</Table>
221267
) : (
@@ -224,6 +270,10 @@ export const LinodeSelectTable = (props: Props) => {
224270
<SelectLinodeCard
225271
handlePowerOff={() => handlePowerOff(linode)}
226272
handleSelection={() => handleSelect(linode)}
273+
isCloneable={cloneableLinodes?.some((l) => l.id === linode.id)}
274+
isShutdownable={shutdownableLinodes?.some(
275+
(l) => l.id === linode.id
276+
)}
227277
key={linode.id}
228278
linode={linode}
229279
selected={linode.id === field.value?.id}

packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
33
import React from 'react';
44

55
import { imageFactory, typeFactory } from 'src/factories';
6-
import { makeResourcePage } from 'src/mocks/serverHandlers';
7-
import { http, HttpResponse, server } from 'src/mocks/testServer';
86
import {
97
renderWithThemeAndHookFormContext,
108
wrapWithTableBody,
@@ -13,26 +11,35 @@ import {
1311
import { LinodeSelectTableRow } from './LinodeSelectTableRow';
1412

1513
const queryMocks = vi.hoisted(() => ({
16-
userPermissions: vi.fn(() => ({
17-
data: {
18-
shutdown_linode: false,
19-
clone_linode: false,
20-
create_linode: false,
21-
},
22-
})),
14+
useImageQuery: vi.fn().mockReturnValue({}),
15+
useRegionsQuery: vi.fn().mockReturnValue({}),
16+
useTypeQuery: vi.fn().mockReturnValue({}),
2317
}));
2418

25-
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
26-
usePermissions: queryMocks.userPermissions,
27-
}));
19+
vi.mock('@linode/queries', async () => {
20+
const actual = await vi.importActual('@linode/queries');
21+
return {
22+
...actual,
23+
useImageQuery: queryMocks.useImageQuery,
24+
useRegionsQuery: queryMocks.useRegionsQuery,
25+
useTypeQuery: queryMocks.useTypeQuery,
26+
};
27+
});
2828

2929
describe('LinodeSelectTableRow', () => {
3030
it('should render a Radio that is labeled by the Linode label', () => {
3131
const linode = linodeFactory.build();
3232

3333
const { getByLabelText } = renderWithThemeAndHookFormContext({
3434
component: wrapWithTableBody(
35-
<LinodeSelectTableRow linode={linode} onSelect={vi.fn()} selected />
35+
<LinodeSelectTableRow
36+
disabled={false}
37+
isCloneable={false}
38+
isShutdownable={false}
39+
linode={linode}
40+
onSelect={vi.fn()}
41+
selected
42+
/>
3643
),
3744
});
3845

@@ -44,7 +51,14 @@ describe('LinodeSelectTableRow', () => {
4451

4552
const { getByLabelText } = renderWithThemeAndHookFormContext({
4653
component: wrapWithTableBody(
47-
<LinodeSelectTableRow linode={linode} onSelect={vi.fn()} selected />
54+
<LinodeSelectTableRow
55+
disabled={false}
56+
isCloneable={false}
57+
isShutdownable={false}
58+
linode={linode}
59+
onSelect={vi.fn()}
60+
selected
61+
/>
4862
),
4963
});
5064

@@ -57,6 +71,9 @@ describe('LinodeSelectTableRow', () => {
5771
const { getByLabelText } = renderWithThemeAndHookFormContext({
5872
component: wrapWithTableBody(
5973
<LinodeSelectTableRow
74+
disabled={false}
75+
isCloneable={false}
76+
isShutdownable={false}
6077
linode={linode}
6178
onSelect={vi.fn()}
6279
selected={false}
@@ -68,20 +85,16 @@ describe('LinodeSelectTableRow', () => {
6885
});
6986

7087
it('should should call onSelect when a radio is selected', async () => {
71-
queryMocks.userPermissions.mockReturnValue({
72-
data: {
73-
shutdown_linode: false,
74-
clone_linode: true,
75-
create_linode: true,
76-
},
77-
});
7888
const linode = linodeFactory.build();
7989

8090
const onSelect = vi.fn();
8191

8292
const { getByLabelText } = renderWithThemeAndHookFormContext({
8393
component: wrapWithTableBody(
8494
<LinodeSelectTableRow
95+
disabled={false}
96+
isCloneable={true}
97+
isShutdownable={false}
8598
linode={linode}
8699
onSelect={onSelect}
87100
selected={false}
@@ -102,15 +115,20 @@ describe('LinodeSelectTableRow', () => {
102115
label: 'My Image Nice Label',
103116
});
104117

105-
server.use(
106-
http.get('*/v4/images/my-image', () => {
107-
return HttpResponse.json(image);
108-
})
109-
);
118+
queryMocks.useImageQuery.mockReturnValue({
119+
data: image,
120+
});
110121

111122
const { findByText } = renderWithThemeAndHookFormContext({
112123
component: wrapWithTableBody(
113-
<LinodeSelectTableRow linode={linode} onSelect={vi.fn()} selected />
124+
<LinodeSelectTableRow
125+
disabled={false}
126+
isCloneable={false}
127+
isShutdownable={false}
128+
linode={linode}
129+
onSelect={vi.fn()}
130+
selected
131+
/>
114132
),
115133
});
116134

@@ -124,19 +142,24 @@ describe('LinodeSelectTableRow', () => {
124142
label: 'US Test',
125143
});
126144

127-
server.use(
128-
http.get('*/v4*/regions', () => {
129-
return HttpResponse.json(makeResourcePage([region]));
130-
})
131-
);
145+
queryMocks.useRegionsQuery.mockReturnValue({
146+
data: [region],
147+
});
132148

133149
const { findByText } = renderWithThemeAndHookFormContext({
134150
component: wrapWithTableBody(
135-
<LinodeSelectTableRow linode={linode} onSelect={vi.fn()} selected />
151+
<LinodeSelectTableRow
152+
disabled={false}
153+
isCloneable={false}
154+
isShutdownable={false}
155+
linode={linode}
156+
onSelect={vi.fn()}
157+
selected
158+
/>
136159
),
137160
});
138161

139-
await findByText(`US, ${region.label}`);
162+
await findByText(region.label);
140163
});
141164

142165
it('should render a Linode plan label', async () => {
@@ -146,15 +169,20 @@ describe('LinodeSelectTableRow', () => {
146169
label: 'Linode Type 1',
147170
});
148171

149-
server.use(
150-
http.get('*/v4/linode/types/linode-type-1', () => {
151-
return HttpResponse.json(type);
152-
})
153-
);
172+
queryMocks.useTypeQuery.mockReturnValue({
173+
data: type,
174+
});
154175

155176
const { findByText } = renderWithThemeAndHookFormContext({
156177
component: wrapWithTableBody(
157-
<LinodeSelectTableRow linode={linode} onSelect={vi.fn()} selected />
178+
<LinodeSelectTableRow
179+
disabled={false}
180+
isCloneable={false}
181+
isShutdownable={false}
182+
linode={linode}
183+
onSelect={vi.fn()}
184+
selected
185+
/>
158186
),
159187
});
160188

@@ -167,6 +195,9 @@ describe('LinodeSelectTableRow', () => {
167195
const { getByText } = renderWithThemeAndHookFormContext({
168196
component: wrapWithTableBody(
169197
<LinodeSelectTableRow
198+
disabled={false}
199+
isCloneable={false}
200+
isShutdownable={false}
170201
linode={linode}
171202
onPowerOff={vi.fn()}
172203
onSelect={vi.fn()}
@@ -180,17 +211,13 @@ describe('LinodeSelectTableRow', () => {
180211

181212
it('should render an enabled power off button if the Linode is powered on, a onPowerOff function is passed, and the row is selected, if user has shutdown_linode permission', async () => {
182213
const linode = linodeFactory.build({ status: 'running' });
183-
queryMocks.userPermissions.mockReturnValue({
184-
data: {
185-
shutdown_linode: true,
186-
clone_linode: true,
187-
create_linode: true,
188-
},
189-
});
190214

191215
const { getByText } = renderWithThemeAndHookFormContext({
192216
component: wrapWithTableBody(
193217
<LinodeSelectTableRow
218+
disabled={false}
219+
isCloneable={true}
220+
isShutdownable={true}
194221
linode={linode}
195222
onPowerOff={vi.fn()}
196223
onSelect={vi.fn()}
@@ -209,6 +236,9 @@ describe('LinodeSelectTableRow', () => {
209236
const { getByText } = renderWithThemeAndHookFormContext({
210237
component: wrapWithTableBody(
211238
<LinodeSelectTableRow
239+
disabled={false}
240+
isCloneable={true}
241+
isShutdownable={true}
212242
linode={linode}
213243
onPowerOff={onPowerOff}
214244
onSelect={vi.fn()}

0 commit comments

Comments
 (0)