Merge pull request #2610 from scott-ray-wilson/group-tables-fixes/improvements

Fix: Group Tables/Pagination Fixes and Improvements
This commit is contained in:
Scott Wilson
2024-10-21 09:39:00 -07:00
committed by GitHub
18 changed files with 247 additions and 81 deletions

View File

@@ -10,13 +10,13 @@ export const groupKeys = {
slug,
offset,
limit,
username
search
}: {
slug: string;
offset: number;
limit: number;
username: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, username }] as const
search: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search }] as const
};
type TUser = {
@@ -33,27 +33,28 @@ export const useListGroupUsers = ({
groupSlug,
offset = 0,
limit = 10,
username
search
}: {
id: string;
groupSlug: string;
offset: number;
limit: number;
username: string;
search: string;
}) => {
return useQuery({
queryKey: groupKeys.specificGroupUserMemberships({
slug: groupSlug,
offset,
limit,
username
search
}),
enabled: Boolean(groupSlug),
keepPreviousData: true,
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
username
search
});
const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number }>(

View File

@@ -3,6 +3,7 @@ export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";

View File

@@ -0,0 +1,16 @@
import { Dispatch, SetStateAction, useEffect } from "react";
export const useResetPageHelper = ({
totalCount,
offset,
setPage
}: {
totalCount: number;
offset: number;
setPage: Dispatch<SetStateAction<number>>;
}) => {
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
};

View File

@@ -21,6 +21,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useDebounce, useResetPageHelper } from "@app/hooks";
import { useAddUserToGroup, useListGroupUsers, useRemoveUserFromGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -33,18 +34,28 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const [debouncedSearch] = useDebounce(searchMemberFilter);
const popUpData = popUp?.groupMembers?.data as {
groupId: string;
slug: string;
};
const offset = (page - 1) * perPage;
const { data, isLoading } = useListGroupUsers({
id: popUpData?.groupId,
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
offset,
limit: perPage,
username: searchMemberFilter
search: debouncedSearch
});
const { totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
@@ -140,9 +151,9 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
{!isLoading && totalCount > 0 && (
<Pagination
count={data.totalCount}
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
@@ -150,7 +161,10 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
/>
)}
{!isLoading && !data?.users?.length && (
<EmptyState title="No users found" icon={faUsers} />
<EmptyState
title={debouncedSearch ? "No users match search" : "No users found"}
icon={faUsers}
/>
)}
</TableContainer>
</ModalContent>

View File

@@ -1,5 +1,11 @@
import { useState } from "react";
import { faEllipsis, faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faEllipsis,
faMagnifyingGlass,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -11,6 +17,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Select,
SelectItem,
@@ -24,7 +31,9 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDebounce } 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";
type Props = {
@@ -43,12 +52,21 @@ type Props = {
) => void;
};
enum GroupsOrderBy {
Name = "name",
Slug = "slug",
Role = "role"
}
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 { mutateAsync: updateMutateAsync } = useUpdateGroup();
const [orderBy, setOrderBy] = useState(GroupsOrderBy.Name);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const { data: roles } = useGetOrgRoles(orgId);
@@ -72,6 +90,43 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
}
};
const filteredGroups = useMemo(() => {
const filtered = debouncedSearch
? groups?.filter(
({ name, slug }) =>
name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
slug.toLowerCase().includes(debouncedSearch.toLowerCase())
)
: groups;
const ordered = filtered?.sort((a, b) => {
switch (orderBy) {
case GroupsOrderBy.Role: {
const aValue = a.role === "custom" ? (a.customRole?.name as string) : a.role;
const bValue = b.role === "custom" ? (b.customRole?.name as string) : b.role;
return aValue.toLowerCase().localeCompare(bValue.toLowerCase());
}
default:
return a[orderBy].toLowerCase().localeCompare(b[orderBy].toLowerCase());
}
});
return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse();
}, [debouncedSearch, groups, orderBy, orderDirection]);
const handleSort = (column: GroupsOrderBy) => {
if (column === orderBy) {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
return (
<div>
<Input
@@ -84,16 +139,70 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Slug
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Slug ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Slug)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Slug
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Role)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Role
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading &&
groups?.map(({ id, name, slug, role, customRole }) => {
filteredGroups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import {
faArrowDown,
@@ -34,7 +33,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination } from "@app/hooks";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
@@ -87,10 +86,11 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
);
const { totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { data: roles } = useGetOrgRoles(organizationId);

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import Link from "next/link";
import {
faArrowDown,
@@ -51,7 +50,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp } from "@app/hooks";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -108,10 +107,11 @@ export const CmekTable = () => {
});
const { keys = [], totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKey",

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import Link from "next/link";
import {
faArrowDown,
@@ -44,7 +43,7 @@ import {
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePagination } from "@app/hooks";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -99,10 +98,11 @@ export const IdentityTab = withProjectPermission(
const { totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();

View File

@@ -16,7 +16,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useGetImportedSecretsSingleEnv,
useGetSecretApprovalPolicyOfABoard,
@@ -164,10 +164,11 @@ const SecretMainPageContent = () => {
totalCount = 0
} = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
// fetch imported secrets to show user the overriden ones
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({

View File

@@ -55,7 +55,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useCreateFolder,
useCreateSecretV3,
@@ -213,10 +213,11 @@ export const SecretOverviewPage = () => {
totalCount = 0
} = overview ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);