Skip to content

Commit c7fb7bc

Browse files
authored
Merge pull request #12062 from aaleksee-akamai/UIE-8672-fix-empty-state
feat: [UIE-8672, UIE-8702, UIE-8703] - IAM RBAC: fix bugs
2 parents 3f70555 + 799f0d6 commit c7fb7bc

15 files changed

Lines changed: 205 additions & 109 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+
IAM RBAC: Fix bugs in the Entities component and the loading state for tabs ([#12062](https://github.com/linode/manager/pull/12062))

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ import { Link } from 'src/components/Link';
66

77
import { Entities } from '../Entities/Entities';
88
import { Permissions } from '../Permissions/Permissions';
9-
import {
10-
type DrawerModes,
11-
type EntitiesOption,
12-
type ExtendedRole,
13-
type ExtendedRoleMap,
14-
getFacadeRoleDescription,
15-
} from '../utilities';
169

10+
import type { EntitiesOption } from '../types';
11+
import { getFacadeRoleDescription, type DrawerModes, type ExtendedRole, type ExtendedRoleMap } from '../utilities';
1712
import type { SxProps, Theme } from '@mui/material';
1813

1914
interface Props {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
getFormattedEntityType,
3030
mapEntityTypes,
3131
mapRolesToPermissions,
32-
transformedAccountEntities,
32+
groupAccountEntitiesByType,
3333
} from '../utilities';
3434
import { AssignedRolesActionMenu } from './AssignedRolesActionMenu';
3535
import { ChangeRoleDrawer } from './ChangeRoleDrawer';
@@ -112,7 +112,7 @@ export const AssignedRolesTable = () => {
112112
const resourceTypes = getResourceTypes(roles);
113113

114114
if (entities) {
115-
const transformedEntities = transformedAccountEntities(entities.data);
115+
const transformedEntities = groupAccountEntitiesByType(entities.data);
116116

117117
roles = addEntitiesNamesToRoles(roles, transformedEntities);
118118
}

packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@ import {
2121
import { AssignedPermissionsPanel } from '../AssignedPermissionsPanel/AssignedPermissionsPanel';
2222
import { getAllRoles, getRoleByName, updateUserRoles } from '../utilities';
2323

24-
import type {
25-
DrawerModes,
26-
EntitiesOption,
27-
ExtendedRoleMap,
28-
RolesType,
29-
} from '../utilities';
24+
import type { EntitiesOption } from '../types';
25+
import type { DrawerModes, ExtendedRoleMap, RolesType } from '../utilities';
3026

3127
interface Props {
3228
mode: DrawerModes;
@@ -76,7 +72,7 @@ export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => {
7672
reset,
7773
setError,
7874
watch,
79-
} = useForm<{ roleName: RolesType | null }>({
75+
} = useForm<{ roleName: null | RolesType }>({
8076
defaultValues: {
8177
roleName: null,
8278
},
@@ -149,6 +145,8 @@ export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => {
149145
</Typography>
150146

151147
<Controller
148+
control={control}
149+
name="roleName"
152150
render={({ field, fieldState }) => (
153151
<Autocomplete
154152
errorText={fieldState.error?.message}
@@ -161,8 +159,6 @@ export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => {
161159
value={field.value || null}
162160
/>
163161
)}
164-
control={control}
165-
name="roleName"
166162
rules={{ required: 'Role is required.' }}
167163
/>
168164

packages/manager/src/features/IAM/Shared/AssignedRolesTable/UpdateEntitiesDrawer.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@ import {
1313
import { AssignedPermissionsPanel } from '../AssignedPermissionsPanel/AssignedPermissionsPanel';
1414
import { toEntityAccess } from '../utilities';
1515

16-
import type {
17-
EntitiesOption,
18-
ExtendedRoleMap,
19-
UpdateEntitiesFormValues,
20-
} from '../utilities';
16+
import type { EntitiesOption } from '../types';
17+
import type { ExtendedRoleMap, UpdateEntitiesFormValues } from '../utilities';
2118
import type { EntityAccessRole } from '@linode/api-v4';
2219

2320
interface Props {

packages/manager/src/features/IAM/Shared/Entities/Entities.test.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { makeResourcePage } from 'src/mocks/serverHandlers';
77
import { renderWithTheme } from 'src/utilities/testHelpers';
88

99
import { Entities } from './Entities';
10-
11-
import type { EntitiesOption } from '../utilities';
10+
import { EntitiesOption } from '../types';
1211

1312
const queryMocks = vi.hoisted(() => ({
1413
useAccountEntities: vi.fn().mockReturnValue({}),

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

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { Autocomplete, Notice, Typography } from '@linode/ui';
1+
import { Autocomplete, Notice, TextField, Typography } from '@linode/ui';
22
import { useTheme } from '@mui/material';
33
import React from 'react';
44

55
import { FormLabel } from 'src/components/FormLabel';
66
import { Link } from 'src/components/Link';
77
import { useAccountEntities } from 'src/queries/entities/entities';
88

9+
import { type DrawerModes, getFormattedEntityType } from '../utilities';
910
import {
1011
getCreateLinkForEntityType,
11-
getFormattedEntityType,
12-
placeholderMap,
13-
transformedAccountEntities,
14-
} from '../utilities';
12+
getEntitiesByType,
13+
getPlaceholder,
14+
mapEntitiesToOptions,
15+
} from './utils';
1516

16-
import type { DrawerModes, EntitiesOption } from '../utilities';
17+
import type { EntitiesOption } from '../types';
1718
import type {
18-
AccountEntity,
1919
EntityType,
2020
EntityTypePermissions,
2121
IamAccessType,
@@ -46,16 +46,18 @@ export const Entities = ({
4646
return [];
4747
}
4848
const typeEntities = getEntitiesByType(type, entities.data);
49-
return typeEntities ? transformedEntities(typeEntities) : [];
49+
return typeEntities ? mapEntitiesToOptions(typeEntities) : [];
5050
}, [entities, access, type]);
5151

5252
if (access === 'account_access') {
5353
return (
5454
<>
5555
<FormLabel>
5656
<Typography
57-
marginBottom={0.5}
58-
sx={{ marginTop: theme.tokens.spacing.S12 }}
57+
sx={{
58+
marginTop: theme.tokens.spacing.S12,
59+
marginBottom: theme.tokens.spacing.S4,
60+
}}
5961
variant="inherit"
6062
>
6163
Entities
@@ -73,7 +75,7 @@ export const Entities = ({
7375
return (
7476
<>
7577
<Autocomplete
76-
errorText={errorText}
78+
disabled={!memoizedEntities.length}
7779
getOptionLabel={(option) => option.label}
7880
isOptionEqualToValue={(option, value) => option.value === value.value}
7981
label="Entities"
@@ -88,7 +90,20 @@ export const Entities = ({
8890
value.length,
8991
memoizedEntities.length
9092
)}
91-
readOnly={getReadonlyState(mode, memoizedEntities.length)}
93+
readOnly={mode === 'change-role'}
94+
renderInput={(params) => (
95+
<TextField
96+
{...params}
97+
error={!!errorText}
98+
errorText={errorText}
99+
label="Entities"
100+
placeholder={getPlaceholder(
101+
type,
102+
value.length,
103+
memoizedEntities.length
104+
)}
105+
/>
106+
)}
92107
sx={{ marginTop: theme.tokens.spacing.S12 }}
93108
value={value || []}
94109
/>
@@ -105,38 +120,3 @@ export const Entities = ({
105120
</>
106121
);
107122
};
108-
109-
const getPlaceholder = (
110-
type: EntityType | EntityTypePermissions,
111-
currentValueLength: number,
112-
possibleEntitiesLength: number
113-
): string =>
114-
currentValueLength > 0
115-
? ' '
116-
: possibleEntitiesLength === 0
117-
? 'None'
118-
: placeholderMap[type] || 'Select';
119-
120-
const getReadonlyState = (
121-
mode: DrawerModes | undefined,
122-
possibleEntitiesLength: number
123-
): boolean => mode === 'change-role' || possibleEntitiesLength === 0;
124-
125-
const transformedEntities = (
126-
entities: { id: number; label: string }[]
127-
): EntitiesOption[] => {
128-
return entities.map((entity) => ({
129-
label: entity.label,
130-
value: entity.id,
131-
}));
132-
};
133-
134-
const getEntitiesByType = (
135-
roleEntityType: EntityType | EntityTypePermissions,
136-
entities: AccountEntity[]
137-
): Pick<AccountEntity, 'id' | 'label'>[] | undefined => {
138-
const entitiesMap = transformedAccountEntities(entities);
139-
140-
// Find the first matching entity by type
141-
return entitiesMap.get(roleEntityType as EntityType);
142-
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { accountEntityFactory } from 'src/factories/accountEntities';
2+
3+
import {
4+
getCreateLinkForEntityType,
5+
getEntitiesByType,
6+
getPlaceholder,
7+
placeholderMap,
8+
} from './utils';
9+
10+
describe('getCreateLinkForEntityType', () => {
11+
it('should return the correct create link for a given entity type', () => {
12+
expect(getCreateLinkForEntityType('linode')).toBe('/linodes/create');
13+
expect(getCreateLinkForEntityType('volume')).toBe('/volumes/create');
14+
expect(getCreateLinkForEntityType('firewall')).toBe('/firewalls/create');
15+
});
16+
});
17+
18+
describe('getPlaceholder', () => {
19+
it('should return a space if currentValueLength is greater than 0', () => {
20+
expect(getPlaceholder('linode', 1, 10)).toBe(' ');
21+
});
22+
23+
it('should return "None" if possibleEntitiesLength is 0', () => {
24+
expect(getPlaceholder('linode', 0, 0)).toBe('None');
25+
});
26+
27+
it('should return the placeholder from placeholderMap if type exists', () => {
28+
expect(getPlaceholder('linode', 0, 10)).toBe(placeholderMap['linode']);
29+
});
30+
});
31+
32+
describe('getEntitiesByType', () => {
33+
it('should return entities of the type "linode', () => {
34+
const mockEntities = [
35+
...accountEntityFactory.buildList(3, {
36+
type: 'linode',
37+
}),
38+
accountEntityFactory.build({
39+
type: 'firewall',
40+
}),
41+
];
42+
43+
const result = getEntitiesByType('linode', mockEntities);
44+
45+
expect(result).toEqual([
46+
{ id: 1, label: 'test-1' },
47+
{ id: 2, label: 'test-2' },
48+
{ id: 3, label: 'test-3' },
49+
]);
50+
});
51+
52+
it('should return entities of the type "linode', () => {
53+
const mockEntities = [
54+
...accountEntityFactory.buildList(3, {
55+
type: 'linode',
56+
}),
57+
accountEntityFactory.build({
58+
id: 1,
59+
label: 'firewall-1',
60+
type: 'firewall',
61+
}),
62+
];
63+
64+
const result = getEntitiesByType('firewall', mockEntities);
65+
66+
expect(result).toEqual([{ id: 1, label: 'firewall-1' }]);
67+
});
68+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { groupAccountEntitiesByType } from '../utilities';
2+
3+
import type { EntitiesOption } from '../types';
4+
import type {
5+
AccountEntity,
6+
EntityType,
7+
EntityTypePermissions,
8+
} from '@linode/api-v4';
9+
10+
export const placeholderMap: Record<string, string> = {
11+
account: 'Select Account',
12+
database: 'Select Databases',
13+
domain: 'Select Domains',
14+
firewall: 'Select Firewalls',
15+
image: 'Select Images',
16+
linode: 'Select Linodes',
17+
longview: 'Select Longviews',
18+
nodebalancer: 'Select Nodebalancers',
19+
stackscript: 'Select Stackscripts',
20+
volume: 'Select Volumes',
21+
vpc: 'Select VPCs',
22+
};
23+
24+
export const getCreateLinkForEntityType = (
25+
entityType: EntityType | EntityTypePermissions
26+
): string => {
27+
// TODO - find the exceptions to this rule - most use the route of /{entityType}s/create (note the "s")
28+
return `/${entityType}s/create`;
29+
};
30+
31+
export const getPlaceholder = (
32+
type: EntityType | EntityTypePermissions,
33+
currentValueLength: number,
34+
possibleEntitiesLength: number
35+
): string => {
36+
let placeholder: string;
37+
38+
if (currentValueLength > 0) {
39+
placeholder = ' ';
40+
} else if (possibleEntitiesLength === 0) {
41+
placeholder = 'None';
42+
} else {
43+
placeholder = placeholderMap[type] || 'Select';
44+
}
45+
46+
return placeholder;
47+
};
48+
49+
export const mapEntitiesToOptions = (
50+
entities: { id: number; label: string }[]
51+
): EntitiesOption[] => {
52+
return entities.map((entity) => ({
53+
label: entity.label,
54+
value: entity.id,
55+
}));
56+
};
57+
58+
export const getEntitiesByType = (
59+
roleEntityType: EntityType | EntityTypePermissions,
60+
entities: AccountEntity[]
61+
): Pick<AccountEntity, 'id' | 'label'>[] | undefined => {
62+
const entitiesMap = groupAccountEntitiesByType(entities);
63+
64+
// Find the first matching entity by type
65+
return entitiesMap.get(roleEntityType as EntityType);
66+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface EntitiesOption {
2+
label: string;
3+
value: number;
4+
}

0 commit comments

Comments
 (0)