diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dccdc383ccbf9..c602a727e389e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -947,7 +947,7 @@ export const getTemplateACL = async ( export const updateTemplateACL = async ( templateId: string, data: TypesGen.UpdateTemplateACL, -): Promise => { +): Promise<{ message: string }> => { const response = await axios.patch( `/api/v2/templates/${templateId}/acl`, data, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index e5a39a015b661..a295832fca00e 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -6,8 +6,13 @@ import { type TemplateVersion, CreateTemplateRequest, ProvisionerJob, + TemplateRole, } from "api/typesGenerated"; -import { type QueryClient, type QueryOptions } from "react-query"; +import { + MutationOptions, + type QueryClient, + type QueryOptions, +} from "react-query"; import { delay } from "utils/delay"; export const templateByNameKey = (orgId: string, name: string) => [ @@ -36,6 +41,53 @@ export const templates = (orgId: string) => { }; }; +export const templateACL = (templateId: string) => { + return { + queryKey: ["templateAcl", templateId], + queryFn: () => API.getTemplateACL(templateId), + }; +}; + +export const setUserRole = ( + queryClient: QueryClient, +): MutationOptions< + Awaited>, + unknown, + { templateId: string; userId: string; role: TemplateRole } +> => { + return { + mutationFn: ({ templateId, userId, role }) => + API.updateTemplateACL(templateId, { + user_perms: { + [userId]: role, + }, + }), + onSuccess: async (_res, { templateId }) => { + await queryClient.invalidateQueries(["templateAcl", templateId]); + }, + }; +}; + +export const setGroupRole = ( + queryClient: QueryClient, +): MutationOptions< + Awaited>, + unknown, + { templateId: string; groupId: string; role: TemplateRole } +> => { + return { + mutationFn: ({ templateId, groupId, role }) => + API.updateTemplateACL(templateId, { + group_perms: { + [groupId]: role, + }, + }), + onSuccess: async (_res, { templateId }) => { + await queryClient.invalidateQueries(["templateAcl", templateId]); + }, + }; +}; + export const templateExamples = (orgId: string) => { return { queryKey: [...getTemplatesQueryKey(orgId), "examples"], diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 898202bd51b7c..6cf11ec1faf62 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -1,7 +1,6 @@ import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"; -import { useMachine } from "@xstate/react"; import { Paywall } from "components/Paywall/Paywall"; import { Stack } from "components/Stack/Stack"; import { useFeatureVisibility } from "hooks/useFeatureVisibility"; @@ -9,10 +8,12 @@ import { useOrganizationId } from "hooks/useOrganizationId"; import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; -import { templateACLMachine } from "xServices/template/templateACLXService"; import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView"; import { docs } from "utils/docs"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { setGroupRole, setUserRole, templateACL } from "api/queries/templates"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; export const TemplatePermissionsPage: FC< React.PropsWithChildren @@ -20,10 +21,16 @@ export const TemplatePermissionsPage: FC< const organizationId = useOrganizationId(); const { template, permissions } = useTemplateSettings(); const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const [state, send] = useMachine(templateACLMachine, { - context: { templateId: template.id }, - }); - const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context; + const templateACLQuery = useQuery(templateACL(template.id)); + const queryClient = useQueryClient(); + + const addUserMutation = useMutation(setUserRole(queryClient)); + const updateUserMutation = useMutation(setUserRole(queryClient)); + const removeUserMutation = useMutation(setUserRole(queryClient)); + + const addGroupMutation = useMutation(setGroupRole(queryClient)); + const updateGroupMutation = useMutation(setGroupRole(queryClient)); + const removeGroupMutation = useMutation(setGroupRole(queryClient)); return ( <> @@ -58,29 +65,67 @@ export const TemplatePermissionsPage: FC< { - send("ADD_USER", { user, role, onDone: reset }); + onAddUser={async (user, role, reset) => { + await addUserMutation.mutateAsync({ + templateId: template.id, + userId: user.id, + role, + }); + reset(); }} - isAddingUser={state.matches("addingUser")} - onUpdateUser={(user, role) => { - send("UPDATE_USER_ROLE", { user, role }); + isAddingUser={addUserMutation.isLoading} + onUpdateUser={async (user, role) => { + await updateUserMutation.mutateAsync({ + templateId: template.id, + userId: user.id, + role, + }); + displaySuccess("User role updated successfully!"); }} - updatingUser={userToBeUpdated} - onRemoveUser={(user) => { - send("REMOVE_USER", { user }); + updatingUserId={ + updateUserMutation.isLoading + ? updateUserMutation.variables?.userId + : undefined + } + onRemoveUser={async (user) => { + await removeUserMutation.mutateAsync({ + templateId: template.id, + userId: user.id, + role: "", + }); + displaySuccess("User removed successfully!"); }} - onAddGroup={(group, role, reset) => { - send("ADD_GROUP", { group, role, onDone: reset }); + onAddGroup={async (group, role, reset) => { + await addGroupMutation.mutateAsync({ + templateId: template.id, + groupId: group.id, + role, + }); + reset(); }} - isAddingGroup={state.matches("addingGroup")} - onUpdateGroup={(group, role) => { - send("UPDATE_GROUP_ROLE", { group, role }); + isAddingGroup={addGroupMutation.isLoading} + onUpdateGroup={async (group, role) => { + await updateGroupMutation.mutateAsync({ + templateId: template.id, + groupId: group.id, + role, + }); + displaySuccess("Group role updated successfully!"); }} - updatingGroup={groupToBeUpdated} - onRemoveGroup={(group) => { - send("REMOVE_GROUP", { group }); + updatingGroupId={ + updateGroupMutation.isLoading + ? updateGroupMutation.variables?.groupId + : undefined + } + onRemoveGroup={async (group) => { + await removeGroupMutation.mutateAsync({ + groupId: group.id, + templateId: template.id, + role: "", + }); + displaySuccess("Group removed successfully!"); }} /> )} diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index e106242ccc443..a4cded2abe1b6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -161,7 +161,7 @@ export interface TemplatePermissionsPageViewProps { ) => void; isAddingUser: boolean; onUpdateUser: (user: TemplateUser, role: TemplateRole) => void; - updatingUser: TemplateUser | undefined; + updatingUserId: TemplateUser["id"] | undefined; onRemoveUser: (user: TemplateUser) => void; // Group onAddGroup: ( @@ -171,7 +171,7 @@ export interface TemplatePermissionsPageViewProps { ) => void; isAddingGroup: boolean; onUpdateGroup: (group: TemplateGroup, role: TemplateRole) => void; - updatingGroup: TemplateGroup | undefined; + updatingGroupId?: TemplateGroup["id"] | undefined; onRemoveGroup: (group: Group) => void; } @@ -185,13 +185,13 @@ export const TemplatePermissionsPageView: FC< // User onAddUser, isAddingUser, - updatingUser, + updatingUserId, onUpdateUser, onRemoveUser, // Group onAddGroup, isAddingGroup, - updatingGroup, + updatingGroupId, onUpdateGroup, onRemoveGroup, }) => { @@ -265,9 +265,7 @@ export const TemplatePermissionsPageView: FC< { onUpdateGroup( group, @@ -313,9 +311,7 @@ export const TemplatePermissionsPageView: FC< { onUpdateUser( user, diff --git a/site/src/xServices/template/templateACLXService.ts b/site/src/xServices/template/templateACLXService.ts deleted file mode 100644 index 9b0b1e7462d1d..0000000000000 --- a/site/src/xServices/template/templateACLXService.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { getTemplateACL, updateTemplateACL } from "api/api"; -import { - TemplateACL, - TemplateGroup, - TemplateRole, - TemplateUser, -} from "api/typesGenerated"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; -import { assign, createMachine } from "xstate"; - -export const templateACLMachine = createMachine( - { - schema: { - context: {} as { - templateId: string; - templateACL?: TemplateACL; - // User - userToBeAdded?: TemplateUser; - userToBeUpdated?: TemplateUser; - addUserCallback?: () => void; - // Group - groupToBeAdded?: TemplateGroup; - groupToBeUpdated?: TemplateGroup; - addGroupCallback?: () => void; - }, - services: {} as { - loadTemplateACL: { - data: TemplateACL; - }; - // User - addUser: { - data: unknown; - }; - updateUser: { - data: unknown; - }; - // Group - addGroup: { - data: unknown; - }; - updateGroup: { - data: unknown; - }; - }, - events: {} as // User - | { - type: "ADD_USER"; - user: TemplateUser; - role: TemplateRole; - onDone: () => void; - } - | { - type: "UPDATE_USER_ROLE"; - user: TemplateUser; - role: TemplateRole; - } - | { - type: "REMOVE_USER"; - user: TemplateUser; - } - // Group - | { - type: "ADD_GROUP"; - group: TemplateGroup; - role: TemplateRole; - onDone: () => void; - } - | { - type: "UPDATE_GROUP_ROLE"; - group: TemplateGroup; - role: TemplateRole; - } - | { - type: "REMOVE_GROUP"; - group: TemplateGroup; - }, - }, - tsTypes: {} as import("./templateACLXService.typegen").Typegen0, - id: "templateACL", - initial: "loading", - states: { - loading: { - invoke: { - src: "loadTemplateACL", - onDone: { - actions: ["assignTemplateACL"], - target: "idle", - }, - }, - }, - idle: { - on: { - // User - ADD_USER: { target: "addingUser", actions: ["assignUserToBeAdded"] }, - UPDATE_USER_ROLE: { - target: "updatingUser", - actions: ["assignUserToBeUpdated"], - }, - REMOVE_USER: { - target: "removingUser", - actions: ["removeUserFromTemplateACL"], - }, - // Group - ADD_GROUP: { - target: "addingGroup", - actions: ["assignGroupToBeAdded"], - }, - UPDATE_GROUP_ROLE: { - target: "updatingGroup", - actions: ["assignGroupToBeUpdated"], - }, - REMOVE_GROUP: { - target: "removingGroup", - actions: ["removeGroupFromTemplateACL"], - }, - }, - }, - // User - addingUser: { - invoke: { - src: "addUser", - onDone: { - target: "idle", - actions: ["addUserToTemplateACL", "runAddUserCallback"], - }, - }, - }, - updatingUser: { - invoke: { - src: "updateUser", - onDone: { - target: "idle", - actions: [ - "updateUserOnTemplateACL", - "clearUserToBeUpdated", - "displayUpdateUserSuccessMessage", - ], - }, - }, - }, - removingUser: { - invoke: { - src: "removeUser", - onDone: { - target: "idle", - actions: ["displayRemoveUserSuccessMessage"], - }, - }, - }, - // Group - addingGroup: { - invoke: { - src: "addGroup", - onDone: { - target: "idle", - actions: ["addGroupToTemplateACL", "runAddGroupCallback"], - }, - }, - }, - updatingGroup: { - invoke: { - src: "updateGroup", - onDone: { - target: "idle", - actions: [ - "updateGroupOnTemplateACL", - "clearGroupToBeUpdated", - "displayUpdateGroupSuccessMessage", - ], - }, - }, - }, - removingGroup: { - invoke: { - src: "removeGroup", - onDone: { - target: "idle", - actions: ["displayRemoveGroupSuccessMessage"], - }, - }, - }, - }, - }, - { - services: { - loadTemplateACL: ({ templateId }) => getTemplateACL(templateId), - // User - addUser: ({ templateId }, { user, role }) => - updateTemplateACL(templateId, { - user_perms: { - [user.id]: role, - }, - }), - updateUser: ({ templateId }, { user, role }) => - updateTemplateACL(templateId, { - user_perms: { - [user.id]: role, - }, - }), - removeUser: ({ templateId }, { user }) => - updateTemplateACL(templateId, { - user_perms: { - [user.id]: "", - }, - }), - // Group - addGroup: ({ templateId }, { group, role }) => - updateTemplateACL(templateId, { - group_perms: { - [group.id]: role, - }, - }), - updateGroup: ({ templateId }, { group, role }) => - updateTemplateACL(templateId, { - group_perms: { - [group.id]: role, - }, - }), - removeGroup: ({ templateId }, { group }) => - updateTemplateACL(templateId, { - group_perms: { - [group.id]: "", - }, - }), - }, - actions: { - assignTemplateACL: assign({ - templateACL: (_, { data }) => data, - }), - // User - assignUserToBeAdded: assign({ - userToBeAdded: (_, { user, role }) => ({ ...user, role }), - addUserCallback: (_, { onDone }) => onDone, - }), - addUserToTemplateACL: assign({ - templateACL: ({ templateACL, userToBeAdded }) => { - if (!userToBeAdded) { - throw new Error("No user to be added"); - } - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - users: [...templateACL.users, userToBeAdded], - }; - }, - }), - runAddUserCallback: ({ addUserCallback }) => { - if (addUserCallback) { - addUserCallback(); - } - }, - assignUserToBeUpdated: assign({ - userToBeUpdated: (_, { user, role }) => ({ ...user, role }), - }), - updateUserOnTemplateACL: assign({ - templateACL: ({ templateACL, userToBeUpdated }) => { - if (!userToBeUpdated) { - throw new Error("No user to be added"); - } - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - users: templateACL.users.map((oldTemplateUser) => { - return oldTemplateUser.id === userToBeUpdated.id - ? userToBeUpdated - : oldTemplateUser; - }), - }; - }, - }), - clearUserToBeUpdated: assign({ - userToBeUpdated: (_) => undefined, - }), - displayUpdateUserSuccessMessage: () => { - displaySuccess("User role update successfully!"); - }, - removeUserFromTemplateACL: assign({ - templateACL: ({ templateACL }, { user }) => { - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - users: templateACL.users.filter((oldTemplateUser) => { - return oldTemplateUser.id !== user.id; - }), - }; - }, - }), - displayRemoveUserSuccessMessage: () => { - displaySuccess("User removed successfully!"); - }, - // Group - assignGroupToBeAdded: assign({ - groupToBeAdded: (_, { group, role }) => ({ ...group, role }), - addGroupCallback: (_, { onDone }) => onDone, - }), - addGroupToTemplateACL: assign({ - templateACL: ({ templateACL, groupToBeAdded }) => { - if (!groupToBeAdded) { - throw new Error("No group to be added"); - } - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - group: [...templateACL.group, groupToBeAdded], - }; - }, - }), - runAddGroupCallback: ({ addGroupCallback }) => { - if (addGroupCallback) { - addGroupCallback(); - } - }, - assignGroupToBeUpdated: assign({ - groupToBeUpdated: (_, { group, role }) => ({ ...group, role }), - }), - updateGroupOnTemplateACL: assign({ - templateACL: ({ templateACL, groupToBeUpdated }) => { - if (!groupToBeUpdated) { - throw new Error("No group to be added"); - } - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - group: templateACL.group.map((oldTemplateGroup) => { - return oldTemplateGroup.id === groupToBeUpdated.id - ? groupToBeUpdated - : oldTemplateGroup; - }), - }; - }, - }), - clearGroupToBeUpdated: assign({ - groupToBeUpdated: (_) => undefined, - }), - displayUpdateGroupSuccessMessage: () => { - displaySuccess("Group role update successfully!"); - }, - removeGroupFromTemplateACL: assign({ - templateACL: ({ templateACL }, { group }) => { - if (!templateACL) { - throw new Error("Template ACL is not loaded yet"); - } - return { - ...templateACL, - group: templateACL.group.filter((oldTemplateGroup) => { - return oldTemplateGroup.id !== group.id; - }), - }; - }, - }), - displayRemoveGroupSuccessMessage: () => { - displaySuccess("Group removed successfully!"); - }, - }, - }, -);