Skip to content

feat: add user groups column to users table #10284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
cb51aa8
refactor: extract UserRoleCell into separate component
Parkreiner Oct 15, 2023
a33fe0c
wip: add placeholder Groups column
Parkreiner Oct 15, 2023
cfd1807
fix: remove redundant css styles
Parkreiner Oct 15, 2023
d278c20
refactor: update EditRolesButton to use Sets to detect selections
Parkreiner Oct 15, 2023
2fdbd65
wip: commit progress for updated roles column
Parkreiner Oct 16, 2023
0c9525e
wip: commit current role pill progress
Parkreiner Oct 16, 2023
2e2bf08
fix: update state sync logic
Parkreiner Oct 16, 2023
b8a73f5
Merge branch 'main' into mes/table-user-groups
Parkreiner Oct 17, 2023
52cb1bf
chore: add groupsByUserId query options factory
Parkreiner Oct 17, 2023
df9b25c
fix: update return value of select function
Parkreiner Oct 17, 2023
a29f395
chore: drill groups data down to cell component
Parkreiner Oct 17, 2023
fd8dafc
wip: commit current cell progress
Parkreiner Oct 17, 2023
3b11414
fix: remove redundant classes
Parkreiner Oct 17, 2023
083bb16
wip: commit current styling progress
Parkreiner Oct 17, 2023
0061dc0
fix: update line height for CTA
Parkreiner Oct 17, 2023
25767c6
fix: update spacing
Parkreiner Oct 17, 2023
c2aea9b
chore: add tooltip for Groups column header
Parkreiner Oct 17, 2023
534cf82
fix: remove tsbuild file
Parkreiner Oct 17, 2023
f8d8de5
refactor: consolidate tooltip components
Parkreiner Oct 18, 2023
3ec6823
fix: update font size defaults inside theme
Parkreiner Oct 18, 2023
85d4de0
fix: expand hoverable/clickable area of groups cell
Parkreiner Oct 18, 2023
376d20c
fix: remove possible undefined cases from HelpTooltip
Parkreiner Oct 18, 2023
b9d2b72
chore: add popover functionality to groups
Parkreiner Oct 18, 2023
dc2fedd
wip: commit progress on groups tooltip
Parkreiner Oct 18, 2023
1624981
fix: remove zero-height group name visual bug
Parkreiner Oct 18, 2023
81f252b
feat: get basic version of user group tooltips done
Parkreiner Oct 18, 2023
6cc07b4
perf: move sort order callback outside loop
Parkreiner Oct 18, 2023
f73ba71
fix: update spacing for tooltip
Parkreiner Oct 18, 2023
c646d9f
feat: make popovers entirely hover-based
Parkreiner Oct 18, 2023
c46b42a
fix: disable scroll locking for popover
Parkreiner Oct 18, 2023
3b36858
docs: add comments explaining some pitfalls with Popover component
Parkreiner Oct 18, 2023
49570b0
refactor: simplify userRoleCell implementation
Parkreiner Oct 18, 2023
d18641e
feat: complete main feature
Parkreiner Oct 18, 2023
40846c6
fix: prevent scroll lock for role tooltips
Parkreiner Oct 18, 2023
c678223
fix: change import to type import
Parkreiner Oct 18, 2023
c642ef8
refactor: simplify how groups are clustered
Parkreiner Oct 19, 2023
774e4d5
Merge branch 'main' into mes/table-user-groups
Parkreiner Oct 19, 2023
d2cc9cc
refactor: update UserRoleCell to use Popover
Parkreiner Oct 19, 2023
33af1ce
refactor: remove unnecessary fragment
Parkreiner Oct 19, 2023
ccb213f
chore: add id/aria support for Popover
Parkreiner Oct 19, 2023
88eaf4a
refactor: update UserGroupsCell to use Popover
Parkreiner Oct 19, 2023
271b704
chore: redo visual design for UserGroupsCell
Parkreiner Oct 19, 2023
8cf2324
fix: shrink UserGroupsCell text
Parkreiner Oct 19, 2023
2e4ce81
fix: update UsersTable test to include groups info
Parkreiner Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: drill groups data down to cell component
  • Loading branch information
