From 6eaa16bd079718686c7f97462490620af3077704 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 16 Jun 2025 16:13:09 -0700 Subject: [PATCH] improvement: add pagination, search and column sort to service token table and improve table rows --- .../components/v2/Pagination/Pagination.tsx | 10 +- .../ServiceTokenSection.tsx | 30 +- .../ServiceTokenSection/ServiceTokenTable.tsx | 300 ++++++++++++++---- 3 files changed, 269 insertions(+), 71 deletions(-) diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index 417e450bd..c1f9306fa 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -70,7 +70,15 @@ export const Pagination = ({ key={`pagination-per-page-options-${perPageOption}`} icon={perPage === perPageOption && } iconPos="right" - onClick={() => onChangePerPage(perPageOption)} + onClick={() => { + const totalPages = Math.ceil(count / perPageOption); + + if (page > totalPages) { + onChangePage(totalPages); + } + + onChangePerPage(perPageOption); + }} > {perPageOption} rows per page diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx index e66ad17cd..712844e18 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; @@ -48,8 +48,29 @@ export const ServiceTokenSection = withProjectPermission( return (
-
-

Service Tokens

+
+
+
+

Service Tokens

+ +
+ + Docs + +
+
+
+

+ {t("section.token.service-tokens-description")} +

+
- Create token + Create Token )}
-

{t("section.token.service-tokens-description")}

void; }; +enum TokensOrderBy { + Name = "name", + Expiration = "expiration" +} + export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); const { data, isPending } = useGetUserWsServiceTokens({ workspaceID: currentWorkspace?.id || "" }); + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection, + orderBy, + setOrderDirection, + setOrderBy + } = usePagination(TokensOrderBy.Name, { + initPerPage: getUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, newPerPage); + }; + + const filteredTokens = useMemo( + () => + data + ?.filter((token) => { + const { name } = token; + + const searchValue = search.trim().toLowerCase(); + + return name.toLowerCase().includes(searchValue); + }) + .sort((a, b) => { + const [tokenOne, tokenTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + switch (orderBy) { + case TokensOrderBy.Expiration: + if (!tokenOne.expiresAt && !tokenTwo.expiresAt) return 0; + if (!tokenOne.expiresAt) return 1; + if (!tokenTwo.expiresAt) return -1; + + return ( + new Date(tokenOne.expiresAt).getTime() - new Date(tokenTwo.expiresAt).getTime() + ); + + case TokensOrderBy.Name: + default: + return tokenOne.name.toLowerCase().localeCompare(tokenTwo.name.toLowerCase()); + } + }) ?? [], + [data, orderDirection, search, orderBy] + ); + + useResetPageHelper({ + totalCount: filteredTokens.length, + offset, + setPage + }); + + const handleSort = (column: TokensOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + const getClassName = (col: TokensOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + + const getColSortIcon = (col: TokensOrderBy) => + orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; + return ( - - - - - - - - - - - {isPending && } - {!isPending && - data && - data.map((row) => ( - - - - - - - ))} - {!isPending && data && data?.length === 0 && ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search service tokens..." + className="flex-1" + containerClassName="mb-4 mt-2" + /> + +
Token NameEnvironment - Secret PathValid Until -
{row.name} -
- {row?.scopes.map(({ secretPath, environment }) => ( -
-
{environment}
- - {secretPath} -
- ))} -
-
{row.expiresAt && new Date(row.expiresAt).toUTCString()} - - {(isAllowed) => ( - - handlePopUpOpen("deleteAPITokenConfirmation", { - name: row.name, - id: row.id - }) - } - colorSchema="danger" - ariaLabel="delete" - isDisabled={!isAllowed} - > - - - )} - -
+ - + + + + - )} - -
- - +
+ Name + handleSort(TokensOrderBy.Name)} + > + + +
+
Environment / Secret Path +
+ Valid Until + handleSort(TokensOrderBy.Expiration)} + > + + +
+
-
+ + + {isPending && } + {!isPending && + filteredTokens.slice(offset, perPage * page).map((row) => ( + + {row.name} + +
+ {row?.scopes.map(({ secretPath, environment }) => ( +
+
+ {environment} +
+ + {secretPath} +
+ ))} +
+ + + {row.expiresAt ? ( + format(row.expiresAt, "MM/dd/yyyy h:mm:ss aa") + ) : ( + N/A + )} + + + + + + + + + + + + {(isAllowed) => ( + } + isDisabled={!isAllowed} + onClick={(e) => { + e.stopPropagation(); + handlePopUpOpen("deleteAPITokenConfirmation", { + name: row.name, + id: row.id + }); + }} + > + Delete Token + + )} + + + + + + + ))} + + + {Boolean(filteredTokens.length) && ( + + )} + {!isPending && !filteredTokens?.length && ( + + )} + +
); };