coder/site/src/pages/UsersPage/UsersPage.tsx

266 lines
8.1 KiB
TypeScript

import { useMachine } from "@xstate/react"
import { User } from "api/typesGenerated"
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
import {
getPaginationContext,
nonInitialPage,
} from "components/PaginationWidget/utils"
import { useMe } from "hooks/useMe"
import { usePermissions } from "hooks/usePermissions"
import { FC, ReactNode, useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { useSearchParams, useNavigate } from "react-router-dom"
import { siteRolesMachine } from "xServices/roles/siteRolesXService"
import { usersMachine } from "xServices/users/usersXService"
import { ConfirmDialog } from "../../components/Dialogs/ConfirmDialog/ConfirmDialog"
import { ResetPasswordDialog } from "../../components/Dialogs/ResetPasswordDialog/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 { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"
export const Language = {
suspendDialogTitle: "Suspend user",
suspendDialogAction: "Suspend",
suspendDialogMessagePrefix: "Do you want to suspend the user",
activateDialogTitle: "Activate user",
activateDialogAction: "Activate",
activateDialogMessagePrefix: "Do you want to activate the user",
}
const getSelectedUser = (id: string, users?: User[]) =>
users?.find((u) => u.id === id)
export const UsersPage: FC<{ children?: ReactNode }> = () => {
const navigate = useNavigate()
const searchParamsResult = useSearchParams()
const { entitlements } = useDashboard()
const [searchParams, setSearchParams] = searchParamsResult
const filter = searchParams.get("filter") ?? ""
const [usersState, usersSend] = useMachine(usersMachine, {
context: {
filter,
paginationContext: getPaginationContext(searchParams),
},
actions: {
updateURL: (context, event) =>
setSearchParams({ page: event.page, filter: context.filter }),
},
})
const {
users,
getUsersError,
usernameToDelete,
usernameToSuspend,
usernameToActivate,
userIdToResetPassword,
newUserPassword,
paginationRef,
count,
} = usersState.context
const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions()
const [rolesState] = useMachine(siteRolesMachine, {
context: {
hasPermission: canEditUsers,
},
})
const { roles } = rolesState.context
// Ideally this only runs if 'canViewDeployment' is true.
// TODO: Prevent api call if the user does not have the perms.
const [state] = useMachine(deploymentConfigMachine)
const { deploymentValues } = state.context
// Indicates if oidc roles are synced from the oidc idp.
// Assign 'false' if unknown.
const oidcRoleSyncEnabled =
viewDeploymentValues &&
deploymentValues?.config.oidc?.user_role_field !== ""
// Is loading if
// - users are loading or
// - the user can edit the users but the roles are loading
const isLoading =
usersState.matches("gettingUsers") ||
(canEditUsers && rolesState.matches("gettingRoles"))
const me = useMe()
const useFilterResult = useFilter({
searchParamsResult,
onUpdate: () => {
usersSend({ type: "UPDATE_PAGE", page: "1" })
},
})
useEffect(() => {
usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query })
}, [useFilterResult.query, usersSend])
const statusMenu = useStatusFilterMenu({
value: useFilterResult.values.status,
onChange: (option) =>
useFilterResult.update({
...useFilterResult.values,
status: option?.value,
}),
})
return (
<>
<Helmet>
<title>{pageTitle("Users")}</title>
</Helmet>
<UsersPageView
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
roles={roles}
users={users}
count={count}
onListWorkspaces={(user) => {
navigate(
"/workspaces?filter=" +
encodeURIComponent(`owner:${user.username}`),
)
}}
onViewActivity={(user) => {
navigate(
"/audit?filter=" + encodeURIComponent(`username:${user.username}`),
)
}}
onDeleteUser={(user) => {
usersSend({
type: "DELETE_USER",
userId: user.id,
username: user.username,
})
}}
onSuspendUser={(user) => {
usersSend({
type: "SUSPEND_USER",
userId: user.id,
username: user.username,
})
}}
onActivateUser={(user) => {
usersSend({
type: "ACTIVATE_USER",
userId: user.id,
username: user.username,
})
}}
onResetUserPassword={(user) => {
usersSend({ type: "RESET_USER_PASSWORD", userId: user.id })
}}
onUpdateUserRoles={(user, roles) => {
usersSend({
type: "UPDATE_USER_ROLES",
userId: user.id,
roles,
})
}}
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
isLoading={isLoading}
canEditUsers={canEditUsers}
canViewActivity={entitlements.features.audit_log.enabled}
paginationRef={paginationRef}
isNonInitialPage={nonInitialPage(searchParams)}
actorID={me.id}
filterProps={{
filter: useFilterResult,
error: getUsersError,
menus: {
status: statusMenu,
},
}}
/>
<DeleteDialog
key={usernameToDelete}
isOpen={
usersState.matches("confirmUserDeletion") ||
usersState.matches("deletingUser")
}
confirmLoading={usersState.matches("deletingUser")}
name={usernameToDelete ?? ""}
entity="user"
onConfirm={() => {
usersSend("CONFIRM_USER_DELETE")
}}
onCancel={() => {
usersSend("CANCEL_USER_DELETE")
}}
/>
<ConfirmDialog
type="delete"
hideCancel={false}
open={
usersState.matches("confirmUserSuspension") ||
usersState.matches("suspendingUser")
}
confirmLoading={usersState.matches("suspendingUser")}
title={Language.suspendDialogTitle}
confirmText={Language.suspendDialogAction}
onConfirm={() => {
usersSend("CONFIRM_USER_SUSPENSION")
}}
onClose={() => {
usersSend("CANCEL_USER_SUSPENSION")
}}
description={
<>
{Language.suspendDialogMessagePrefix}
{usernameToSuspend && " "}
<strong>{usernameToSuspend ?? ""}</strong>?
</>
}
/>
<ConfirmDialog
type="success"
hideCancel={false}
open={
usersState.matches("confirmUserActivation") ||
usersState.matches("activatingUser")
}
confirmLoading={usersState.matches("activatingUser")}
title={Language.activateDialogTitle}
confirmText={Language.activateDialogAction}
onConfirm={() => {
usersSend("CONFIRM_USER_ACTIVATION")
}}
onClose={() => {
usersSend("CANCEL_USER_ACTIVATION")
}}
description={
<>
{Language.activateDialogMessagePrefix}
{usernameToActivate && " "}
<strong>{usernameToActivate ?? ""}</strong>?
</>
}
/>
{userIdToResetPassword && (
<ResetPasswordDialog
open={
usersState.matches("confirmUserPasswordReset") ||
usersState.matches("resettingUserPassword")
}
loading={usersState.matches("resettingUserPassword")}
user={getSelectedUser(userIdToResetPassword, users)}
newPassword={newUserPassword}
onClose={() => {
usersSend("CANCEL_USER_PASSWORD_RESET")
}}
onConfirm={() => {
usersSend("CONFIRM_USER_PASSWORD_RESET")
}}
/>
)}
</>
)
}
export default UsersPage