From 217de6250f7d28c1ba8501faf969c9f0615e55a5 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 13 Sep 2024 14:12:53 +0530 Subject: [PATCH] feat: pagination for main secret page --- .../views/SecretMainPage/SecretMainPage.tsx | 97 ++++++++++++--- .../SecretMainPage/SecretMainPage.types.ts | 7 +- .../components/ActionBar/ActionBar.tsx | 14 +-- .../SecretListView/SecretListView.tsx | 117 ++++-------------- .../SecretOverviewPage/SecretOverviewPage.tsx | 1 + 5 files changed, 113 insertions(+), 123 deletions(-) diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index a86d5d0d6..3938217a2 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import { subject } from "@casl/ability"; @@ -8,14 +8,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import NavHeader from "@app/components/navigation/NavHeader"; import { createNotification } from "@app/components/notifications"; import { PermissionDeniedBanner } from "@app/components/permissions"; -import { ContentLoader } from "@app/components/v2"; +import { ContentLoader, Pagination } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; +import { useDebounce, usePopUp } from "@app/hooks"; import { useGetDynamicSecrets, useGetImportedSecretsSingleEnv, @@ -39,7 +39,7 @@ import { SecretImportListView } from "./components/SecretImportListView"; import { SecretListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; import { StoreProvider } from "./SecretMainPage.store"; -import { Filter, GroupBy, SortDir } from "./SecretMainPage.types"; +import { Filter, SortDir } from "./SecretMainPage.types"; const LOADER_TEXT = [ "Retrieving your encrypted secrets...", @@ -47,6 +47,7 @@ const LOADER_TEXT = [ "Getting secret import links..." ]; +const INIT_PER_PAGE = 10; export const SecretMainPage = () => { const { t } = useTranslation(); const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace(); @@ -59,6 +60,10 @@ export const SecretMainPage = () => { tags: {}, searchFilter: (router.query.searchFilter as string) || "" }); + const debouncedSearchFilter = useDebounce(filter.searchFilter); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(INIT_PER_PAGE); + const paginationOffset = (page - 1) * perPage; const [snapshotId, setSnapshotId] = useState(null); const isRollbackMode = Boolean(snapshotId); @@ -185,11 +190,6 @@ export const SecretMainPage = () => { }); }; - const handleGroupByChange = useCallback( - (groupBy?: GroupBy) => setFilter((state) => ({ ...state, groupBy })), - [] - ); - const handleTagToggle = useCallback( (tagId: string) => setFilter((state) => { @@ -223,6 +223,66 @@ export const SecretMainPage = () => { const loadingOnAccess = canReadSecret && (isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading); + + const rows = useMemo(() => { + const filteredSecrets = + secrets + ?.filter(({ key }) => key.toUpperCase().includes(debouncedSearchFilter.toUpperCase())) + .sort((a, b) => + sortDir === SortDir.ASC ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) + ) ?? []; + const filteredFolders = + folders + ?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase())) + .sort((a, b) => + sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) + ) ?? []; + const filteredDynamicSecrets = + dynamicSecrets + ?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase())) + .sort((a, b) => + sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) + ) ?? []; + + const totalRows = + filteredFolders.length + filteredDynamicSecrets.length + filteredSecrets.length; + + const paginatedFolders = filteredFolders.slice(paginationOffset, paginationOffset + perPage); + + let remainingRows = perPage - paginatedFolders.length; + const dynamicSecretStartIndex = Math.max(0, paginationOffset - filteredFolders.length); + const paginatiedDynamicSecrets = + remainingRows > 0 + ? filteredDynamicSecrets.slice( + dynamicSecretStartIndex, + dynamicSecretStartIndex + remainingRows + ) + : []; + + remainingRows -= paginatiedDynamicSecrets.length; + const secretStartIndex = Math.max( + 0, + paginationOffset - filteredFolders.length - filteredDynamicSecrets.length + ); + + const paginatiedSecrets = + remainingRows > 0 + ? filteredSecrets.slice(secretStartIndex, secretStartIndex + remainingRows) + : []; + + return { + folders: paginatedFolders, + secrets: paginatiedSecrets, + dynamicSecrets: paginatiedDynamicSecrets, + totalRows + }; + }, [sortDir, debouncedSearchFilter, folders, secrets, dynamicSecrets, paginationOffset, perPage]); + + useEffect(() => { + // reset page if no longer valid + if (rows.totalRows < paginationOffset) setPage(1); + }, [rows.totalRows]); + // loading screen when you don't have permission but as folder's is viewable need to wait for that const loadingOnDenied = !canReadSecret && isFoldersLoading; if (loadingOnAccess || loadingOnDenied) { @@ -258,7 +318,6 @@ export const SecretMainPage = () => { filter={filter} tags={tags} onVisiblilityToggle={handleToggleVisibility} - onGroupByChange={handleGroupByChange} onSearchChange={handleSearchChange} onToggleTagFilter={handleTagToggle} snapshotCount={snapshotCount || 0} @@ -301,7 +360,7 @@ export const SecretMainPage = () => { /> )} { environment={environment} projectSlug={projectSlug} secretPath={secretPath} - dynamicSecrets={dynamicSecrets || []} + dynamicSecrets={rows.dynamicSecrets || []} /> )} {canReadSecret && ( { /> )} {!canReadSecret && folders?.length === 0 && } + {!loadingOnAccess && rows.totalRows > INIT_PER_PAGE && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} ; searchFilter: string; - groupBy?: GroupBy | null; }; export enum SortDir { @@ -9,6 +8,8 @@ export enum SortDir { DESC = "desc" } -export enum GroupBy { - PREFIX = "prefix" +export enum RowType { + Folder = "folder", + DynamicSecret = "dynamic", + Secret = "Secret" } diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 65cb0389f..94ac029bd 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -62,7 +62,7 @@ import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; -import { Filter, GroupBy } from "../../SecretMainPage.types"; +import { Filter } from "../../SecretMainPage.types"; import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { FolderForm } from "./FolderForm"; @@ -81,7 +81,6 @@ type Props = { isVisible?: boolean; snapshotCount: number; isSnapshotCountLoading?: boolean; - onGroupByChange: (opt?: GroupBy) => void; onSearchChange: (term: string) => void; onToggleTagFilter: (tagId: string) => void; onVisiblilityToggle: () => void; @@ -101,7 +100,6 @@ export const ActionBar = ({ isSnapshotCountLoading, onSearchChange, onToggleTagFilter, - onGroupByChange, onVisiblilityToggle, onClickRollbackMode }: Props) => { @@ -307,16 +305,6 @@ export const ActionBar = ({ - Group By - - } - onClick={() => onGroupByChange(!filter.groupBy ? GroupBy.PREFIX : undefined)} - > - Prefix - Filter By { - const groupedSecrets: Record = {}; - secrets.forEach((secret) => { - const lastSeperatorIndex = secret.key.lastIndexOf("_"); - const namespace = - lastSeperatorIndex !== -1 ? secret.key.substring(0, lastSeperatorIndex) : "misc"; - if (!groupedSecrets?.[namespace]) groupedSecrets[namespace] = []; - groupedSecrets[namespace].push(secret); - }); - - return Object.keys(groupedSecrets) - .sort((a, b) => - sortDir === SortDir.ASC - ? a.toLowerCase().localeCompare(b.toLowerCase()) - : b.toLowerCase().localeCompare(a.toLowerCase()) - ) - .map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] })); -}; - -const reorderSecret = ( - secrets: SecretV3RawSanitized[], - sortDir: SortDir, - filter?: GroupBy | null -) => { - if (filter === GroupBy.PREFIX) { - return reorderSecretGroupByUnderscore(secrets, sortDir); - } - - return [ - { - namespace: "", - secrets: secrets?.sort((a, b) => - sortDir === SortDir.ASC - ? a.key.toLowerCase().localeCompare(b.key.toLowerCase()) - : b.key.toLowerCase().localeCompare(a.key.toLowerCase()) - ) - } - ]; -}; - export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) => secrets.filter(({ key, value, tags }) => { const isTagFilterActive = Boolean(Object.keys(filter.tags).length); @@ -88,8 +45,6 @@ export const SecretListView = ({ environment, workspaceId, secretPath = "/", - filter, - sortDir = SortDir.ASC, tags: wsTags = [], isVisible, isProtectedBranch = false @@ -331,52 +286,30 @@ export const SecretListView = ({ return ( <> - {reorderSecret(secrets, sortDir, filter.groupBy).map( - ({ namespace, secrets: groupedSecrets }) => { - const filteredSecrets = filterSecrets(groupedSecrets, filter); - return ( -
-
- {namespace} -
- {FontAwesomeSpriteSymbols.map(({ icon, symbol }) => ( - - ))} - {filteredSecrets.map((secret) => ( - - handlePopUpOpen("createSharedSecret", { - value: secret.valueOverride ?? secret.value - }) - } - /> - ))} -
- ); - } - )} + {FontAwesomeSpriteSymbols.map(({ icon, symbol }) => ( + + ))} + {secrets.map((secret) => ( + + handlePopUpOpen("createSharedSecret", { + value: secret.valueOverride ?? secret.value + }) + } + /> + ))} { {!isTableLoading && rows.length > INIT_PER_PAGE && (