Parkreiner committed Oct 17, 2023
commit a29f39525bec7780eb5f080a4eb84921308ab917
137 changes: 72 additions & 65 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import { User } from "api/typesGenerated";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { nonInitialPage } from "components/PaginationWidget/utils";
import { useMe } from "hooks/useMe";
import { usePermissions } from "hooks/usePermissions";
import { FC, ReactNode, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useSearchParams, useNavigate } from "react-router-dom";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ResetPasswordDialog } from "./ResetPasswordDialog";
import { pageTitle } from "utils/page";
import { UsersPageView } from "./UsersPageView";
import { useStatusFilterMenu } from "./UsersFilter";
import { useFilter } from "components/Filter/filter";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { type FC, type ReactNode, useState } from "react";
Copy link
Member Author

@Parkreiner Parkreiner Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost all the diffs for this file are me just moving things around to group them better, and renaming some variables for clarity. The major things to look for are any code involving groupsByUserIdQuery


import { type User } from "api/typesGenerated";
import { roles } from "api/queries/roles";
import { groupsByUserId } from "api/queries/groups";
import { getErrorMessage } from "api/errors";
import { deploymentConfig } from "api/queries/deployment";
import { prepareQuery } from "utils/filters";
import { usePagination } from "hooks";
import {
users,
suspendUser,
Expand All @@ -27,38 +14,55 @@ import {
updateRoles,
authMethods,
} from "api/queries/users";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";

import { useMutation, useQuery, useQueryClient } from "react-query";
import { useSearchParams, useNavigate } from "react-router-dom";
import { useOrganizationId, usePagination } from "hooks";
import { useMe } from "hooks/useMe";
import { usePermissions } from "hooks/usePermissions";
import { useStatusFilterMenu } from "./UsersFilter";
import { useFilter } from "components/Filter/filter";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { generateRandomString } from "utils/random";
import { prepareQuery } from "utils/filters";

import { Helmet } from "react-helmet-async";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { nonInitialPage } from "components/PaginationWidget/utils";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ResetPasswordDialog } from "./ResetPasswordDialog";
import { pageTitle } from "utils/page";
import { UsersPageView } from "./UsersPageView";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";

export const UsersPage: FC<{ children?: ReactNode }> = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();

const searchParamsResult = useSearchParams();
const { entitlements } = useDashboard();
const [searchParams] = searchParamsResult;
const filter = searchParams.get("filter") ?? "";
const pagination = usePagination({
searchParamsResult,
});

const pagination = usePagination({ searchParamsResult });
const usersQuery = useQuery(
users({
q: prepareQuery(filter),
q: prepareQuery(searchParams.get("filter") ?? ""),
limit: pagination.limit,
offset: pagination.offset,
}),
);

const organizationId = useOrganizationId();
const groupsByUserIdQuery = useQuery(groupsByUserId(organizationId));
const authMethodsQuery = useQuery(authMethods());

const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions();
const rolesQuery = useQuery(roles());
const { data: deploymentValues } = useQuery({
...deploymentConfig(),
enabled: viewDeploymentValues,
});
// Indicates if oidc roles are synced from the oidc idp.
// Assign 'false' if unknown.
const oidcRoleSyncEnabled =
viewDeploymentValues &&
deploymentValues?.config.oidc?.user_role_field !== "";

const me = useMe();
const useFilterResult = useFilter({
searchParamsResult,
Expand All @@ -74,36 +78,47 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
status: option?.value,
}),
});
const authMethodsQuery = useQuery(authMethods());
const isLoading =
usersQuery.isLoading || rolesQuery.isLoading || authMethodsQuery.isLoading;

const [confirmSuspendUser, setConfirmSuspendUser] = useState<User>();
const [userToSuspend, setUserToSuspend] = useState<User>();
const suspendUserMutation = useMutation(suspendUser(queryClient));

const [confirmActivateUser, setConfirmActivateUser] = useState<User>();
const [userToActivate, setUserToActivate] = useState<User>();
const activateUserMutation = useMutation(activateUser(queryClient));

const [confirmDeleteUser, setConfirmDeleteUser] = useState<User>();
const [userToDelete, setUserToDelete] = useState<User>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Way better names 🙏

const deleteUserMutation = useMutation(deleteUser(queryClient));

const [confirmResetPassword, setConfirmResetPassword] = useState<{
user: User;
newPassword: string;
}>();
const updatePasswordMutation = useMutation(updatePassword());

const updatePasswordMutation = useMutation(updatePassword());
const updateRolesMutation = useMutation(updateRoles(queryClient));

