From 18a2547b245508f7f53312d94762199604ea5e02 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 27 Nov 2024 20:35:15 -0800 Subject: [PATCH] improvement: move user groups to own tab and add pagination/search/sort to groups tables --- .../src/views/Org/MembersPage/MembersPage.tsx | 12 +- .../OrgGroupsSection/OrgGroupsTable.tsx | 332 ++++++++++-------- .../OrgMembersTab/OrgMembersTab.tsx | 2 - .../OrgMembersSection/OrgMembersTable.tsx | 2 +- frontend/src/views/Org/Types/TabSections.ts | 19 +- .../views/Project/MembersPage/MembersPage.tsx | 19 +- .../components/GroupsSection/GroupsTable.tsx | 233 ++++++++---- .../components/MembersTab/MembersTab.tsx | 5 - .../MembersTab/components/MembersTable.tsx | 2 +- 9 files changed, 381 insertions(+), 245 deletions(-) diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 786e672ac..eb006fef9 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -7,7 +7,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; import { isTabSection, TabSections } from "@app/views/Org/Types"; -import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; +import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; export const MembersPage = withPermission( () => { @@ -25,9 +25,9 @@ export const MembersPage = withPermission( const updateSelectedTab = (tab: string) => { router.push({ pathname: router.pathname, - query: { ...router.query, selectedTab: tab }, + query: { ...router.query, selectedTab: tab } }); - } + }; return (
@@ -36,16 +36,20 @@ export const MembersPage = withPermission( Users + Groups

Machine Identities

- Organization Roles + Organization Roles
+ + + diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 21582805c..ba1b11f22 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -1,9 +1,10 @@ -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { faArrowDown, faArrowUp, faEllipsis, faMagnifyingGlass, + faSearch, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -19,6 +20,7 @@ import { EmptyState, IconButton, Input, + Pagination, Select, SelectItem, Table, @@ -31,7 +33,7 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; -import { useDebounce } from "@app/hooks"; +import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -59,14 +61,10 @@ enum GroupsOrderBy { } export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { - const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); - const [debouncedSearch] = useDebounce(searchGroupsFilter.trim()); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - const { isLoading, data: groups } = useGetOrganizationGroups(orgId); + const { isLoading, data: groups = [] } = useGetOrganizationGroups(orgId); const { mutateAsync: updateMutateAsync } = useUpdateGroup(); - const [orderBy, setOrderBy] = useState(GroupsOrderBy.Name); - const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC); const { data: roles } = useGetOrgRoles(orgId); @@ -90,12 +88,27 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { } }; + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + orderBy, + setOrderBy, + setOrderDirection, + toggleOrderDirection + } = usePagination(GroupsOrderBy.Name, { initPerPage: 20 }); + const filteredGroups = useMemo(() => { - const filtered = debouncedSearch + const filtered = search ? groups?.filter( ({ name, slug }) => - name.toLowerCase().includes(debouncedSearch.toLowerCase()) || - slug.toLowerCase().includes(debouncedSearch.toLowerCase()) + name.toLowerCase().includes(search.toLowerCase()) || + slug.toLowerCase().includes(search.toLowerCase()) ) : groups; @@ -113,13 +126,11 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { }); return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse(); - }, [debouncedSearch, groups, orderBy, orderDirection]); + }, [search, groups, orderBy, orderDirection]); const handleSort = (column: GroupsOrderBy) => { if (column === orderBy) { - setOrderDirection((prev) => - prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC - ); + toggleOrderDirection(); return; } @@ -127,11 +138,17 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { setOrderDirection(OrderByDirection.ASC); }; + useResetPageHelper({ + totalCount: filteredGroups.length, + offset, + setPage + }); + return (
setSearchGroupsFilter(e.target.value)} + value={search} + onChange={(e) => setSearch(e.target.value)} leftIcon={} placeholder="Search groups..." /> @@ -202,143 +219,160 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { {isLoading && } {!isLoading && - filteredGroups?.map(({ id, name, slug, role, customRole }) => { - return ( - - {name} - {slug} - - - {(isAllowed) => { - return ( - + handleChangeRole({ + id, + role: selectedRole + }) + } + > + {(roles || []).map(({ slug: roleSlug, name: roleName }) => ( + + {roleName} + + ))} + + ); + }} + + + + + +
+ +
+
+ + { + e.stopPropagation(); + createNotification({ + text: "Copied group ID to clipboard", + type: "info" + }); + navigator.clipboard.writeText(id); + }} > - {(roles || []).map(({ slug: roleSlug, name: roleName }) => ( - - {roleName} - - ))} - - ); - }} - - - - - -
- -
-
- - { - e.stopPropagation(); - createNotification({ - text: "Copied group ID to clipboard", - type: "info" - }); - navigator.clipboard.writeText(id); - }} - > - Copy Group ID - - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("groupMembers", { - groupId: id, - slug - }); - }} - disabled={!isAllowed} - > - Manage Users - - )} - - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("group", { - groupId: id, - name, - slug, - role, - customRole - }); - }} - disabled={!isAllowed} - > - Edit Group - - )} - - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("deleteGroup", { - groupId: id, - name - }); - }} - disabled={!isAllowed} - > - Delete Group - - )} - - -
- - - ); - })} + Copy Group ID +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("groupMembers", { + groupId: id, + slug + }); + }} + disabled={!isAllowed} + > + Manage Users + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("group", { + groupId: id, + name, + slug, + role, + customRole + }); + }} + disabled={!isAllowed} + > + Edit Group + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteGroup", { + groupId: id, + name + }); + }} + disabled={!isAllowed} + > + Delete Group + + )} + +
+
+ + + ); + })} - {filteredGroups?.length === 0 && ( + {Boolean(filteredGroups.length) && ( + + )} + {!isLoading && !filteredGroups?.length && ( )} diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx index 36230c971..1afb9bfbd 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx @@ -1,6 +1,5 @@ import { motion } from "framer-motion"; -import { OrgGroupsSection } from "../OrgGroupsTab/components"; import { OrgMembersSection } from "./components"; export const OrgMembersTab = () => { @@ -13,7 +12,6 @@ export const OrgMembersTab = () => { exit={{ opacity: 0, translateX: 30 }} > - ); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 2533b54d0..942ad549d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -171,7 +171,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro setOrderBy, setOrderDirection, toggleOrderDirection - } = usePagination(OrgMembersOrderBy.Name); + } = usePagination(OrgMembersOrderBy.Name, { initPerPage: 20 }); const filteredUsers = useMemo( () => diff --git a/frontend/src/views/Org/Types/TabSections.ts b/frontend/src/views/Org/Types/TabSections.ts index 945370d45..0ddfc4452 100644 --- a/frontend/src/views/Org/Types/TabSections.ts +++ b/frontend/src/views/Org/Types/TabSections.ts @@ -1,9 +1,10 @@ -export enum TabSections { - Member = "members", - Roles = "roles", - Identities = "identities" -} - -export const isTabSection = (value: string): value is TabSections => { - return (Object.values(TabSections) as string[]).includes(value); -} +export enum TabSections { + Member = "members", + Groups = "groups", + Roles = "roles", + Identities = "identities" +} + +export const isTabSection = (value: string): value is TabSections => { + return (Object.values(TabSections) as string[]).includes(value); +}; diff --git a/frontend/src/views/Project/MembersPage/MembersPage.tsx b/frontend/src/views/Project/MembersPage/MembersPage.tsx index f3af1454e..a05787286 100644 --- a/frontend/src/views/Project/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Project/MembersPage/MembersPage.tsx @@ -6,9 +6,14 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { isTabSection,TabSections } from "../Types"; -import { IdentityTab, MembersTab,ProjectRoleListTab, ServiceTokenTab } from "./components"; - +import { isTabSection, TabSections } from "../Types"; +import { + GroupsTab, + IdentityTab, + MembersTab, + ProjectRoleListTab, + ServiceTokenTab +} from "./components"; export const MembersPage = withProjectPermission( () => { @@ -26,9 +31,9 @@ export const MembersPage = withProjectPermission( const updateSelectedTab = (tab: string) => { router.push({ pathname: router.pathname, - query: { ...router.query, selectedTab: tab }, + query: { ...router.query, selectedTab: tab } }); - } + }; return (
@@ -37,6 +42,7 @@ export const MembersPage = withProjectPermission( Users + Groups

Machine Identities

@@ -48,6 +54,9 @@ export const MembersPage = withProjectPermission( + + + diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 4bebd7637..f1d9ea1a3 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -1,4 +1,12 @@ -import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faMagnifyingGlass, + faSearch, + faTrash, + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -6,6 +14,8 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, + Input, + Pagination, Table, TableContainer, TableSkeleton, @@ -17,7 +27,9 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; import { useListWorkspaceGroups } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { GroupRoles } from "./GroupRoles"; @@ -32,76 +44,159 @@ type Props = { ) => void; }; +enum GroupsOrderBy { + Name = "name" +} + export const GroupTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.id || ""); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + orderBy, + toggleOrderDirection + } = usePagination(GroupsOrderBy.Name, { initPerPage: 20 }); + + const { data: groupMemberships = [], isLoading } = useListWorkspaceGroups( + currentWorkspace?.id || "" + ); + + const filteredGroupMemberships = useMemo(() => { + const filtered = search + ? groupMemberships?.filter( + ({ group: { name, slug } }) => + name.toLowerCase().includes(search.toLowerCase()) || + slug.toLowerCase().includes(search.toLowerCase()) + ) + : groupMemberships; + + const ordered = filtered?.sort((a, b) => + a.group.name.toLowerCase().localeCompare(b.group.name.toLowerCase()) + ); + + return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse(); + }, [search, groupMemberships, orderBy, orderDirection]); + + useResetPageHelper({ + totalCount: filteredGroupMemberships.length, + offset, + setPage + }); + return ( - - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ group: { id, name }, roles, createdAt }) => { - return ( - - - - - - - ); - })} - -
NameRoleAdded on -
{name} - - {(isAllowed) => ( - - )} - - {format(new Date(createdAt), "yyyy-MM-dd")} - - {(isAllowed) => ( -
- - { - handlePopUpOpen("deleteGroup", { - id, - name - }); - }} - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - -
- )} -
-
- {!isLoading && data?.length === 0 && ( - - )} -
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + filteredGroupMemberships && + filteredGroupMemberships.length > 0 && + filteredGroupMemberships + .slice(offset, perPage * page) + .map(({ group: { id, name }, roles, createdAt }) => { + return ( + + + + + + + ); + })} + +
+
+ Name + + + +
+
RoleAdded on +
{name} + + {(isAllowed) => ( + + )} + + {format(new Date(createdAt), "yyyy-MM-dd")} + + {(isAllowed) => ( +
+ + { + handlePopUpOpen("deleteGroup", { + id, + name + }); + }} + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + +
+ )} +
+
+ {Boolean(filteredGroupMemberships.length) && ( + + )} + {!isLoading && !filteredGroupMemberships?.length && ( + + )} +
+
); }; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx index 25ffc38da..6b5660c83 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx @@ -1,12 +1,8 @@ import { motion } from "framer-motion"; -import { useWorkspace } from "@app/context"; - -import { GroupsSection } from "../GroupsTab/components"; import { MembersSection } from "./components"; export const MembersTab = () => { - const { currentWorkspace } = useWorkspace(); return ( { exit={{ opacity: 0, translateX: 30 }} > - {currentWorkspace?.version && currentWorkspace.version > 1 && } ); }; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx index d81d4005c..d90468620 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx @@ -86,7 +86,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { setOrderBy, setOrderDirection, toggleOrderDirection - } = usePagination(MembersOrderBy.Name); + } = usePagination(MembersOrderBy.Name, { initPerPage: 20 }); const { data: members = [], isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);