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 (
-
-
-
-
- | Name |
- Role |
- Added on |
- |
-
-
-
- {isLoading && }
- {!isLoading &&
- data &&
- data.length > 0 &&
- data.map(({ group: { id, name }, roles, createdAt }) => {
- return (
-
- | {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..."
+ />
+
+
+
+
+ |
+
+ Name
+
+
+
+
+ |
+ Role |
+ Added on |
+ |
+
+
+
+ {isLoading && }
+ {!isLoading &&
+ filteredGroupMemberships &&
+ filteredGroupMemberships.length > 0 &&
+ filteredGroupMemberships
+ .slice(offset, perPage * page)
+ .map(({ group: { id, name }, roles, createdAt }) => {
+ return (
+
+ | {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/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
index 389f71868..fd0b13172 100644
--- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
+++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
@@ -121,9 +121,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
});
return (orgUsers || [])
.filter(({ user: u }) => !wsUserUsernames.has(u.username))
- .map((member) => ({
- value: member.id,
- label: `${member.user.firstName} ${member.user.lastName}`
+ .map(({ id, inviteEmail, user: { firstName, lastName, email } }) => ({
+ value: id,
+ label:
+ firstName && lastName
+ ? `${firstName} ${lastName}`
+ : firstName || lastName || email || inviteEmail
}));
}, [orgUsers, members]);
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 591d2663b..17d1d523b 100644
--- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx
+++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx
@@ -1,9 +1,12 @@
-import { useMemo, useState } from "react";
+import { useMemo } from "react";
import { useRouter } from "next/router";
import {
+ faArrowDown,
+ faArrowUp,
faClock,
faEllipsisV,
faMagnifyingGlass,
+ faSearch,
faTrash,
faUsers
} from "@fortawesome/free-solid-svg-icons";
@@ -18,6 +21,7 @@ import {
HoverCardTrigger,
IconButton,
Input,
+ Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -35,7 +39,9 @@ import {
useUser,
useWorkspace
} from "@app/context";
+import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetWorkspaceUsers } from "@app/hooks/api";
+import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -54,9 +60,12 @@ type Props = {
) => void;
};
-export const MembersTable = ({ handlePopUpOpen }: Props) => {
- const [searchMemberFilter, setSearchMemberFilter] = useState("");
+enum MembersOrderBy {
+ Name = "firstName",
+ Email = "email"
+}
+export const MembersTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const router = useRouter();
@@ -64,26 +73,80 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
const userId = user?.id || "";
const workspaceId = currentWorkspace?.id || "";
- const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
+ const {
+ search,
+ setSearch,
+ setPage,
+ page,
+ perPage,
+ setPerPage,
+ offset,
+ orderDirection,
+ orderBy,
+ setOrderBy,
+ setOrderDirection,
+ toggleOrderDirection
+ } = usePagination
(MembersOrderBy.Name, { initPerPage: 20 });
- const filterdUsers = useMemo(
+ const { data: members = [], isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
+
+ const filteredUsers = useMemo(
() =>
- members?.filter(
- ({ user: u, inviteEmail }) =>
- u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
- u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
- u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
- u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
- inviteEmail?.includes(searchMemberFilter.toLowerCase())
- ),
- [members, searchMemberFilter]
+ members
+ ?.filter(
+ ({ user: u, inviteEmail }) =>
+ u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
+ u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
+ u?.username?.toLowerCase().includes(search.toLowerCase()) ||
+ u?.email?.toLowerCase().includes(search.toLowerCase()) ||
+ inviteEmail?.toLowerCase().includes(search.toLowerCase())
+ )
+ .sort((a, b) => {
+ const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
+
+ let valueOne: string;
+ let valueTwo: string;
+
+ switch (orderBy) {
+ case MembersOrderBy.Email:
+ valueOne = memberOne.user.email || memberOne.inviteEmail;
+ valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
+ break;
+ case MembersOrderBy.Name:
+ default:
+ valueOne = memberOne.user.firstName;
+ valueTwo = memberTwo.user.firstName;
+ }
+
+ if (!valueOne) return 1;
+ if (!valueTwo) return -1;
+
+ return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
+ }),
+ [members, search, orderDirection, orderBy]
);
+ useResetPageHelper({
+ totalCount: filteredUsers.length,
+ offset,
+ setPage
+ });
+
+ const handleSort = (column: MembersOrderBy) => {
+ if (column === orderBy) {
+ toggleOrderDirection();
+ return;
+ }
+
+ setOrderBy(column);
+ setOrderDirection(OrderByDirection.ASC);
+ };
+
return (
setSearchMemberFilter(e.target.value)}
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
leftIcon={
}
placeholder="Search members..."
/>
@@ -91,8 +154,44 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
- | Name |
- Username |
+
+
+ Name
+ handleSort(MembersOrderBy.Name)}
+ >
+
+
+
+ |
+
+
+ Email
+ handleSort(MembersOrderBy.Email)}
+ >
+
+
+
+ |
Role |
|
@@ -100,7 +199,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
{isMembersLoading && }
{!isMembersLoading &&
- filterdUsers?.map((projectMember) => {
+ filteredUsers.slice(offset, perPage * page).map((projectMember) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
const email = u?.email || inviteEmail;
@@ -239,8 +338,22 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
})}
- {!isMembersLoading && filterdUsers?.length === 0 && (
-
+ {Boolean(filteredUsers.length) && (
+
+ )}
+ {!isMembersLoading && !filteredUsers?.length && (
+
)}