Skip to content

Commit d278c20

Browse files
committed
refactor: update EditRolesButton to use Sets to detect selections
1 parent cfd1807 commit d278c20

File tree

3 files changed

+80
-45
lines changed

3 files changed

+80
-45
lines changed

site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ const meta: Meta<typeof EditRolesButton> = {
1717
export default meta;
1818
type Story = StoryObj<typeof EditRolesButton>;
1919

20+
const selectedRoleNames = new Set([MockUserAdminRole.name, MockOwnerRole.name]);
21+
2022
export const Open: Story = {
2123
args: {
24+
selectedRoleNames,
2225
roles: MockSiteRoles,
23-
selectedRoles: [MockUserAdminRole, MockOwnerRole],
2426
},
2527
parameters: {
2628
chromatic: { delay: 300 },
@@ -30,8 +32,8 @@ export const Open: Story = {
3032
export const Loading: Story = {
3133
args: {
3234
isLoading: true,
35+
selectedRoleNames,
3336
roles: MockSiteRoles,
34-
selectedRoles: [MockUserAdminRole, MockOwnerRole],
3537
userLoginType: "password",
3638
oidcRoleSync: false,
3739
},

site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const Option: React.FC<{
5757
export interface EditRolesButtonProps {
5858
isLoading: boolean;
5959
roles: Role[];
60-
selectedRoles: Role[];
60+
selectedRoleNames: Set<string>;
6161
onChange: (roles: Role["name"][]) => void;
6262
defaultIsOpen?: boolean;
6363
oidcRoleSync: boolean;
@@ -66,7 +66,7 @@ export interface EditRolesButtonProps {
6666

6767
export const EditRolesButton: FC<EditRolesButtonProps> = ({
6868
roles,
69-
selectedRoles,
69+
selectedRoleNames,
7070
onChange,
7171
isLoading,
7272
defaultIsOpen = false,
@@ -77,11 +77,11 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
7777
const anchorRef = useRef<HTMLButtonElement>(null);
7878
const [isOpen, setIsOpen] = useState(defaultIsOpen);
7979
const id = isOpen ? "edit-roles-popover" : undefined;
80-
const selectedRoleNames = selectedRoles.map((role) => role.name);
8180

8281
const handleChange = (roleName: string) => {
83-
if (selectedRoleNames.includes(roleName)) {
84-
onChange(selectedRoleNames.filter((role) => role !== roleName));
82+
if (selectedRoleNames.has(roleName)) {
83+
const serialized = [...selectedRoleNames];
84+
onChange(serialized.filter((role) => role !== roleName));
8585
return;
8686
}
8787

@@ -137,7 +137,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
137137
<Option
138138
key={role.name}
139139
onChange={handleChange}
140-
isChecked={selectedRoleNames.includes(role.name)}
140+
isChecked={selectedRoleNames.has(role.name)}
141141
value={role.name}
142142
name={role.display_name}
143143
description={roleDescriptions[role.name] ?? ""}

site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
1-
import { makeStyles } from "@mui/styles";
1+
import { useState } from "react";
2+
import { useTheme } from "@emotion/react";
23
import { type User, type Role } from "api/typesGenerated";
3-
import { combineClasses } from "utils/combineClasses";
44

55
import { EditRolesButton } from "./EditRolesButton";
66
import { Pill } from "components/Pill/Pill";
77
import TableCell from "@mui/material/TableCell";
88
import Stack from "@mui/material/Stack";
99

10-
const useStyles = makeStyles((theme) => ({
11-
rolePill: {
12-
backgroundColor: theme.palette.background.paperLight,
13-
borderColor: theme.palette.divider,
14-
},
15-
rolePillOwner: {
16-
backgroundColor: theme.palette.info.dark,
17-
borderColor: theme.palette.info.light,
18-
},
19-
}));
10+
const roleNameDisplayOrder: readonly string[] = [
11+
"owner",
12+
"user-admin",
13+
"template-admin",
14+
"auditor",
15+
];
2016

21-
const roleOrder = ["owner", "user-admin", "template-admin", "auditor"];
17+
const fallbackRole: Role = {
18+
name: "member",
19+
display_name: "Member",
20+
} as const;
21+
22+
function getPillRoleList(userRoles: readonly Role[]): readonly Role[] {
23+
if (userRoles.length === 0) {
24+
return [fallbackRole];
25+
}
26+
27+
const matchedOwnerRole = userRoles.find((role) => role.name === "owner");
28+
if (matchedOwnerRole !== undefined) {
29+
return [matchedOwnerRole];
30+
}
31+
32+
return [...userRoles].sort((r1, r2) => {
33+
if (r1.name === r2.name) {
34+
return 0;
35+
}
36+
37+
return r1.name < r2.name ? -1 : 1;
38+
});
39+
}
40+
41+
function getSelectedRoleNames(roles: readonly Role[]) {
42+
const roleNameSet = new Set(roles.map((role) => role.name));
43+
if (roleNameSet.size === 0) {
44+
roleNameSet.add(fallbackRole.name);
45+
}
46+
47+
return roleNameSet;
48+
}
2249

23-
const sortRoles = (roles: readonly Role[]) => {
50+
function sortRolesByAccessLevel(roles: readonly Role[]) {
2451
return [...roles].sort(
25-
(a, b) => roleOrder.indexOf(a.name) - roleOrder.indexOf(b.name),
52+
(r1, r2) =>
53+
roleNameDisplayOrder.indexOf(r1.name) -
54+
roleNameDisplayOrder.indexOf(r2.name),
2655
);
27-
};
56+
}
2857

2958
type Props = {
3059
canEditUsers: boolean;
@@ -35,12 +64,6 @@ type Props = {
3564
onUserRolesUpdate: (user: User, newRoleNames: string[]) => void;
3665
};
3766

38-
// When the user has no role we want to show they are a Member
39-
const fallbackRole: Role = {
40-
name: "member",
41-
display_name: "Member",
42-
} as const;
43-
4467
export function UserRoleCell({
4568
canEditUsers,
4669
roles,
@@ -49,18 +72,20 @@ export function UserRoleCell({
4972
oidcRoleSyncEnabled,
5073
onUserRolesUpdate,
5174
}: Props) {
52-
const styles = useStyles();
75+
const theme = useTheme();
5376

54-
const userRoles =
55-
user.roles.length === 0 ? [fallbackRole] : sortRoles(user.roles);
77+
const pillRoleList = getPillRoleList(user.roles);
78+
const [rolesTruncated, setRolesTruncated] = useState(
79+
user.roles.length - pillRoleList.length,
80+
);
5681

5782
return (
5883
<TableCell>
5984
<Stack direction="row" spacing={1}>
6085
{canEditUsers && (
6186
<EditRolesButton
62-
roles={roles ? sortRoles(roles) : []}
63-
selectedRoles={userRoles}
87+
roles={sortRolesByAccessLevel(roles ?? [])}
88+
selectedRoleNames={getSelectedRoleNames(user.roles)}
6489
isLoading={isLoading}
6590
userLoginType={user.login_type}
6691
oidcRoleSync={oidcRoleSyncEnabled}
@@ -69,21 +94,29 @@ export function UserRoleCell({
6994
const rolesWithoutFallback = roles.filter(
7095
(role) => role !== fallbackRole.name,
7196
);
97+
7298
onUserRolesUpdate(user, rolesWithoutFallback);
7399
}}
74100
/>
75101
)}
76102

77-
{userRoles.map((role) => (
78-
<Pill
79-
key={role.name}
80-
text={role.display_name}
81-
className={combineClasses({
82-
[styles.rolePill]: true,
83-
[styles.rolePillOwner]: role.name === "owner",
84-
})}
85-
/>
86-
))}
103+
{pillRoleList.map((role) => {
104+
const isOwnerRole = role.name === "owner";
105+
const { palette } = theme;
106+
107+
return (
108+
<Pill
109+
key={role.name}
110+
text={role.display_name}
111+
css={{
112+
backgroundColor: isOwnerRole
113+
? palette.info.dark
114+
: palette.background.paperLight,
115+
borderColor: isOwnerRole ? palette.info.light : palette.divider,
116+
}}
117+
/>
118+
);
119+
})}
87120
</Stack>
88121
</TableCell>
89122
);

0 commit comments

Comments
 (0)