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/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index d10f7be25..c869b03ec 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -67,13 +67,12 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void; }; -export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { +const ServiceTokenForm = () => { const { t } = useTranslation(); const { currentWorkspace } = useWorkspace(); const { control, - reset, handleSubmit, formState: { isSubmitting } } = useForm({ @@ -152,13 +151,197 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { } }; + return !hasServiceToken ? ( +
+ ( + + + + )} + /> + {tokenScopes.map(({ id }, index) => ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + remove(index)} + > + + +
+ ))} +
+ +
+ ( + + + + )} + /> + { + const options = [ + { + label: "Read (default)", + value: "read" + }, + { + label: "Write (optional)", + value: "write" + } + ] as const; + + return ( + + <> + {options.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} + + + ); + }} + /> +
+ + + + +
+ + ) : ( +
+

{newToken}

+ + + + {t("common.click-to-copy")} + + +
+ ); +}; + +export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { + const { t } = useTranslation(); + + const { currentWorkspace } = useWorkspace(); + return ( { handlePopUpToggle("createAPIToken", open); - reset(); - setToken(""); }} > { } subTitle={t("section.token.add-dialog.description") as string} > - {!hasServiceToken ? ( -
- ( - - - - )} - /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - > - - -
- ))} -
- -
- ( - - - - )} - /> - { - const options = [ - { - label: "Read (default)", - value: "read" - }, - { - label: "Write (optional)", - value: "write" - } - ] as const; - - return ( - - <> - {options.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} - - - ); - }} - /> -
- - - - -
- - ) : ( -
-

{newToken}

- - - - {t("common.click-to-copy")} - - -
- )} +
); 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, scopes } = token; + + const searchValue = search.trim().toLowerCase(); + + if (name.toLowerCase().includes(searchValue)) { + return true; + } + + return scopes.some( + ({ environment, secretPath }) => + environment.toLowerCase().includes(searchValue) || + secretPath.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 by name, environment or secret path..." + 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 && ( + + )} + +
); };