diff --git a/site/src/modules/groups.ts b/site/src/modules/groups.ts index fc642d7769f4f..5e83d6649acd4 100644 --- a/site/src/modules/groups.ts +++ b/site/src/modules/groups.ts @@ -1,15 +1,21 @@ -import type { Group, ReducedUser, User } from "api/typesGenerated"; +import type { + Group, + ReducedUser, + User, + WorkspaceUser, +} from "api/typesGenerated"; -type UserOrGroupAutocompleteValue = User | ReducedUser | Group | null; +/** + * Union of all user-like types that can be distinguished from Group. + */ +type UserLike = User | ReducedUser | WorkspaceUser; /** * Type guard to check if the value is a Group. * Groups have a "members" property that users don't have. */ -export const isGroup = ( - value: UserOrGroupAutocompleteValue, -): value is Group => { - return value !== null && typeof value === "object" && "members" in value; +export const isGroup = (value: UserLike | Group): value is Group => { + return "members" in value; }; /** diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx new file mode 100644 index 0000000000000..2529b2df567e7 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx @@ -0,0 +1,304 @@ +import type { + Group, + WorkspaceACL, + WorkspaceGroup, + WorkspaceRole, + WorkspaceUser, +} from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Avatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/Avatar/AvatarData"; +import { Button } from "components/Button/Button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "components/Select/Select"; +import { Spinner } from "components/Spinner/Spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { EllipsisVertical, UserPlusIcon } from "lucide-react"; +import { getGroupSubtitle } from "modules/groups"; +import type { FC, ReactNode } from "react"; + +interface RoleSelectProps { + value: WorkspaceRole; + disabled?: boolean; + onValueChange: (value: WorkspaceRole) => void; +} + +const RoleSelect: FC = ({ + value, + disabled, + onValueChange, +}) => { + const roleLabels: Record = { + use: "Use", + admin: "Admin", + "": "", + }; + + return ( + + ); +}; + +type AddWorkspaceMemberFormProps = { + isLoading: boolean; + onSubmit: () => void; + disabled: boolean; + children: ReactNode; +}; + +export const AddWorkspaceMemberForm: FC = ({ + isLoading, + onSubmit, + disabled, + children, +}) => { + return ( +
+
+ {children} + +
+
+ ); +}; + +type RoleSelectFieldProps = { + value: WorkspaceRole; + onChange: (value: WorkspaceRole) => void; + disabled?: boolean; +}; + +export const RoleSelectField: FC = ({ + value, + onChange, + disabled, +}) => { + return ( + + ); +}; + +interface WorkspaceSharingFormProps { + workspaceACL: WorkspaceACL | undefined; + canUpdatePermissions: boolean; + error: unknown; + onUpdateUser: (user: WorkspaceUser, role: WorkspaceRole) => void; + updatingUserId: WorkspaceUser["id"] | undefined; + onRemoveUser: (user: WorkspaceUser) => void; + onUpdateGroup: (group: WorkspaceGroup, role: WorkspaceRole) => void; + updatingGroupId?: WorkspaceGroup["id"] | undefined; + onRemoveGroup: (group: Group) => void; + addMemberForm?: ReactNode; +} + +export const WorkspaceSharingForm: FC = ({ + workspaceACL, + canUpdatePermissions, + error, + updatingUserId, + onUpdateUser, + onRemoveUser, + updatingGroupId, + onUpdateGroup, + onRemoveGroup, + addMemberForm, +}) => { + const isEmpty = Boolean( + workspaceACL && + workspaceACL.users.length === 0 && + workspaceACL.group.length === 0, + ); + + return ( +
+ {Boolean(error) && } + {canUpdatePermissions && addMemberForm} + + + + Member + Role + + + + + {!workspaceACL ? ( + + ) : isEmpty ? ( + + + + + + ) : ( + <> + {workspaceACL.group.map((group) => ( + + + + } + title={group.display_name || group.name} + subtitle={getGroupSubtitle(group)} + /> + + + {canUpdatePermissions ? ( + onUpdateGroup(group, value)} + /> + ) : ( +
{group.role}
+ )} +
+ + + {canUpdatePermissions && ( + + + + + + onRemoveGroup(group)} + > + Remove + + + + )} + +
+ ))} + + {workspaceACL.users.map((user) => ( + + + + + + {canUpdatePermissions ? ( + onUpdateUser(user, value)} + /> + ) : ( +
{user.role}
+ )} +
+ + + {canUpdatePermissions && ( + + + + + + onRemoveUser(user)} + > + Remove + + + + )} + +
+ ))} + + )} +
+
+
+ ); +}; diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing.ts b/site/src/modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing.ts new file mode 100644 index 0000000000000..819eaecd0b937 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing.ts @@ -0,0 +1,128 @@ +import { + setWorkspaceGroupRole, + setWorkspaceUserRole, + workspaceACL, +} from "api/queries/workspaces"; +import type { + Group, + Workspace, + WorkspaceGroup, + WorkspaceRole, + WorkspaceUser, +} from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { useMutation, useQuery, useQueryClient } from "react-query"; + +/** + * Encapsulates all data fetching and mutations for workspace sharing. + * This hook manages the workspace ACL query and provides methods to + * add, update, and remove users and groups from the workspace. + */ +export function useWorkspaceSharing(workspace: Workspace) { + const queryClient = useQueryClient(); + + const workspaceACLQuery = useQuery(workspaceACL(workspace.id)); + + const addUserMutation = useMutation(setWorkspaceUserRole(queryClient)); + const updateUserMutation = useMutation(setWorkspaceUserRole(queryClient)); + const removeUserMutation = useMutation(setWorkspaceUserRole(queryClient)); + + const addGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); + const updateGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); + const removeGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); + + const addUser = async ( + user: WorkspaceUser, + role: WorkspaceRole, + reset: () => void, + ) => { + await addUserMutation.mutateAsync({ + workspaceId: workspace.id, + userId: user.id, + role, + }); + displaySuccess("User added to workspace successfully!"); + reset(); + }; + + const updateUser = async (user: WorkspaceUser, role: WorkspaceRole) => { + await updateUserMutation.mutateAsync({ + workspaceId: workspace.id, + userId: user.id, + role, + }); + displaySuccess("User role updated successfully!"); + }; + + const removeUser = async (user: WorkspaceUser) => { + await removeUserMutation.mutateAsync({ + workspaceId: workspace.id, + userId: user.id, + role: "", + }); + displaySuccess("User removed successfully!"); + }; + + const addGroup = async ( + group: Group, + role: WorkspaceRole, + reset: () => void, + ) => { + await addGroupMutation.mutateAsync({ + workspaceId: workspace.id, + groupId: group.id, + role, + }); + displaySuccess("Group added to workspace successfully!"); + reset(); + }; + + const updateGroup = async (group: WorkspaceGroup, role: WorkspaceRole) => { + await updateGroupMutation.mutateAsync({ + workspaceId: workspace.id, + groupId: group.id, + role, + }); + displaySuccess("Group role updated successfully!"); + }; + + const removeGroup = async (group: Group) => { + await removeGroupMutation.mutateAsync({ + workspaceId: workspace.id, + groupId: group.id, + role: "", + }); + displaySuccess("Group removed successfully!"); + }; + + const mutationError = + addUserMutation.error ?? + updateUserMutation.error ?? + removeUserMutation.error ?? + addGroupMutation.error ?? + updateGroupMutation.error ?? + removeGroupMutation.error; + + return { + workspaceACL: workspaceACLQuery.data, + isLoading: workspaceACLQuery.isLoading, + error: workspaceACLQuery.error, + mutationError, + // User actions + addUser, + updateUser, + removeUser, + isAddingUser: addUserMutation.isPending, + updatingUserId: updateUserMutation.isPending + ? updateUserMutation.variables?.userId + : undefined, + // Group actions + addGroup, + updateGroup, + removeGroup, + isAddingGroup: addGroupMutation.isPending, + updatingGroupId: updateGroupMutation.isPending + ? updateGroupMutation.variables?.groupId + : undefined, + } as const; +} diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx index ccf0251f03ac1..124baf80ef024 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx @@ -1,16 +1,10 @@ import { checkAuthorization } from "api/queries/authCheck"; -import { - setWorkspaceGroupRole, - setWorkspaceUserRole, - workspaceACL, -} from "api/queries/workspaces"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; import { Link } from "components/Link/Link"; import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { workspaceChecks } from "modules/workspaces/permissions"; +import { useWorkspaceSharing } from "modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing"; import type { FC } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; +import { useQuery } from "react-query"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; @@ -18,32 +12,17 @@ import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView"; const WorkspaceSharingPage: FC = () => { const workspace = useWorkspaceSettings(); - const queryClient = useQueryClient(); + const sharing = useWorkspaceSharing(workspace); - const workspaceACLQuery = useQuery(workspaceACL(workspace.id)); const checks = workspaceChecks(workspace); const permissionsQuery = useQuery({ ...checkAuthorization({ checks }), }); const permissions = permissionsQuery.data; - - const addUserMutation = useMutation(setWorkspaceUserRole(queryClient)); - const updateUserMutation = useMutation(setWorkspaceUserRole(queryClient)); - const removeUserMutation = useMutation(setWorkspaceUserRole(queryClient)); - - const addGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); - const updateGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); - const removeGroupMutation = useMutation(setWorkspaceGroupRole(queryClient)); - const canUpdatePermissions = Boolean(permissions?.updateWorkspace); - const mutationError = - addUserMutation.error ?? - updateUserMutation.error ?? - removeUserMutation.error ?? - addGroupMutation.error ?? - updateGroupMutation.error ?? - removeGroupMutation.error; + const error = + sharing.error ?? permissionsQuery.error ?? sharing.mutationError; return (
@@ -60,80 +39,21 @@ const WorkspaceSharingPage: FC = () => {
- {workspaceACLQuery.isError && ( - - )} - {permissionsQuery.isError && ( - - )} - {Boolean(mutationError) && } - { - await addUserMutation.mutateAsync({ - workspaceId: workspace.id, - userId: user.id, - role, - }); - displaySuccess("User added to workspace successfully!"); - reset(); - }} - isAddingUser={addUserMutation.isPending} - onUpdateUser={async (user, role) => { - await updateUserMutation.mutateAsync({ - workspaceId: workspace.id, - userId: user.id, - role, - }); - displaySuccess("User role updated successfully!"); - }} - updatingUserId={ - updateUserMutation.isPending - ? updateUserMutation.variables?.userId - : undefined - } - onRemoveUser={async (user) => { - await removeUserMutation.mutateAsync({ - workspaceId: workspace.id, - userId: user.id, - role: "", - }); - displaySuccess("User removed successfully!"); - }} - onAddGroup={async (group, role, reset) => { - await addGroupMutation.mutateAsync({ - workspaceId: workspace.id, - groupId: group.id, - role, - }); - displaySuccess("Group added to workspace successfully!"); - reset(); - }} - isAddingGroup={addGroupMutation.isPending} - onUpdateGroup={async (group, role) => { - await updateGroupMutation.mutateAsync({ - workspaceId: workspace.id, - groupId: group.id, - role, - }); - displaySuccess("Group role updated successfully!"); - }} - updatingGroupId={ - updateGroupMutation.isPending - ? updateGroupMutation.variables?.groupId - : undefined - } - onRemoveGroup={async (group) => { - await removeGroupMutation.mutateAsync({ - workspaceId: workspace.id, - groupId: group.id, - role: "", - }); - displaySuccess("Group removed successfully!"); - }} + error={error} + onAddUser={sharing.addUser} + isAddingUser={sharing.isAddingUser} + onUpdateUser={sharing.updateUser} + updatingUserId={sharing.updatingUserId} + onRemoveUser={sharing.removeUser} + onAddGroup={sharing.addGroup} + isAddingGroup={sharing.isAddingGroup} + onUpdateGroup={sharing.updateGroup} + updatingGroupId={sharing.updatingGroupId} + onRemoveGroup={sharing.removeGroup} /> ); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx index c4fe8a2cc5eff..0a0d79bd1f494 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx @@ -7,35 +7,12 @@ import type { WorkspaceRole, WorkspaceUser, } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { AvatarData } from "components/Avatar/AvatarData"; -import { Button } from "components/Button/Button"; +import { isGroup } from "modules/groups"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "components/DropdownMenu/DropdownMenu"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "components/Select/Select"; -import { Spinner } from "components/Spinner/Spinner"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "components/Table/Table"; -import { TableLoader } from "components/TableLoader/TableLoader"; -import { EllipsisVertical, UserPlusIcon } from "lucide-react"; -import { getGroupSubtitle } from "modules/groups"; + AddWorkspaceMemberForm, + RoleSelectField, + WorkspaceSharingForm, +} from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm"; import { type FC, useState } from "react"; import { UserOrGroupAutocomplete, @@ -72,10 +49,10 @@ const AddWorkspaceUserOrGroup: FC = ({ }; return ( -
{ - event.preventDefault(); - + { if (selectedOption && selectedRole) { onSubmit( { @@ -88,85 +65,21 @@ const AddWorkspaceUserOrGroup: FC = ({ } }} > -
- { - setSelectedOption(newValue); - }} - /> - - - - -
- - ); -}; - -interface RoleSelectProps { - value: WorkspaceRole; - disabled?: boolean; - onValueChange: (value: WorkspaceRole) => void; -} - -const RoleSelect: FC = ({ - value, - disabled, - onValueChange, -}) => { - const roleLabels: Record = { - use: "Use", - admin: "Admin", - "": "", - }; - - return ( - + { + setSelectedOption(newValue); + }} + /> + + +
); }; @@ -174,6 +87,7 @@ interface WorkspaceSharingPageViewProps { workspace: Workspace; workspaceACL: WorkspaceACL | undefined; canUpdatePermissions: boolean; + error: unknown; onAddUser: ( user: WorkspaceUser, role: WorkspaceRole, @@ -194,6 +108,7 @@ export const WorkspaceSharingPageView: FC = ({ workspace, workspaceACL, canUpdatePermissions, + error, onAddUser, isAddingUser, updatingUserId, @@ -205,153 +120,29 @@ export const WorkspaceSharingPageView: FC = ({ onUpdateGroup, onRemoveGroup, }) => { - const isEmpty = Boolean( - workspaceACL && - workspaceACL.users.length === 0 && - workspaceACL.group.length === 0, - ); - return ( -
- {canUpdatePermissions && ( + - "members" in value + isGroup(value) ? onAddGroup(value, role, resetAutocomplete) : onAddUser(value, role, resetAutocomplete) } /> - )} - - - - Member - Role - - - - - {!workspaceACL ? ( - - ) : isEmpty ? ( - - - - - - ) : ( - <> - {workspaceACL.group.map((group) => ( - - - - } - title={group.display_name || group.name} - subtitle={getGroupSubtitle(group)} - /> - - - {canUpdatePermissions ? ( - onUpdateGroup(group, value)} - /> - ) : ( -
{group.role}
- )} -
- - - {canUpdatePermissions && ( - - - - - - onRemoveGroup(group)} - > - Remove - - - - )} - -
- ))} - - {workspaceACL.users.map((user) => ( - - - - - - {canUpdatePermissions ? ( - onUpdateUser(user, value)} - /> - ) : ( -
{user.role}
- )} -
- - - {canUpdatePermissions && ( - - - - - - onRemoveUser(user)} - > - Remove - - - - )} - -
- ))} - - )} -
-
-
+ } + /> ); };