From cb51aa8c227be35f01f662888a40a241cd52ecd4 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 15 Oct 2023 21:32:22 +0000 Subject: [PATCH 01/42] refactor: extract UserRoleCell into separate component --- site/src/pages/UsersPage/UsersPageView.tsx | 4 +- .../UsersPage/UsersTable/UserRoleCell.tsx | 90 +++++++ .../pages/UsersPage/UsersTable/UsersTable.tsx | 7 +- .../UsersPage/UsersTable/UsersTableBody.tsx | 228 +++++++----------- 4 files changed, 189 insertions(+), 140 deletions(-) create mode 100644 site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 56cd39e85d446..8c070c4e2eaee 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -12,10 +12,10 @@ export interface UsersPageViewProps { users?: TypesGen.User[]; roles?: TypesGen.AssignableRoles[]; isUpdatingUserRoles?: boolean; - canEditUsers?: boolean; + canEditUsers: boolean; oidcRoleSyncEnabled: boolean; canViewActivity?: boolean; - isLoading?: boolean; + isLoading: boolean; authMethods?: TypesGen.AuthMethods; onSuspendUser: (user: TypesGen.User) => void; onDeleteUser: (user: TypesGen.User) => void; diff --git a/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx b/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx new file mode 100644 index 0000000000000..f2e78b9a75030 --- /dev/null +++ b/site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx @@ -0,0 +1,90 @@ +import { makeStyles } from "@mui/styles"; +import { type User, type Role } from "api/typesGenerated"; +import { combineClasses } from "utils/combineClasses"; + +import { EditRolesButton } from "./EditRolesButton"; +import { Pill } from "components/Pill/Pill"; +import TableCell from "@mui/material/TableCell"; +import Stack from "@mui/material/Stack"; + +const useStyles = makeStyles((theme) => ({ + rolePill: { + backgroundColor: theme.palette.background.paperLight, + borderColor: theme.palette.divider, + }, + rolePillOwner: { + backgroundColor: theme.palette.info.dark, + borderColor: theme.palette.info.light, + }, +})); + +const roleOrder = ["owner", "user-admin", "template-admin", "auditor"]; + +const sortRoles = (roles: readonly Role[]) => { + return [...roles].sort( + (a, b) => roleOrder.indexOf(a.name) - roleOrder.indexOf(b.name), + ); +}; + +type Props = { + canEditUsers: boolean; + roles: undefined | readonly Role[]; + user: User; + isLoading: boolean; + oidcRoleSyncEnabled: boolean; + onUserRolesUpdate: (user: User, newRoleNames: string[]) => void; +}; + +// When the user has no role we want to show they are a Member +const fallbackRole: Role = { + name: "member", + display_name: "Member", +} as const; + +export function UserRoleCell({ + canEditUsers, + roles, + user, + isLoading, + oidcRoleSyncEnabled, + onUserRolesUpdate, +}: Props) { + const styles = useStyles(); + + const userRoles = + user.roles.length === 0 ? [fallbackRole] : sortRoles(user.roles); + + return ( + + + {canEditUsers && ( + { + // Remove the fallback role because it is only for the UI + const rolesWithoutFallback = roles.filter( + (role) => role !== fallbackRole.name, + ); + onUserRolesUpdate(user, rolesWithoutFallback); + }} + /> + )} + + {userRoles.map((role) => ( + + ))} + + + ); +} diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index 4a47c68d49cae..97a681c638dc6 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -16,15 +16,15 @@ export const Language = { statusLabel: "Status", lastSeenLabel: "Last Seen", loginTypeLabel: "Login Type", -}; +} as const; export interface UsersTableProps { users?: TypesGen.User[]; roles?: TypesGen.AssignableRoles[]; isUpdatingUserRoles?: boolean; - canEditUsers?: boolean; + canEditUsers: boolean; canViewActivity?: boolean; - isLoading?: boolean; + isLoading: boolean; onSuspendUser: (user: TypesGen.User) => void; onActivateUser: (user: TypesGen.User) => void; onDeleteUser: (user: TypesGen.User) => void; @@ -78,6 +78,7 @@ export const UsersTable: FC> = ({ {canEditUsers && } + { - return role.name === "owner"; -}; - -const roleOrder = ["owner", "user-admin", "template-admin", "auditor"]; - -const sortRoles = (roles: TypesGen.Role[]) => { - return roles.slice(0).sort((a, b) => { - return roleOrder.indexOf(a.name) - roleOrder.indexOf(b.name); - }); -}; - interface UsersTableBodyProps { users?: TypesGen.User[]; authMethods?: TypesGen.AuthMethods; roles?: TypesGen.AssignableRoles[]; isUpdatingUserRoles?: boolean; - canEditUsers?: boolean; - isLoading?: boolean; + canEditUsers: boolean; + isLoading: boolean; canViewActivity?: boolean; onSuspendUser: (user: TypesGen.User) => void; onDeleteUser: (user: TypesGen.User) => void; @@ -101,15 +87,23 @@ export const UsersTableBody: FC< + + + + + + + + {canEditUsers && ( @@ -118,6 +112,7 @@ export const UsersTableBody: FC< + @@ -129,6 +124,7 @@ export const UsersTableBody: FC< + @@ -140,127 +136,89 @@ export const UsersTableBody: FC< + - <> - {users && - users.map((user) => { - // When the user has no role we want to show they are a Member - const fallbackRole: TypesGen.Role = { - name: "member", - display_name: "Member", - }; - const userRoles = - user.roles.length === 0 - ? [fallbackRole] - : sortRoles(user.roles); + {users?.map((user) => ( + + + + - return ( - - - - - - - {canEditUsers && ( - { - // Remove the fallback role because it is only for the UI - const rolesWithoutFallback = roles.filter( - (role) => role !== fallbackRole.name, - ); - onUpdateUserRoles(user, rolesWithoutFallback); - }} - /> - )} - {userRoles.map((role) => ( - - ))} - - - - - - - {user.status} - - + + + + + - {canEditUsers && ( - - Suspend…, - onClick: onSuspendUser, - disabled: false, - } - : { - label: <>Activate…, - onClick: onActivateUser, - disabled: false, - }, - { - label: <>Delete…, - onClick: onDeleteUser, - disabled: user.id === actorID, - }, - { - label: <>Reset password…, - onClick: onResetUserPassword, - disabled: user.login_type !== "password", - }, - { - label: "View workspaces", - onClick: onListWorkspaces, - disabled: false, - }, - { - label: ( - <> - View activity - {!canViewActivity && } - - ), - onClick: onViewActivity, - disabled: !canViewActivity, - }, - ]} - /> - - )} - - ); - })} - + + {user.status} + + + + {canEditUsers && ( + + Suspend…, + onClick: onSuspendUser, + disabled: false, + } + : { + label: <>Activate…, + onClick: onActivateUser, + disabled: false, + }, + { + label: <>Delete…, + onClick: onDeleteUser, + disabled: user.id === actorID, + }, + { + label: <>Reset password…, + onClick: onResetUserPassword, + disabled: user.login_type !== "password", + }, + { + label: "View workspaces", + onClick: onListWorkspaces, + disabled: false, + }, + { + label: ( + <> + View activity + {!canViewActivity && } + + ), + onClick: onViewActivity, + disabled: !canViewActivity, + }, + ]} + /> + + )} + + ))} ); From a33fe0caf69a7b780742a3d9f88445a77a361c18 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 15 Oct 2023 21:34:41 +0000 Subject: [PATCH 02/42] wip: add placeholder Groups column --- site/src/pages/UsersPage/UsersTable/UsersTable.tsx | 13 +++++++++---- .../pages/UsersPage/UsersTable/UsersTableBody.tsx | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index 97a681c638dc6..cfbdbd8fd5154 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -13,6 +13,7 @@ import { UsersTableBody } from "./UsersTableBody"; export const Language = { usernameLabel: "User", rolesLabel: "Roles", + groupsLabel: "Groups", statusLabel: "Status", lastSeenLabel: "Last Seen", loginTypeLabel: "Login Type", @@ -65,15 +66,19 @@ export const UsersTable: FC> = ({ - {Language.usernameLabel} - + {Language.usernameLabel} + + {Language.rolesLabel} - {Language.loginTypeLabel} - {Language.statusLabel} + + {Language.groupsLabel} + {Language.loginTypeLabel} + {Language.statusLabel} + {/* 1% is a trick to make the table cell width fit the content */} {canEditUsers && } diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 7537277688c5b..6cd953a470f72 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -157,6 +157,8 @@ export const UsersTableBody: FC< onUserRolesUpdate={onUpdateUserRoles} /> + 5 Groups + From cfd1807d8d4b440c803a3817d76a17cd9a7bb72a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 15 Oct 2023 21:45:50 +0000 Subject: [PATCH 03/42] fix: remove redundant css styles --- site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 6cd953a470f72..00923117709fa 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -315,12 +315,4 @@ const useStyles = makeStyles((theme) => ({ suspended: { color: theme.palette.text.secondary, }, - rolePill: { - backgroundColor: theme.palette.background.paperLight, - borderColor: theme.palette.divider, - }, - rolePillOwner: { - backgroundColor: theme.palette.info.dark, - borderColor: theme.palette.info.light, - }, })); From d278c20b57a06853de1935a412b5d811b0aac6e5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Sun, 15 Oct 2023 22:30:50 +0000 Subject: [PATCH 04/42] refactor: update EditRolesButton to use Sets to detect selections --- .../UsersTable/EditRolesButton.stories.tsx | 6 +- .../UsersPage/UsersTable/EditRolesButton.tsx | 12 +- .../UsersPage/UsersTable/UserRoleCell.tsx | 107 ++++++++++++------ 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx index 422faabb66574..4d515f3d850e2 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx @@ -17,10 +17,12 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const selectedRoleNames = new Set([MockUserAdminRole.name, MockOwnerRole.name]); + export const Open: Story = { args: { + selectedRoleNames, roles: MockSiteRoles, - selectedRoles: [MockUserAdminRole, MockOwnerRole], }, parameters: { chromatic: { delay: 300 }, @@ -30,8 +32,8 @@ export const Open: Story = { export const Loading: Story = { args: { isLoading: true, + selectedRoleNames, roles: MockSiteRoles, - selectedRoles: [MockUserAdminRole, MockOwnerRole], userLoginType: "password", oidcRoleSync: false, }, diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx index be5e38f9302a4..382e019398d89 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx @@ -57,7 +57,7 @@ const Option: React.FC<{ export interface EditRolesButtonProps { isLoading: boolean; roles: Role[]; - selectedRoles: Role[]; + selectedRoleNames: Set; onChange: (roles: Role["name"][]) => void; defaultIsOpen?: boolean; oidcRoleSync: boolean; @@ -66,7 +66,7 @@ export interface EditRolesButtonProps { export const EditRolesButton: FC = ({ roles, - selectedRoles, + selectedRoleNames, onChange, isLoading, defaultIsOpen = false, @@ -77,11 +77,11 @@ export const EditRolesButton: FC = ({ const anchorRef = useRef(null); const [isOpen, setIsOpen] = useState(defaultIsOpen); const id = isOpen ? "edit-roles-popover" : undefined; - const selectedRoleNames = selectedRoles.map((role) => role.name); const handleChange = (roleName: string) => { - if (selectedRoleNames.includes(roleName)) { - onChange(selectedRoleNames.filter((role) => role !== roleName)); + if (selectedRoleNames.has(roleName)) { + const serialized = [...selectedRoleNames]; + onChange(serialized.filter((role) => role !== roleName)); return; } @@ -137,7 +137,7 @@ export const EditRolesButton: FC = ({