Merge pull request #2798 from Infisical/project-overview-pagination

Improvement: Add Pagination to the Project Overview Page
This commit is contained in:
Scott Wilson
2024-11-28 08:59:04 -08:00
committed by GitHub
2 changed files with 172 additions and 79 deletions
+14 -3
View File
@@ -3,9 +3,16 @@ import { useState } from "react";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useDebounce } from "@app/hooks/useDebounce"; import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(initialOrderBy: T) => { export const usePagination = <T extends string>(
initialOrderBy: T,
{
initPerPage = 100
}: {
initPerPage?: number;
} = {}
) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(100); const [perPage, setPerPage] = useState(initPerPage);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC); const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy); const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -26,6 +33,10 @@ export const usePagination = <T extends string>(initialOrderBy: T) => {
search, search,
setSearch, setSearch,
orderBy, orderBy,
setOrderBy setOrderBy,
toggleOrderDirection: () =>
setOrderDirection((prev) =>
prev === OrderByDirection.DESC ? OrderByDirection.ASC : OrderByDirection.DESC
)
}; };
}; };
+158 -76
View File
@@ -1,6 +1,6 @@
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex // REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
import { useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
@@ -9,20 +9,22 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowDownAZ,
faArrowRight, faArrowRight,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faArrowUpZA,
faBorderAll, faBorderAll,
faCheck, faCheck,
faCheckCircle, faCheckCircle,
faClipboard, faClipboard,
faExclamationCircle, faExclamationCircle,
faFileShield,
faHandPeace, faHandPeace,
faList, faList,
faMagnifyingGlass, faMagnifyingGlass,
faNetworkWired, faNetworkWired,
faPlug, faPlug,
faPlus, faPlus,
faSearch,
faStar as faSolidStar, faStar as faSolidStar,
faUserPlus faUserPlus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -32,7 +34,15 @@ import * as Tabs from "@radix-ui/react-tabs";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2"; import {
Button,
IconButton,
Input,
Pagination,
Skeleton,
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
import { NewProjectModal } from "@app/components/v2/projects"; import { NewProjectModal } from "@app/components/v2/projects";
import { import {
OrgPermissionActions, OrgPermissionActions,
@@ -42,7 +52,9 @@ import {
useUser, useUser,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useRegisterUserAction } from "@app/hooks/api"; import { useRegisterUserAction } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
@@ -81,6 +93,10 @@ enum ProjectsViewMode {
LIST = "list" LIST = "list"
} }
enum ProjectOrderBy {
Name = "name"
}
function copyToClipboard(id: string, setState: (value: boolean) => void) { function copyToClipboard(id: string, setState: (value: boolean) => void) {
// Get the text field // Get the text field
const copyText = document.getElementById(id) as HTMLInputElement; const copyText = document.getElementById(id) as HTMLInputElement;
@@ -496,26 +512,48 @@ const OrganizationPage = () => {
}); });
}, []); }, []);
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()) const {
setPage,
perPage,
setPerPage,
page,
offset,
limit,
toggleOrderDirection,
orderDirection
} = usePagination(ProjectOrderBy.Name, { initPerPage: 24 });
const filteredWorkspaces = useMemo(
() =>
orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) =>
orderDirection === OrderByDirection.ASC
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
),
[searchFilter, page, perPage, orderDirection, offset, limit]
); );
const { workspacesWithFaveProp, favoriteWorkspaces, nonFavoriteWorkspaces } = useMemo(() => { useResetPageHelper({
setPage,
offset,
totalCount: filteredWorkspaces.length
});
const { workspacesWithFaveProp } = useMemo(() => {
const workspacesWithFav = filteredWorkspaces const workspacesWithFav = filteredWorkspaces
.map((w): Workspace & { isFavorite: boolean } => ({ .map((w): Workspace & { isFavorite: boolean } => ({
...w, ...w,
isFavorite: Boolean(projectFavorites?.includes(w.id)) isFavorite: Boolean(projectFavorites?.includes(w.id))
})) }))
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)); .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
.slice(offset, limit * page);
const favWorkspaces = workspacesWithFav.filter((w) => w.isFavorite);
const nonFavWorkspaces = workspacesWithFav.filter((w) => !w.isFavorite);
return { return {
workspacesWithFaveProp: workspacesWithFav, workspacesWithFaveProp: workspacesWithFav
favoriteWorkspaces: favWorkspaces,
nonFavoriteWorkspaces: nonFavWorkspaces
}; };
}, [filteredWorkspaces, projectFavorites]); }, [filteredWorkspaces, projectFavorites]);
@@ -566,7 +604,7 @@ const OrganizationPage = () => {
{isFavorite ? ( {isFavorite ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faSolidStar} icon={faSolidStar}
className="text-sm text-mineshaft-300 hover:text-mineshaft-400" className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
removeProjectFromFavorites(workspace.id); removeProjectFromFavorites(workspace.id);
@@ -623,11 +661,10 @@ const OrganizationPage = () => {
key={workspace.id} key={workspace.id}
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
index === 0 && "rounded-t-md" index === 0 && "rounded-t-md"
} ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`} }`}
> >
<div className="flex items-center sm:col-span-3 lg:col-span-4"> <div className="flex items-center sm:col-span-3 lg:col-span-4">
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" /> <div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
</div> </div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2"> <div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="text-center text-sm text-mineshaft-300"> <div className="text-center text-sm text-mineshaft-300">
@@ -636,7 +673,7 @@ const OrganizationPage = () => {
{isFavorite ? ( {isFavorite ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faSolidStar} icon={faSolidStar}
className="ml-6 text-sm text-mineshaft-300 hover:text-mineshaft-400" className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
removeProjectFromFavorites(workspace.id); removeProjectFromFavorites(workspace.id);
@@ -656,63 +693,75 @@ const OrganizationPage = () => {
</div> </div>
); );
const projectsGridView = ( let projectsComponents: ReactNode;
<>
{favoriteWorkspaces.length > 0 && (
<>
<p className="mt-6 text-xl font-semibold text-white">Favorites</p>
<div
className={`b grid w-full grid-cols-1 gap-4 ${
nonFavoriteWorkspaces.length > 0 && "border-b border-mineshaft-600"
} py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`}
>
{favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))}
</div>
</>
)}
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
))}
{!isProjectViewLoading &&
nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
</div>
</>
);
const projectsListView = ( if (filteredWorkspaces.length || isProjectViewLoading) {
<div className="mt-4 w-full rounded-md"> switch (projectsViewMode) {
{isProjectViewLoading && case ProjectsViewMode.GRID:
Array.apply(0, Array(3)).map((_x, i) => ( projectsComponents = (
<div <div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
key={`workspace-cards-loading-${i + 1}`} {isProjectViewLoading &&
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ Array.apply(0, Array(3)).map((_x, i) => (
i === 0 && "rounded-t-md" <div
} ${i === 2 && "rounded-b-md border-b"}`} key={`workspace-cards-loading-${i + 1}`}
> className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
<Skeleton className="w-full bg-mineshaft-600" /> >
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
))}
{!isProjectViewLoading && (
<>
{workspacesWithFaveProp.map((workspace) =>
renderProjectGridItem(workspace, workspace.isFavorite)
)}
</>
)}
</div> </div>
))} );
{!isProjectViewLoading &&
workspacesWithFaveProp.map((workspace, ind) => break;
renderProjectListItem(workspace, workspace.isFavorite, ind) case ProjectsViewMode.LIST:
)} default:
</div> projectsComponents = (
); <div className="mt-4 w-full rounded-md">
{isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
i === 0 && "rounded-t-md"
} ${i === 2 && "rounded-b-md border-b"}`}
>
<Skeleton className="w-full bg-mineshaft-600" />
</div>
))}
{!isProjectViewLoading &&
workspacesWithFaveProp.map((workspace, ind) =>
renderProjectListItem(workspace, workspace.isFavorite, ind)
)}
</div>
);
break;
}
} else if (orgWorkspaces.length) {
projectsComponents = (
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faSearch}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light">No projects match search...</div>
</div>
);
}
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
@@ -754,6 +803,24 @@ const OrganizationPage = () => {
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/> />
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Tooltip content="Toggle Sort Direction">
<IconButton
className="min-w-[2.4rem] border-none hover:bg-mineshaft-600"
ariaLabel={`Sort ${
orderDirection === OrderByDirection.ASC ? "descending" : "ascending"
}`}
variant="plain"
size="xs"
colorSchema="secondary"
onClick={toggleOrderDirection}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDownAZ : faArrowUpZA}
/>
</IconButton>
</Tooltip>
</div>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1"> <div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<IconButton <IconButton
variant="outline_bg" variant="outline_bg"
@@ -804,9 +871,24 @@ const OrganizationPage = () => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
{projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView} {projectsComponents}
{!isProjectViewLoading && Boolean(filteredWorkspaces.length) && (
<Pagination
className={
projectsViewMode === ProjectsViewMode.GRID
? "col-span-full border-transparent bg-transparent"
: "rounded-b-md border border-mineshaft-600"
}
perPage={perPage}
perPageList={[12, 24, 48, 96]}
count={filteredWorkspaces.length}
page={page}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{isWorkspaceEmpty && ( {isWorkspaceEmpty && (
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300"> <div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon <FontAwesomeIcon
icon={faFolderOpen} icon={faFolderOpen}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400" className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"