Skip to content

Commit 9921e9e

Browse files
fix: [UIE-9245] - EntitiesSelect performance on large accounts (#13168)
* save progress * Cleanup * handle search * handle search * handle search * Added changeset: EntitiesSelect performance on large accounts * fix select all * deselect all
1 parent 4905ac3 commit 9921e9e

File tree

4 files changed

+77
-18
lines changed

4 files changed

+77
-18
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+
EntitiesSelect performance on large accounts ([#13168](https://github.com/linode/manager/pull/13168))

packages/manager/src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33
import { Link } from 'src/components/Link';
44

55
import { ROLES_LEARN_MORE_LINK } from '../constants';
6-
import { Entities } from '../Entities/Entities';
6+
import { EntitiesSelect } from '../Entities/EntitiesSelect';
77
import { Permissions } from '../Permissions/Permissions';
88
import { type ExtendedRole, getFacadeRoleDescription } from '../utilities';
99
import {
@@ -62,7 +62,7 @@ export const AssignedPermissionsPanel = ({
6262
)}
6363
{mode !== 'change-role-for-entity' && (
6464
<StyledEntityBox hideDetails={hideDetails}>
65-
<Entities
65+
<EntitiesSelect
6666
access={role.access}
6767
errorText={errorText}
6868
mode={mode}

packages/manager/src/features/IAM/Shared/Entities/Entities.test.tsx renamed to packages/manager/src/features/IAM/Shared/Entities/EntitiesSelect.test.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react';
55
import { accountEntityFactory } from 'src/factories/accountEntities';
66
import { renderWithTheme } from 'src/utilities/testHelpers';
77

8-
import { Entities } from './Entities';
8+
import { EntitiesSelect } from './EntitiesSelect';
99

1010
import type { EntitiesOption } from '../types';
1111

@@ -40,7 +40,7 @@ const mockValue: EntitiesOption[] = [];
4040
describe('Entities', () => {
4141
it('renders correct data when it is an account access and type is an account', () => {
4242
renderWithTheme(
43-
<Entities
43+
<EntitiesSelect
4444
access="account_access"
4545
mode="assign-role"
4646
onChange={mockOnChange}
@@ -61,7 +61,7 @@ describe('Entities', () => {
6161

6262
it('renders correct data when it is an account access and type is not an account', () => {
6363
renderWithTheme(
64-
<Entities
64+
<EntitiesSelect
6565
access="account_access"
6666
mode="assign-role"
6767
onChange={mockOnChange}
@@ -86,7 +86,7 @@ describe('Entities', () => {
8686
});
8787

8888
renderWithTheme(
89-
<Entities
89+
<EntitiesSelect
9090
access="entity_access"
9191
mode="assign-role"
9292
onChange={mockOnChange}
@@ -112,7 +112,7 @@ describe('Entities', () => {
112112
});
113113

114114
renderWithTheme(
115-
<Entities
115+
<EntitiesSelect
116116
access="entity_access"
117117
mode="assign-role"
118118
onChange={mockOnChange}
@@ -138,7 +138,7 @@ describe('Entities', () => {
138138
});
139139

140140
renderWithTheme(
141-
<Entities
141+
<EntitiesSelect
142142
access="entity_access"
143143
mode="assign-role"
144144
onChange={mockOnChange}
@@ -160,7 +160,7 @@ describe('Entities', () => {
160160
});
161161

162162
renderWithTheme(
163-
<Entities
163+
<EntitiesSelect
164164
access="entity_access"
165165
mode="assign-role"
166166
onChange={mockOnChange}
@@ -176,7 +176,7 @@ describe('Entities', () => {
176176

177177
it('renders Autocomplete as readonly when mode is "change-role"', () => {
178178
renderWithTheme(
179-
<Entities
179+
<EntitiesSelect
180180
access="entity_access"
181181
mode="change-role"
182182
onChange={mockOnChange}
@@ -194,7 +194,7 @@ describe('Entities', () => {
194194
const errorMessage = 'Entities are required.';
195195

196196
renderWithTheme(
197-
<Entities
197+
<EntitiesSelect
198198
access="entity_access"
199199
errorText={errorMessage}
200200
mode="assign-role"

packages/manager/src/features/IAM/Shared/Entities/Entities.tsx renamed to packages/manager/src/features/IAM/Shared/Entities/EntitiesSelect.tsx

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,23 @@ interface Props {
2626
value: EntitiesOption[];
2727
}
2828

29-
export const Entities = ({
29+
// For large entity lists, we want to display the initial 100 results and then load more as the user scrolls.
30+
const INITIAL_DISPLAY_COUNT = 100;
31+
32+
export const EntitiesSelect = ({
3033
access,
3134
errorText,
3235
mode,
3336
onChange,
3437
type,
3538
value,
3639
}: Props) => {
37-
const { data: entities } = useAllAccountEntities({});
40+
const { data: entities, isLoading } = useAllAccountEntities({});
3841
const theme = useTheme();
3942

43+
const [displayCount, setDisplayCount] = React.useState(INITIAL_DISPLAY_COUNT);
44+
const [inputValue, setInputValue] = React.useState('');
45+
4046
const memoizedEntities = React.useMemo(() => {
4147
if (access !== 'entity_access' || !entities) {
4248
return [];
@@ -46,6 +52,30 @@ export const Entities = ({
4652
return typeEntities ? mapEntitiesToOptions(typeEntities) : [];
4753
}, [entities, access, type]);
4854

55+
const filteredEntities = React.useMemo(() => {
56+
if (!inputValue) {
57+
return memoizedEntities;
58+
}
59+
60+
return memoizedEntities.filter((option) =>
61+
option.label.toLowerCase().includes(inputValue.toLowerCase())
62+
);
63+
}, [memoizedEntities, inputValue]);
64+
65+
const visibleOptions = React.useMemo(() => {
66+
const slice = filteredEntities.slice(0, displayCount);
67+
68+
const selectedNotVisible = value.filter(
69+
(selected) => !slice.some((opt) => opt.value === selected.value)
70+
);
71+
72+
return [...slice, ...selectedNotVisible];
73+
}, [filteredEntities, displayCount, value]);
74+
75+
React.useEffect(() => {
76+
setDisplayCount(INITIAL_DISPLAY_COUNT);
77+
}, [filteredEntities]);
78+
4979
if (access === 'account_access') {
5080
return (
5181
<>
@@ -75,16 +105,28 @@ export const Entities = ({
75105
getOptionLabel={(option) => option.label}
76106
isOptionEqualToValue={(option, value) => option.value === value.value}
77107
label="Entities"
108+
loading={isLoading}
78109
multiple
79110
noMarginTop
80-
onChange={(_, newValue) => {
81-
onChange(newValue || []);
111+
onChange={(_, newValue, reason) => {
112+
if (
113+
reason === 'selectOption' &&
114+
newValue.length === displayCount &&
115+
filteredEntities.length > displayCount
116+
) {
117+
onChange(filteredEntities);
118+
} else {
119+
onChange(newValue || []);
120+
}
82121
}}
83-
options={memoizedEntities}
122+
onInputChange={(_, value) => {
123+
setInputValue(value);
124+
}}
125+
options={visibleOptions}
84126
placeholder={getPlaceholder(
85127
type,
86128
value.length,
87-
memoizedEntities.length
129+
filteredEntities.length
88130
)}
89131
readOnly={mode === 'change-role'}
90132
renderInput={(params) => (
@@ -97,10 +139,22 @@ export const Entities = ({
97139
placeholder={getPlaceholder(
98140
type,
99141
value.length,
100-
memoizedEntities.length
142+
filteredEntities.length
101143
)}
102144
/>
103145
)}
146+
slotProps={{
147+
listbox: {
148+
onScroll: (e) => {
149+
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
150+
if (scrollHeight - scrollTop <= clientHeight * 1.5) {
151+
setDisplayCount((prev) =>
152+
Math.min(prev + 200, filteredEntities.length)
153+
);
154+
}
155+
},
156+
},
157+
}}
104158
sx={{
105159
marginTop: 0,
106160
'& .MuiChip-root': {

0 commit comments

Comments
 (0)