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

View File

@ -3,9 +3,16 @@ import { useState } from "react";
import { OrderByDirection } from "@app/hooks/api/generic/types";
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 [perPage, setPerPage] = useState(100);
const [perPage, setPerPage] = useState(initPerPage);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
@ -26,6 +33,10 @@ export const usePagination = <T extends string>(initialOrderBy: T) => {
search,
setSearch,
orderBy,
setOrderBy
setOrderBy,
toggleOrderDirection: () =>
setOrderDirection((prev) =>
prev === OrderByDirection.DESC ? OrderByDirection.ASC : OrderByDirection.DESC
)
};
};

View File

@ -1,6 +1,6 @@
// 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 Head from "next/head";
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 { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import {
faArrowDownAZ,
faArrowRight,
faArrowUpRightFromSquare,
faArrowUpZA,
faBorderAll,
faCheck,
faCheckCircle,
faClipboard,
faExclamationCircle,
faFileShield,
faHandPeace,
faList,
faMagnifyingGlass,
faNetworkWired,
faPlug,
faPlus,
faSearch,
faStar as faSolidStar,
faUserPlus
} 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 { OrgPermissionCan } from "@app/components/permissions";
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 {
OrgPermissionActions,
@ -42,7 +52,9 @@ import {
useUser,
useWorkspace
} from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useRegisterUserAction } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types";
@ -81,6 +93,10 @@ enum ProjectsViewMode {
LIST = "list"
}
enum ProjectOrderBy {
Name = "name"
}
function copyToClipboard(id: string, setState: (value: boolean) => void) {
// Get the text field
const copyText = document.getElementById(id) as HTMLInputElement;
@ -496,26 +512,48 @@ const OrganizationPage = () => {
});
}, []);
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
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
.map((w): Workspace & { isFavorite: boolean } => ({
...w,
isFavorite: Boolean(projectFavorites?.includes(w.id))
}))
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
const favWorkspaces = workspacesWithFav.filter((w) => w.isFavorite);
const nonFavWorkspaces = workspacesWithFav.filter((w) => !w.isFavorite);
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
.slice(offset, limit * page);
return {
workspacesWithFaveProp: workspacesWithFav,
favoriteWorkspaces: favWorkspaces,
nonFavoriteWorkspaces: nonFavWorkspaces
workspacesWithFaveProp: workspacesWithFav
};
}, [filteredWorkspaces, projectFavorites]);
@ -566,7 +604,7 @@ const OrganizationPage = () => {
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
@ -623,11 +661,10 @@ const OrganizationPage = () => {
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 ${
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">
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="text-center text-sm text-mineshaft-300">
@ -636,7 +673,7 @@ const OrganizationPage = () => {
{isFavorite ? (
<FontAwesomeIcon
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) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
@ -656,63 +693,75 @@ const OrganizationPage = () => {
</div>
);
const projectsGridView = (
<>
{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>
</>
);
let projectsComponents: ReactNode;
const projectsListView = (
<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" />
if (filteredWorkspaces.length || isProjectViewLoading) {
switch (projectsViewMode) {
case ProjectsViewMode.GRID:
projectsComponents = (
<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 && (
<>
{workspacesWithFaveProp.map((workspace) =>
renderProjectGridItem(workspace, workspace.isFavorite)
)}
</>
)}
</div>
))}
{!isProjectViewLoading &&
workspacesWithFaveProp.map((workspace, ind) =>
renderProjectListItem(workspace, workspace.isFavorite, ind)
)}
</div>
);
);
break;
case ProjectsViewMode.LIST:
default:
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 (
<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)}
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">
<IconButton
variant="outline_bg"
@ -804,9 +871,24 @@ const OrganizationPage = () => {
)}
</OrgPermissionCan>
</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 && (
<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
icon={faFolderOpen}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"