improvement: move user groups to own tab and add pagination/search/sort to groups tables

This commit is contained in:
Scott Wilson
2024-11-27 20:35:15 -08:00
parent 588b3c77f9
commit 18a2547b24
9 changed files with 381 additions and 245 deletions
@@ -7,7 +7,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { isTabSection, TabSections } from "@app/views/Org/Types"; 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( export const MembersPage = withPermission(
() => { () => {
@@ -25,9 +25,9 @@ export const MembersPage = withPermission(
const updateSelectedTab = (tab: string) => { const updateSelectedTab = (tab: string) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...router.query, selectedTab: tab }, query: { ...router.query, selectedTab: tab }
}); });
} };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -36,16 +36,20 @@ export const MembersPage = withPermission(
<Tabs value={activeTab} onValueChange={updateSelectedTab}> <Tabs value={activeTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={TabSections.Member}>Users</Tab> <Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Groups}>Groups</Tab>
<Tab value={TabSections.Identities}> <Tab value={TabSections.Identities}>
<div className="flex items-center"> <div className="flex items-center">
<p>Machine Identities</p> <p>Machine Identities</p>
</div> </div>
</Tab> </Tab>
<Tab value={TabSections.Roles}>Organization Roles</Tab> <Tab value={TabSections.Roles}>Organization Roles</Tab>
</TabList> </TabList>
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<OrgMembersTab /> <OrgMembersTab />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Groups}>
<OrgGroupsTab />
</TabPanel>
<TabPanel value={TabSections.Identities}> <TabPanel value={TabSections.Identities}>
<OrgIdentityTab /> <OrgIdentityTab />
</TabPanel> </TabPanel>
@@ -1,9 +1,10 @@
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faEllipsis, faEllipsis,
faMagnifyingGlass, faMagnifyingGlass,
faSearch,
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -19,6 +20,7 @@ import {
EmptyState, EmptyState,
IconButton, IconButton,
Input, Input,
Pagination,
Select, Select,
SelectItem, SelectItem,
Table, Table,
@@ -31,7 +33,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; 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 { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -59,14 +61,10 @@ enum GroupsOrderBy {
} }
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const [debouncedSearch] = useDebounce(searchGroupsFilter.trim());
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId); const { isLoading, data: groups = [] } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup(); const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const [orderBy, setOrderBy] = useState(GroupsOrderBy.Name);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const { data: roles } = useGetOrgRoles(orgId); 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>(GroupsOrderBy.Name, { initPerPage: 20 });
const filteredGroups = useMemo(() => { const filteredGroups = useMemo(() => {
const filtered = debouncedSearch const filtered = search
? groups?.filter( ? groups?.filter(
({ name, slug }) => ({ name, slug }) =>
name.toLowerCase().includes(debouncedSearch.toLowerCase()) || name.toLowerCase().includes(search.toLowerCase()) ||
slug.toLowerCase().includes(debouncedSearch.toLowerCase()) slug.toLowerCase().includes(search.toLowerCase())
) )
: groups; : groups;
@@ -113,13 +126,11 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
}); });
return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse(); return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse();
}, [debouncedSearch, groups, orderBy, orderDirection]); }, [search, groups, orderBy, orderDirection]);
const handleSort = (column: GroupsOrderBy) => { const handleSort = (column: GroupsOrderBy) => {
if (column === orderBy) { if (column === orderBy) {
setOrderDirection((prev) => toggleOrderDirection();
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return; return;
} }
@@ -127,11 +138,17 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
setOrderDirection(OrderByDirection.ASC); setOrderDirection(OrderByDirection.ASC);
}; };
useResetPageHelper({
totalCount: filteredGroups.length,
offset,
setPage
});
return ( return (
<div> <div>
<Input <Input
value={searchGroupsFilter} value={search}
onChange={(e) => setSearchGroupsFilter(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..." placeholder="Search groups..."
/> />
@@ -202,143 +219,160 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />} {isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading && {!isLoading &&
filteredGroups?.map(({ id, name, slug, role, customRole }) => { filteredGroups
return ( .slice(offset, perPage * page)
<Tr className="h-10" key={`org-group-${id}`}> .map(({ id, name, slug, role, customRole }) => {
<Td>{name}</Td> return (
<Td>{slug}</Td> <Tr className="h-10" key={`org-group-${id}`}>
<Td> <Td>{name}</Td>
<OrgPermissionCan <Td>{slug}</Td>
I={OrgPermissionActions.Edit} <Td>
a={OrgPermissionSubjects.Groups} <OrgPermissionCan
> I={OrgPermissionActions.Edit}
{(isAllowed) => { a={OrgPermissionSubjects.Groups}
return ( >
<Select {(isAllowed) => {
value={role === "custom" ? (customRole?.slug as string) : role} return (
isDisabled={!isAllowed} <Select
className="w-48 bg-mineshaft-600" value={role === "custom" ? (customRole?.slug as string) : role}
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" isDisabled={!isAllowed}
onValueChange={(selectedRole) => className="w-48 bg-mineshaft-600"
handleChangeRole({ dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
id, onValueChange={(selectedRole) =>
role: selectedRole handleChangeRole({
}) id,
} role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
createNotification({
text: "Copied group ID to clipboard",
type: "info"
});
navigator.clipboard.writeText(id);
}}
> >
{(roles || []).map(({ slug: roleSlug, name: roleName }) => ( Copy Group ID
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}> </DropdownMenuItem>
{roleName} <OrgPermissionCan
</SelectItem> I={OrgPermissionActions.Edit}
))} a={OrgPermissionSubjects.Identity}
</Select> >
); {(isAllowed) => (
}} <DropdownMenuItem
</OrgPermissionCan> className={twMerge(
</Td> !isAllowed &&
<Td> "pointer-events-none cursor-not-allowed opacity-50"
<DropdownMenu> )}
<DropdownMenuTrigger asChild className="rounded-lg"> onClick={(e) => {
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> e.stopPropagation();
<FontAwesomeIcon size="sm" icon={faEllipsis} /> handlePopUpOpen("groupMembers", {
</div> groupId: id,
</DropdownMenuTrigger> slug
<DropdownMenuContent align="start" className="p-1"> });
<DropdownMenuItem }}
onClick={(e) => { disabled={!isAllowed}
e.stopPropagation(); >
createNotification({ Manage Users
text: "Copied group ID to clipboard", </DropdownMenuItem>
type: "info" )}
}); </OrgPermissionCan>
navigator.clipboard.writeText(id); <OrgPermissionCan
}} I={OrgPermissionActions.Edit}
> a={OrgPermissionSubjects.Identity}
Copy Group ID >
</DropdownMenuItem> {(isAllowed) => (
<OrgPermissionCan <DropdownMenuItem
I={OrgPermissionActions.Edit} className={twMerge(
a={OrgPermissionSubjects.Identity} !isAllowed &&
> "pointer-events-none cursor-not-allowed opacity-50"
{(isAllowed) => ( )}
<DropdownMenuItem onClick={(e) => {
className={twMerge( e.stopPropagation();
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" handlePopUpOpen("group", {
)} groupId: id,
onClick={(e) => { name,
e.stopPropagation(); slug,
handlePopUpOpen("groupMembers", { role,
groupId: id, customRole
slug });
}); }}
}} disabled={!isAllowed}
disabled={!isAllowed} >
> Edit Group
Manage Users </DropdownMenuItem>
</DropdownMenuItem> )}
)} </OrgPermissionCan>
</OrgPermissionCan> <OrgPermissionCan
<OrgPermissionCan I={OrgPermissionActions.Delete}
I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}
a={OrgPermissionSubjects.Identity} >
> {(isAllowed) => (
{(isAllowed) => ( <DropdownMenuItem
<DropdownMenuItem className={twMerge(
className={twMerge( isAllowed
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" ? "hover:!bg-red-500 hover:!text-white"
)} : "pointer-events-none cursor-not-allowed opacity-50"
onClick={(e) => { )}
e.stopPropagation(); onClick={(e) => {
handlePopUpOpen("group", { e.stopPropagation();
groupId: id, handlePopUpOpen("deleteGroup", {
name, groupId: id,
slug, name
role, });
customRole }}
}); disabled={!isAllowed}
}} >
disabled={!isAllowed} Delete Group
> </DropdownMenuItem>
Edit Group )}
</DropdownMenuItem> </OrgPermissionCan>
)} </DropdownMenuContent>
</OrgPermissionCan> </DropdownMenu>
<OrgPermissionCan </Td>
I={OrgPermissionActions.Delete} </Tr>
a={OrgPermissionSubjects.Groups} );
> })}
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
groupId: id,
name
});
}}
disabled={!isAllowed}
>
Delete Group
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody> </TBody>
</Table> </Table>
{filteredGroups?.length === 0 && ( {Boolean(filteredGroups.length) && (
<Pagination
count={filteredGroups.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{!isLoading && !filteredGroups?.length && (
<EmptyState <EmptyState
title={groups?.length === 0 ? "No groups found" : "No groups match search"} title={
icon={faUsers} groups.length
? "No organization groups match search..."
: "No organization groups found"
}
icon={groups.length ? faSearch : faUsers}
/> />
)} )}
</TableContainer> </TableContainer>
@@ -1,6 +1,5 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { OrgGroupsSection } from "../OrgGroupsTab/components";
import { OrgMembersSection } from "./components"; import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => { export const OrgMembersTab = () => {
@@ -13,7 +12,6 @@ export const OrgMembersTab = () => {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<OrgMembersSection /> <OrgMembersSection />
<OrgGroupsSection />
</motion.div> </motion.div>
); );
}; };
@@ -171,7 +171,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setOrderBy, setOrderBy,
setOrderDirection, setOrderDirection,
toggleOrderDirection toggleOrderDirection
} = usePagination<OrgMembersOrderBy>(OrgMembersOrderBy.Name); } = usePagination<OrgMembersOrderBy>(OrgMembersOrderBy.Name, { initPerPage: 20 });
const filteredUsers = useMemo( const filteredUsers = useMemo(
() => () =>
+2 -1
View File
@@ -1,9 +1,10 @@
export enum TabSections { export enum TabSections {
Member = "members", Member = "members",
Groups = "groups",
Roles = "roles", Roles = "roles",
Identities = "identities" Identities = "identities"
} }
export const isTabSection = (value: string): value is TabSections => { export const isTabSection = (value: string): value is TabSections => {
return (Object.values(TabSections) as string[]).includes(value); return (Object.values(TabSections) as string[]).includes(value);
} };
@@ -6,9 +6,14 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { isTabSection,TabSections } from "../Types"; import { isTabSection, TabSections } from "../Types";
import { IdentityTab, MembersTab,ProjectRoleListTab, ServiceTokenTab } from "./components"; import {
GroupsTab,
IdentityTab,
MembersTab,
ProjectRoleListTab,
ServiceTokenTab
} from "./components";
export const MembersPage = withProjectPermission( export const MembersPage = withProjectPermission(
() => { () => {
@@ -26,9 +31,9 @@ export const MembersPage = withProjectPermission(
const updateSelectedTab = (tab: string) => { const updateSelectedTab = (tab: string) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...router.query, selectedTab: tab }, query: { ...router.query, selectedTab: tab }
}); });
} };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -37,6 +42,7 @@ export const MembersPage = withProjectPermission(
<Tabs value={activeTab} onValueChange={updateSelectedTab}> <Tabs value={activeTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={TabSections.Member}>Users</Tab> <Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Groups}>Groups</Tab>
<Tab value={TabSections.Identities}> <Tab value={TabSections.Identities}>
<div className="flex items-center"> <div className="flex items-center">
<p>Machine Identities</p> <p>Machine Identities</p>
@@ -48,6 +54,9 @@ export const MembersPage = withProjectPermission(
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<MembersTab /> <MembersTab />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Groups}>
<GroupsTab />
</TabPanel>
<TabPanel value={TabSections.Identities}> <TabPanel value={TabSections.Identities}>
<IdentityTab /> <IdentityTab />
</TabPanel> </TabPanel>
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -6,6 +14,8 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
Input,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -17,7 +27,9 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useListWorkspaceGroups } from "@app/hooks/api"; import { useListWorkspaceGroups } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { GroupRoles } from "./GroupRoles"; import { GroupRoles } from "./GroupRoles";
@@ -32,76 +44,159 @@ type Props = {
) => void; ) => void;
}; };
enum GroupsOrderBy {
Name = "name"
}
export const GroupTable = ({ handlePopUpOpen }: Props) => { export const GroupTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace(); 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 ( return (
<TableContainer> <div>
<Table> <Input
<THead> value={search}
<Tr> onChange={(e) => setSearch(e.target.value)}
<Th>Name</Th> leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
<Th>Role</Th> placeholder="Search members..."
<Th>Added on</Th> />
<Th className="w-5" /> <TableContainer className="mt-4">
</Tr> <Table>
</THead> <THead>
<TBody> <Tr>
{isLoading && <TableSkeleton columns={4} innerKey="project-groups" />} <Th className="w-1/3">
{!isLoading && <div className="flex items-center">
data && Name
data.length > 0 && <IconButton
data.map(({ group: { id, name }, roles, createdAt }) => { variant="plain"
return ( className="ml-2"
<Tr className="group h-10" key={`st-v3-${id}`}> ariaLabel="sort"
<Td>{name}</Td> onClick={toggleOrderDirection}
<Td> >
<ProjectPermissionCan <FontAwesomeIcon
I={ProjectPermissionActions.Edit} icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
a={ProjectPermissionSub.Groups} />
> </IconButton>
{(isAllowed) => ( </div>
<GroupRoles roles={roles} disableEdit={!isAllowed} groupId={id} /> </Th>
)} <Th>Role</Th>
</ProjectPermissionCan> <Th>Added on</Th>
</Td> <Th className="w-5" />
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> </Tr>
<Td className="flex justify-end"> </THead>
<ProjectPermissionCan <TBody>
I={ProjectPermissionActions.Delete} {isLoading && <TableSkeleton columns={4} innerKey="project-groups" />}
a={ProjectPermissionSub.Groups} {!isLoading &&
> filteredGroupMemberships &&
{(isAllowed) => ( filteredGroupMemberships.length > 0 &&
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100"> filteredGroupMemberships
<Tooltip content="Remove"> .slice(offset, perPage * page)
<IconButton .map(({ group: { id, name }, roles, createdAt }) => {
onClick={() => { return (
handlePopUpOpen("deleteGroup", { <Tr className="group h-10" key={`st-v3-${id}`}>
id, <Td>{name}</Td>
name <Td>
}); <ProjectPermissionCan
}} I={ProjectPermissionActions.Edit}
colorSchema="danger" a={ProjectPermissionSub.Groups}
variant="plain" >
ariaLabel="update" {(isAllowed) => (
className="ml-4" <GroupRoles roles={roles} disableEdit={!isAllowed} groupId={id} />
isDisabled={!isAllowed} )}
> </ProjectPermissionCan>
<FontAwesomeIcon icon={faTrash} /> </Td>
</IconButton> <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
</Tooltip> <Td className="flex justify-end">
</div> <ProjectPermissionCan
)} I={ProjectPermissionActions.Delete}
</ProjectPermissionCan> a={ProjectPermissionSub.Groups}
</Td> >
</Tr> {(isAllowed) => (
); <div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
})} <Tooltip content="Remove">
</TBody> <IconButton
</Table> onClick={() => {
{!isLoading && data?.length === 0 && ( handlePopUpOpen("deleteGroup", {
<EmptyState title="No groups have been added to this project" icon={faServer} /> id,
)} name
</TableContainer> });
}}
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{Boolean(filteredGroupMemberships.length) && (
<Pagination
count={filteredGroupMemberships.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{!isLoading && !filteredGroupMemberships?.length && (
<EmptyState
title={
groupMemberships.length
? "No project groups match search..."
: "No project groups found"
}
icon={groupMemberships.length ? faSearch : faUsers}
/>
)}
</TableContainer>
</div>
); );
}; };
@@ -1,12 +1,8 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useWorkspace } from "@app/context";
import { GroupsSection } from "../GroupsTab/components";
import { MembersSection } from "./components"; import { MembersSection } from "./components";
export const MembersTab = () => { export const MembersTab = () => {
const { currentWorkspace } = useWorkspace();
return ( return (
<motion.div <motion.div
key="panel-project-members" key="panel-project-members"
@@ -16,7 +12,6 @@ export const MembersTab = () => {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<MembersSection /> <MembersSection />
{currentWorkspace?.version && currentWorkspace.version > 1 && <GroupsSection />}
</motion.div> </motion.div>
); );
}; };
@@ -86,7 +86,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
setOrderBy, setOrderBy,
setOrderDirection, setOrderDirection,
toggleOrderDirection toggleOrderDirection
} = usePagination<MembersOrderBy>(MembersOrderBy.Name); } = usePagination<MembersOrderBy>(MembersOrderBy.Name, { initPerPage: 20 });
const { data: members = [], isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); const { data: members = [], isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);