From 07a4bd4f8f879bb22149c4859df352384bd83354 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 15 Sep 2023 13:08:11 +0000 Subject: [PATCH 1/7] Remove groups service --- site/src/api/queries/groups.ts | 8 +++ site/src/pages/GroupsPage/GroupsPage.tsx | 26 +++++---- site/src/xServices/groups/groupsXService.ts | 60 --------------------- 3 files changed, 23 insertions(+), 71 deletions(-) create mode 100644 site/src/api/queries/groups.ts delete mode 100644 site/src/xServices/groups/groupsXService.ts diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts new file mode 100644 index 0000000000000..7c3828048dd1a --- /dev/null +++ b/site/src/api/queries/groups.ts @@ -0,0 +1,8 @@ +import * as API from "api/api"; + +export const groups = (organizationId: string) => { + return { + queryKey: ["groups"], + queryFn: () => API.getGroups(organizationId), + }; +}; diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index d63e4dfb69ca9..1af0759b1b0ec 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -1,24 +1,28 @@ -import { useMachine } from "@xstate/react"; import { useFeatureVisibility } from "hooks/useFeatureVisibility"; import { useOrganizationId } from "hooks/useOrganizationId"; import { usePermissions } from "hooks/usePermissions"; -import { FC } from "react"; +import { FC, useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; -import { groupsMachine } from "xServices/groups/groupsXService"; import GroupsPageView from "./GroupsPageView"; +import { useQuery } from "@tanstack/react-query"; +import { groups } from "api/queries/groups"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { getErrorMessage } from "api/errors"; export const GroupsPage: FC = () => { const organizationId = useOrganizationId(); const { createGroup: canCreateGroup } = usePermissions(); const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const [state] = useMachine(groupsMachine, { - context: { - organizationId, - shouldFetchGroups: isTemplateRBACEnabled, - }, - }); - const { groups } = state.context; + const groupsQuery = useQuery(groups(organizationId)); + + useEffect(() => { + if (groupsQuery.error) { + displayError( + getErrorMessage(groupsQuery.error, "Error on loading groups."), + ); + } + }, [groupsQuery.error]); return ( <> @@ -27,7 +31,7 @@ export const GroupsPage: FC = () => { diff --git a/site/src/xServices/groups/groupsXService.ts b/site/src/xServices/groups/groupsXService.ts deleted file mode 100644 index 3552838724bae..0000000000000 --- a/site/src/xServices/groups/groupsXService.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getGroups } from "api/api"; -import { getErrorMessage } from "api/errors"; -import { Group } from "api/typesGenerated"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { assign, createMachine } from "xstate"; - -export const groupsMachine = createMachine( - { - id: "groupsMachine", - predictableActionArguments: true, - schema: { - context: {} as { - organizationId: string; - shouldFetchGroups: boolean; - groups?: Group[]; - }, - services: {} as { - loadGroups: { - data: Group[]; - }; - }, - }, - tsTypes: {} as import("./groupsXService.typegen").Typegen0, - initial: "loading", - states: { - loading: { - always: [{ target: "idle", cond: "cantFetchGroups" }], - invoke: { - src: "loadGroups", - onDone: { - actions: ["assignGroups"], - target: "idle", - }, - onError: { - target: "idle", - actions: ["displayLoadingGroupsError"], - }, - }, - }, - idle: {}, - }, - }, - { - guards: { - cantFetchGroups: ({ shouldFetchGroups }) => !shouldFetchGroups, - }, - services: { - loadGroups: ({ organizationId }) => getGroups(organizationId), - }, - actions: { - assignGroups: assign({ - groups: (_, { data }) => data, - }), - displayLoadingGroupsError: (_, { data }) => { - const message = getErrorMessage(data, "Error on loading groups."); - displayError(message); - }, - }, - }, -); From 6b93fcc795ea56f01441a47d9c6cad6c444a723a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 15 Sep 2023 13:14:02 +0000 Subject: [PATCH 2/7] Remove create group service --- site/src/api/queries/groups.ts | 15 +++++ site/src/pages/GroupsPage/CreateGroupPage.tsx | 30 ++++------ .../xServices/groups/createGroupXService.ts | 59 ------------------- 3 files changed, 26 insertions(+), 78 deletions(-) delete mode 100644 site/src/xServices/groups/createGroupXService.ts diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 7c3828048dd1a..67a6dab9adb2f 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -1,4 +1,6 @@ +import { QueryClient } from "@tanstack/react-query"; import * as API from "api/api"; +import { CreateGroupRequest } from "api/typesGenerated"; export const groups = (organizationId: string) => { return { @@ -6,3 +8,16 @@ export const groups = (organizationId: string) => { queryFn: () => API.getGroups(organizationId), }; }; + +export const createGroup = (queryClient: QueryClient) => { + return { + mutationFn: ({ + organizationId, + ...request + }: CreateGroupRequest & { organizationId: string }) => + API.createGroup(organizationId, request), + onSuccess: async () => { + await queryClient.invalidateQueries(["groups"]); + }, + }; +}; diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index 953f9cebb0b6c..b09c4f9176991 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -1,26 +1,17 @@ -import { useMachine } from "@xstate/react"; import { useOrganizationId } from "hooks/useOrganizationId"; import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { createGroupMachine } from "xServices/groups/createGroupXService"; import CreateGroupPageView from "./CreateGroupPageView"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createGroup } from "api/queries/groups"; export const CreateGroupPage: FC = () => { + const queryClient = useQueryClient(); const navigate = useNavigate(); const organizationId = useOrganizationId(); - const [createState, sendCreateEvent] = useMachine(createGroupMachine, { - context: { - organizationId, - }, - actions: { - onCreate: (_, { data }) => { - navigate(`/groups/${data.id}`); - }, - }, - }); - const { error } = createState.context; + const createGroupMutation = useMutation(createGroup(queryClient)); return ( <> @@ -28,14 +19,15 @@ export const CreateGroupPage: FC = () => { {pageTitle("Create Group")} { - sendCreateEvent({ - type: "CREATE", - data, + onSubmit={async (data) => { + const newGroup = await createGroupMutation.mutateAsync({ + organizationId, + ...data, }); + navigate(`/groups/${newGroup.id}`); }} - formErrors={error} - isLoading={createState.matches("creatingGroup")} + formErrors={createGroupMutation.error} + isLoading={createGroupMutation.isLoading} /> ); diff --git a/site/src/xServices/groups/createGroupXService.ts b/site/src/xServices/groups/createGroupXService.ts deleted file mode 100644 index 69dc6b3f7e7f3..0000000000000 --- a/site/src/xServices/groups/createGroupXService.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createGroup } from "api/api"; -import { CreateGroupRequest, Group } from "api/typesGenerated"; -import { createMachine, assign } from "xstate"; - -export const createGroupMachine = createMachine( - { - id: "createGroupMachine", - schema: { - context: {} as { - organizationId: string; - error?: unknown; - }, - services: {} as { - createGroup: { - data: Group; - }; - }, - events: {} as { - type: "CREATE"; - data: CreateGroupRequest; - }, - }, - tsTypes: {} as import("./createGroupXService.typegen").Typegen0, - initial: "idle", - states: { - idle: { - on: { - CREATE: { - target: "creatingGroup", - }, - }, - }, - creatingGroup: { - invoke: { - src: "createGroup", - onDone: { - target: "idle", - actions: ["onCreate"], - }, - onError: { - target: "idle", - actions: ["assignError"], - }, - }, - }, - }, - }, - { - services: { - createGroup: ({ organizationId }, { data }) => - createGroup(organizationId, data), - }, - actions: { - assignError: assign({ - error: (_, event) => event.data, - }), - }, - }, -); From 7466c8b7b841be7e984858e6f5d9121ca23cd071 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 15 Sep 2023 13:28:34 +0000 Subject: [PATCH 3/7] Remove edit group service --- site/src/api/queries/groups.ts | 29 ++++- .../pages/GroupsPage/SettingsGroupPage.tsx | 47 ++++---- .../src/xServices/groups/editGroupXService.ts | 100 ------------------ 3 files changed, 52 insertions(+), 124 deletions(-) delete mode 100644 site/src/xServices/groups/editGroupXService.ts diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 67a6dab9adb2f..cf6eb7dbd3b99 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -1,6 +1,10 @@ import { QueryClient } from "@tanstack/react-query"; import * as API from "api/api"; -import { CreateGroupRequest } from "api/typesGenerated"; +import { + CreateGroupRequest, + Group, + PatchGroupRequest, +} from "api/typesGenerated"; export const groups = (organizationId: string) => { return { @@ -9,6 +13,13 @@ export const groups = (organizationId: string) => { }; }; +export const group = (groupId: string) => { + return { + queryKey: ["group", groupId], + queryFn: () => API.getGroup(groupId), + }; +}; + export const createGroup = (queryClient: QueryClient) => { return { mutationFn: ({ @@ -21,3 +32,19 @@ export const createGroup = (queryClient: QueryClient) => { }, }; }; + +export const patchGroup = (queryClient: QueryClient) => { + return { + mutationFn: ({ + groupId, + ...request + }: PatchGroupRequest & { groupId: string }) => + API.patchGroup(groupId, request), + onSuccess: async (updatedGroup: Group) => { + await Promise.all([ + queryClient.invalidateQueries(["groups"]), + queryClient.invalidateQueries(["group", updatedGroup.id]), + ]); + }, + }; +}; diff --git a/site/src/pages/GroupsPage/SettingsGroupPage.tsx b/site/src/pages/GroupsPage/SettingsGroupPage.tsx index d61bc5beac9d1..28ecb49080b76 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPage.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPage.tsx @@ -1,33 +1,24 @@ -import { useMachine } from "@xstate/react"; import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { editGroupMachine } from "xServices/groups/editGroupXService"; import SettingsGroupPageView from "./SettingsGroupPageView"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { group, patchGroup } from "api/queries/groups"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { getErrorMessage } from "api/errors"; export const SettingsGroupPage: FC = () => { - const { groupId } = useParams(); - if (!groupId) { - throw new Error("Group ID not defined."); - } - + const { groupId } = useParams() as { groupId: string }; + const queryClient = useQueryClient(); + const groupQuery = useQuery(group(groupId)); + const patchGroupMutation = useMutation(patchGroup(queryClient)); const navigate = useNavigate(); const navigateToGroup = () => { navigate(`/groups/${groupId}`); }; - const [editState, sendEditEvent] = useMachine(editGroupMachine, { - context: { - groupId, - }, - actions: { - onUpdate: navigateToGroup, - }, - }); - const { error, group } = editState.context; - return ( <> @@ -36,13 +27,23 @@ export const SettingsGroupPage: FC = () => { { - sendEditEvent({ type: "UPDATE", data }); + onSubmit={async (data) => { + try { + await patchGroupMutation.mutateAsync({ + groupId, + ...data, + add_users: [], + remove_users: [], + }); + navigateToGroup(); + } catch (error) { + displayError(getErrorMessage(error, "Failed to update group")); + } }} - group={group} - formErrors={error} - isLoading={editState.matches("loading")} - isUpdating={editState.matches("updating")} + group={groupQuery.data} + formErrors={groupQuery.error} + isLoading={groupQuery.isLoading} + isUpdating={patchGroupMutation.isLoading} /> ); diff --git a/site/src/xServices/groups/editGroupXService.ts b/site/src/xServices/groups/editGroupXService.ts deleted file mode 100644 index 755a4863e52e4..0000000000000 --- a/site/src/xServices/groups/editGroupXService.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { getGroup, patchGroup } from "api/api"; -import { getErrorMessage } from "api/errors"; -import { Group } from "api/typesGenerated"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { assign, createMachine } from "xstate"; - -export const editGroupMachine = createMachine( - { - id: "editGroup", - schema: { - context: {} as { - groupId: string; - group?: Group; - error?: unknown; - }, - services: {} as { - loadGroup: { - data: Group; - }; - updateGroup: { - data: Group; - }; - }, - events: {} as { - type: "UPDATE"; - data: { - display_name: string; - name: string; - avatar_url: string; - quota_allowance: number; - }; - }, - }, - tsTypes: {} as import("./editGroupXService.typegen").Typegen0, - initial: "loading", - states: { - loading: { - invoke: { - src: "loadGroup", - onDone: { - actions: ["assignGroup"], - target: "idle", - }, - onError: { - actions: ["displayLoadGroupError"], - target: "idle", - }, - }, - }, - idle: { - on: { - UPDATE: { - target: "updating", - }, - }, - }, - updating: { - invoke: { - src: "updateGroup", - onDone: { - actions: ["onUpdate"], - }, - onError: { - target: "idle", - actions: ["assignError"], - }, - }, - }, - }, - }, - { - services: { - loadGroup: ({ groupId }) => getGroup(groupId), - - updateGroup: ({ group }, { data }) => { - if (!group) { - throw new Error("Group not defined."); - } - - return patchGroup(group.id, { - ...data, - add_users: [], - remove_users: [], - }); - }, - }, - actions: { - assignGroup: assign({ - group: (_, { data }) => data, - }), - displayLoadGroupError: (_, { data }) => { - const message = getErrorMessage(data, "Failed to the group."); - displayError(message); - }, - assignError: assign({ - error: (_, event) => event.data, - }), - }, - }, -); From 93c0f378ce8ad72f9b61494ae3b6c3afcb5e3a2e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 15 Sep 2023 16:23:49 +0000 Subject: [PATCH 4/7] Remove groups service --- site/src/api/api.ts | 18 ++ site/src/api/queries/groups.ts | 57 +++++ site/src/pages/GroupsPage/GroupPage.tsx | 216 +++++++++------- site/src/xServices/groups/groupXService.ts | 275 --------------------- 4 files changed, 199 insertions(+), 367 deletions(-) delete mode 100644 site/src/xServices/groups/groupXService.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 4c29b92017e00..d54bfce9894c7 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -954,6 +954,24 @@ export const patchGroup = async ( return response.data; }; +export const addMember = async (groupId: string, userId: string) => { + return patchGroup(groupId, { + name: "", + display_name: "", + add_users: [userId], + remove_users: [], + }); +}; + +export const removeMember = async (groupId: string, userId: string) => { + return patchGroup(groupId, { + name: "", + display_name: "", + add_users: [], + remove_users: [userId], + }); +}; + export const deleteGroup = async (groupId: string): Promise => { await axios.delete(`/api/v2/groups/${groupId}`); }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index cf6eb7dbd3b99..1368d4e251d7e 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -1,5 +1,6 @@ import { QueryClient } from "@tanstack/react-query"; import * as API from "api/api"; +import { checkAuthorization } from "api/api"; import { CreateGroupRequest, Group, @@ -20,6 +21,24 @@ export const group = (groupId: string) => { }; }; +export const groupPermissions = (groupId: string) => { + return { + queryKey: ["group", groupId, "permissions"], + queryFn: () => + checkAuthorization({ + checks: { + canUpdateGroup: { + object: { + resource_type: "group", + resource_id: groupId, + }, + action: "update", + }, + }, + }), + }; +}; + export const createGroup = (queryClient: QueryClient) => { return { mutationFn: ({ @@ -48,3 +67,41 @@ export const patchGroup = (queryClient: QueryClient) => { }, }; }; + +export const deleteGroup = (queryClient: QueryClient) => { + return { + mutationFn: API.deleteGroup, + onSuccess: async (_: void, groupId: string) => { + await Promise.all([ + queryClient.invalidateQueries(["groups"]), + queryClient.invalidateQueries(["group", groupId]), + ]); + }, + }; +}; + +export const addMember = (queryClient: QueryClient) => { + return { + mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => + API.addMember(groupId, userId), + onSuccess: async (updatedGroup: Group) => { + await Promise.all([ + queryClient.invalidateQueries(["groups"]), + queryClient.invalidateQueries(["group", updatedGroup.id]), + ]); + }, + }; +}; + +export const removeMember = (queryClient: QueryClient) => { + return { + mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => + API.removeMember(groupId, userId), + onSuccess: async (updatedGroup: Group) => { + await Promise.all([ + queryClient.invalidateQueries(["groups"]), + queryClient.invalidateQueries(["group", updatedGroup.id]), + ]); + }, + }; +}; diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index f0dc74ce800de..908347a78947f 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -9,7 +9,6 @@ import TableRow from "@mui/material/TableRow"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; import PersonAdd from "@mui/icons-material/PersonAdd"; import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; -import { useMachine } from "@xstate/react"; import { User } from "api/typesGenerated"; import { AvatarData } from "components/AvatarData/AvatarData"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; @@ -30,7 +29,6 @@ import { useState } from "react"; import { Helmet } from "react-helmet-async"; import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { groupMachine } from "xServices/groups/groupXService"; import { Maybe } from "components/Conditionals/Maybe"; import { makeStyles } from "@mui/styles"; import { @@ -39,76 +37,38 @@ import { } from "components/TableToolbar/TableToolbar"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { isEveryoneGroup } from "utils/groups"; - -const AddGroupMember: React.FC<{ - isLoading: boolean; - onSubmit: (user: User, reset: () => void) => void; -}> = ({ isLoading, onSubmit }) => { - const [selectedUser, setSelectedUser] = useState(null); - const styles = useStyles(); - - const resetValues = () => { - setSelectedUser(null); - }; - - return ( -
{ - e.preventDefault(); - - if (selectedUser) { - onSubmit(selectedUser, resetValues); - } - }} - > - - { - setSelectedUser(newValue); - }} - /> - - } - loading={isLoading} - > - Add user - - -
- ); -}; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + addMember, + deleteGroup, + group, + groupPermissions, + removeMember, +} from "api/queries/groups"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { getErrorMessage } from "api/errors"; export const GroupPage: React.FC = () => { - const { groupId } = useParams(); - if (!groupId) { - throw new Error("groupId is not defined."); - } - + const { groupId } = useParams() as { groupId: string }; + const queryClient = useQueryClient(); const navigate = useNavigate(); - const [state, send] = useMachine(groupMachine, { - context: { - groupId, - }, - actions: { - redirectToGroups: () => { - navigate("/groups"); - }, - }, - }); - const { group, permissions } = state.context; - const isLoading = group === undefined || permissions === undefined; + const groupQuery = useQuery(group(groupId)); + const groupData = groupQuery.data; + const { data: permissions } = useQuery(groupPermissions(groupId)); + const addMemberMutation = useMutation(addMember(queryClient)); + const removeMemberMutation = useMutation(removeMember(queryClient)); + const deleteGroupMutation = useMutation(deleteGroup(queryClient)); + const [isDeletingGroup, setIsDeletinGroup] = useState(false); + const isLoading = !groupData || !permissions; const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; return ( <> - {pageTitle((group?.display_name || group?.name) ?? "Loading...")} + {pageTitle( + (groupData?.display_name || groupData?.name) ?? "Loading...", + )} @@ -125,9 +85,9 @@ export const GroupPage: React.FC = () => {