// Indicates if oidc roles are synced from the oidc idp.
// Assign 'false' if unknown.
const oidcRoleSyncEnabled =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I'm not completely sure. I didn't change the code or the comment, and I didn't dig deep enough into some of the components to see where it was actually used

viewDeploymentValues &&
deploymentValues?.config.oidc?.user_role_field !== "";

const isLoading =
usersQuery.isLoading ||
rolesQuery.isLoading ||
authMethodsQuery.isLoading ||
groupsByUserIdQuery.isLoading;

return (
<>
<Helmet>
<title>{pageTitle("Users")}</title>
</Helmet>

<UsersPageView
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
roles={rolesQuery.data}
users={usersQuery.data?.users}
groupsByUserId={groupsByUserIdQuery.data}
authMethods={authMethodsQuery.data}
onListWorkspaces={(user) => {
navigate(
Expand All @@ -116,9 +131,9 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
"/audit?filter=" + encodeURIComponent(`username:${user.username}`),
);
}}
onDeleteUser={setConfirmDeleteUser}
onSuspendUser={setConfirmSuspendUser}
onActivateUser={setConfirmActivateUser}
onDeleteUser={setUserToDelete}
onSuspendUser={setUserToSuspend}
onActivateUser={setUserToActivate}
onResetUserPassword={(user) => {
setConfirmResetPassword({
user,
Expand Down Expand Up @@ -147,9 +162,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
filterProps={{
filter: useFilterResult,
error: usersQuery.error,
menus: {
status: statusMenu,
},
menus: { status: statusMenu },
}}
count={usersQuery.data?.count}
page={pagination.page}
Expand All @@ -158,75 +171,69 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
/>

<DeleteDialog
key={confirmDeleteUser?.username}
isOpen={confirmDeleteUser !== undefined}
key={userToDelete?.username}
isOpen={userToDelete !== undefined}
confirmLoading={deleteUserMutation.isLoading}
name={confirmDeleteUser?.username ?? ""}
name={userToDelete?.username ?? ""}
entity="user"
onCancel={() => setUserToDelete(undefined)}
onConfirm={async () => {
try {
await deleteUserMutation.mutateAsync(confirmDeleteUser!.id);
setConfirmDeleteUser(undefined);
await deleteUserMutation.mutateAsync(userToDelete!.id);
setUserToDelete(undefined);
displaySuccess("Successfully deleted the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error deleting user."));
}
}}
onCancel={() => {
setConfirmDeleteUser(undefined);
}}
/>

<ConfirmDialog
type="delete"
hideCancel={false}
open={confirmSuspendUser !== undefined}
open={userToSuspend !== undefined}
confirmLoading={suspendUserMutation.isLoading}
title="Suspend user"
confirmText="Suspend"
onClose={() => setUserToSuspend(undefined)}
onConfirm={async () => {
try {
await suspendUserMutation.mutateAsync(confirmSuspendUser!.id);
setConfirmSuspendUser(undefined);
await suspendUserMutation.mutateAsync(userToSuspend!.id);
setUserToSuspend(undefined);
displaySuccess("Successfully suspended the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error suspending user."));
}
}}
onClose={() => {
setConfirmSuspendUser(undefined);
}}
description={
<>
Do you want to suspend the user{" "}
<strong>{confirmSuspendUser?.username ?? ""}</strong>?
<strong>{userToSuspend?.username ?? ""}</strong>?
</>
}
/>

<ConfirmDialog
type="success"
hideCancel={false}
open={confirmActivateUser !== undefined}
open={userToActivate !== undefined}
confirmLoading={activateUserMutation.isLoading}
title="Activate user"
confirmText="Activate"
onClose={() => setUserToActivate(undefined)}
onConfirm={async () => {
try {
await activateUserMutation.mutateAsync(confirmActivateUser!.id);
setConfirmActivateUser(undefined);
await activateUserMutation.mutateAsync(userToActivate!.id);
setUserToActivate(undefined);
displaySuccess("Successfully activated the user.");
} catch (e) {
displayError(getErrorMessage(e, "Error activating user."));
}
}}
onClose={() => {
setConfirmActivateUser(undefined);
}}
description={
<>
Do you want to activate{" "}
<strong>{confirmActivateUser?.username ?? ""}</strong>?
<strong>{userToActivate?.username ?? ""}</strong>?
</>
}
/>
Expand Down
10 changes: 8 additions & 2 deletions site/src/pages/UsersPage/UsersPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ComponentProps, FC } from "react";
import * as TypesGen from "api/typesGenerated";
import { type ComponentProps, type FC } from "react";
import type * as TypesGen from "api/typesGenerated";
import { type GroupsByUserId } from "api/queries/groups";

import { UsersTable } from "./UsersTable/UsersTable";
import { UsersFilter } from "./UsersFilter";
import {
Expand Down Expand Up @@ -30,6 +32,8 @@ export interface UsersPageViewProps {
filterProps: ComponentProps<typeof UsersFilter>;
isNonInitialPage: boolean;
actorID: string;
groupsByUserId: GroupsByUserId | undefined;

// Pagination
count?: number;
page: number;
Expand Down Expand Up @@ -60,6 +64,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
limit,
onPageChange,
page,
groupsByUserId,
}) => {
return (
<>
Expand All @@ -77,6 +82,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
<UsersTable
users={users}
roles={roles}
groupsByUserId={groupsByUserId}
onSuspendUser={onSuspendUser}
onDeleteUser={onDeleteUser}
onListWorkspaces={onListWorkspaces}
Expand Down
12 changes: 12 additions & 0 deletions site/src/pages/UsersPage/UsersTable/GroupsCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import TableCell from "@mui/material/TableCell";
import { GroupsByUserId } from "api/queries/groups";
import { User } from "api/typesGenerated";

type GroupsCellProps = {
user: User;
groupsByUserId: GroupsByUserId | undefined;
};

export function GroupsCell({ user, groupsByUserId }: GroupsCellProps) {
return <TableCell>5 Groups</TableCell>;
}
13 changes: 9 additions & 4 deletions site/src/pages/UsersPage/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { type FC } from "react";
import type * as TypesGen from "api/typesGenerated";
import { type GroupsByUserId } from "api/queries/groups";

import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import { FC } from "react";
import * as TypesGen from "api/typesGenerated";
import { Stack } from "components/Stack/Stack";
import { UserRoleHelpTooltip } from "./UserRoleHelpTooltip";
import { UsersTableBody } from "./UsersTableBody";
Expand All @@ -20,8 +22,9 @@ export const Language = {
} as const;

export interface UsersTableProps {
users?: TypesGen.User[];
roles?: TypesGen.AssignableRoles[];
users: TypesGen.User[] | undefined;
roles: TypesGen.AssignableRoles[] | undefined;
groupsByUserId: GroupsByUserId | undefined;
isUpdatingUserRoles?: boolean;
canEditUsers: boolean;
canViewActivity?: boolean;
Expand Down Expand Up @@ -60,6 +63,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
actorID,
oidcRoleSyncEnabled,
authMethods,
groupsByUserId,
}) => {
return (
<TableContainer>
Expand Down Expand Up @@ -88,6 +92,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
<UsersTableBody
users={users}
roles={roles}
groupsByUserId={groupsByUserId}
isLoading={isLoading}
canEditUsers={canEditUsers}
canViewActivity={canViewActivity}
Expand Down
8 changes: 6 additions & 2 deletions site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ import GitHub from "@mui/icons-material/GitHub";
import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
import ShieldOutlined from "@mui/icons-material/ShieldOutlined";
import { UserRoleCell } from "./UserRoleCell";
import { GroupsByUserId } from "api/queries/groups";
import { GroupsCell } from "./GroupsCell";

dayjs.extend(relativeTime);

interface UsersTableBodyProps {
users?: TypesGen.User[];
users: TypesGen.User[] | undefined;
groupsByUserId: GroupsByUserId | undefined;
authMethods?: TypesGen.AuthMethods;
roles?: TypesGen.AssignableRoles[];
isUpdatingUserRoles?: boolean;
Expand Down Expand Up @@ -72,6 +75,7 @@ export const UsersTableBody: FC<
isNonInitialPage,
actorID,
oidcRoleSyncEnabled,
groupsByUserId,
}) => {
return (
<ChooseOne>
Expand Down Expand Up @@ -153,7 +157,7 @@ export const UsersTableBody: FC<
onUserRolesUpdate={onUpdateUserRoles}
/>

<TableCell>5 Groups</TableCell>
<GroupsCell groupsByUserId={groupsByUserId} user={user} />

<TableCell>
<LoginType authMethods={authMethods!} value={user.login_type} />
Expand Down