diff --git a/frontend/src/pages/pam/ApprovalRequestDetailPage/ApprovalRequestDetailPage.tsx b/frontend/src/pages/pam/ApprovalRequestDetailPage/ApprovalRequestDetailPage.tsx
index 2d12b8c68..70abf1983 100644
--- a/frontend/src/pages/pam/ApprovalRequestDetailPage/ApprovalRequestDetailPage.tsx
+++ b/frontend/src/pages/pam/ApprovalRequestDetailPage/ApprovalRequestDetailPage.tsx
@@ -2,13 +2,21 @@ import { Helmet } from "react-helmet";
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
-import { Link, useParams } from "@tanstack/react-router";
+import { Link, useNavigate, useParams } from "@tanstack/react-router";
-import { ContentLoader, EmptyState, PageHeader } from "@app/components/v2";
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ ConfirmActionModal,
+ ContentLoader,
+ EmptyState,
+ PageHeader
+} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
-import { useOrganization, useProject } from "@app/context";
+import { useOrganization, useProject, useUser } from "@app/context";
+import { usePopUp } from "@app/hooks";
import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
-import { approvalRequestQuery } from "@app/hooks/api/approvalRequests";
+import { approvalRequestQuery, useCancelApprovalRequest } from "@app/hooks/api/approvalRequests";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ApprovalStepsSection, RequestActionsSection, RequestDetailsSection } from "./components";
@@ -19,6 +27,10 @@ const PageContent = () => {
});
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
+ const { user: currentUser } = useUser();
+ const cancelApprovalRequest = useCancelApprovalRequest();
+ const navigate = useNavigate();
+ const { handlePopUpOpen, handlePopUpToggle, popUp } = usePopUp(["cancelRequest"]);
const { data: request, isPending } = useQuery(
approvalRequestQuery.getById({
@@ -27,6 +39,32 @@ const PageContent = () => {
})
);
+ const handleRequestCancel = async () => {
+ if (cancelApprovalRequest.isPending || !request) return;
+
+ await cancelApprovalRequest.mutateAsync(
+ {
+ requestId: request.id,
+ policyType: ApprovalPolicyType.PamAccess
+ },
+ {
+ onSuccess: () => {
+ createNotification({
+ text: "Successfully cancelled request",
+ type: "success"
+ });
+ navigate({
+ to: "/organizations/$orgId/projects/pam/$projectId/approvals",
+ params: {
+ orgId: currentProject.orgId,
+ projectId: currentProject.id
+ }
+ });
+ }
+ }
+ );
+ };
+
if (isPending) {
return (
@@ -62,7 +100,20 @@ const PageContent = () => {
scope={ProjectType.PAM}
title="Approval Request"
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.accessDuration} by ${request.requesterName || "Unknown"}`}
- />
+ >
+
+ {request.requesterId === currentUser.id && (
+
+ )}
+
+
@@ -73,6 +124,14 @@ const PageContent = () => {
+ handlePopUpToggle("cancelRequest", isOpen)}
+ onConfirmed={handleRequestCancel}
+ buttonText="Confirm"
+ />
);
};
diff --git a/frontend/src/pages/pam/ApprovalsPage/ApprovalsPage.tsx b/frontend/src/pages/pam/ApprovalsPage/ApprovalsPage.tsx
index b14ca5328..d9bdbfda5 100644
--- a/frontend/src/pages/pam/ApprovalsPage/ApprovalsPage.tsx
+++ b/frontend/src/pages/pam/ApprovalsPage/ApprovalsPage.tsx
@@ -7,6 +7,7 @@ import { ApprovalControlTabs } from "@app/types/project";
import { ApprovalRequestTab } from "./components/ApprovalRequestTab";
import { PolicyTab } from "./components/PolicyTab";
+import { RequestGrantTab } from "./components/RequestGrantTab";
const Page = () => {
const navigate = useNavigate();
@@ -55,7 +56,7 @@ const Page = () => {
- Hello
+
diff --git a/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/RequestGrantTab.tsx b/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/RequestGrantTab.tsx
index 564b1cf4f..9f611aa00 100644
--- a/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/RequestGrantTab.tsx
+++ b/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/RequestGrantTab.tsx
@@ -1,3 +1,433 @@
-export const RequestGrantTab = () => {
- return Grant list
;
+import { useMemo, useState } from "react";
+import { useForm } from "react-hook-form";
+import {
+ faCheckCircle,
+ faChevronRight,
+ faEllipsisV,
+ faFileCircleQuestion,
+ faFilter,
+ faInfo,
+ faMagnifyingGlass,
+ faSearch,
+ faXmark
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useQuery } from "@tanstack/react-query";
+import { Link } from "@tanstack/react-router";
+import { formatDistance } from "date-fns";
+import { twMerge } from "tailwind-merge";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ DropdownSubMenu,
+ DropdownSubMenuContent,
+ DropdownSubMenuTrigger,
+ EmptyState,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Pagination,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ TextArea,
+ Th,
+ THead,
+ Tooltip,
+ Tr
+} from "@app/components/v2";
+import { Badge } from "@app/components/v3";
+import { useProject } from "@app/context";
+import { getMemberLabel } from "@app/helpers/members";
+import { getUserTablePreference, PreferenceKey } from "@app/helpers/userTablePreferences";
+import { usePagination } from "@app/hooks";
+import { useGetWorkspaceUsers } from "@app/hooks/api";
+import {
+ approvalGrantQuery,
+ ApprovalGrantStatus,
+ useRevokeApprovalGrant
+} from "@app/hooks/api/approvalGrants";
+import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
+
+const revokeGrantSchema = z.object({
+ revocationReason: z.string().max(256).optional()
+});
+
+type TRevokeGrantForm = z.infer;
+
+const getStatusBadgeColor = (status: ApprovalGrantStatus) => {
+ switch (status) {
+ case ApprovalGrantStatus.Active:
+ return "success";
+ case ApprovalGrantStatus.Expired:
+ return "neutral";
+ case ApprovalGrantStatus.Revoked:
+ return "danger";
+ default:
+ return "neutral";
+ }
+};
+
+export const RequestGrantTab = () => {
+ const { currentProject } = useProject();
+ const [search, setSearch] = useState("");
+ const [filter, setFilter] = useState(ApprovalGrantStatus.Active);
+ const [revokeModalOpen, setRevokeModalOpen] = useState(false);
+ const [grantToRevoke, setGrantToRevoke] = useState(null);
+ const { data: members = [] } = useGetWorkspaceUsers(currentProject.id);
+
+ const projectId = currentProject?.id || "";
+
+ const { data: grants = [], isPending: isGrantsLoading } = useQuery(
+ approvalGrantQuery.list({
+ policyType: ApprovalPolicyType.PamAccess,
+ projectId
+ })
+ );
+
+ const { mutateAsync: revokeGrant, isPending: isRevoking } = useRevokeApprovalGrant();
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors }
+ } = useForm({
+ resolver: zodResolver(revokeGrantSchema)
+ });
+
+ const { page, perPage, setPage, setPerPage, offset } = usePagination("", {
+ initPerPage: getUserTablePreference("pamApprovalGrants", PreferenceKey.PerPage, 20)
+ });
+
+ const filteredGrants = useMemo(() => {
+ let filtered = grants;
+
+ // Apply search filter
+ if (search) {
+ filtered = filtered.filter(
+ (grant) =>
+ grant.attributes.accountPath.toLowerCase().includes(search.toLowerCase()) ||
+ grant.id.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ return filtered.filter((grant) => grant.status === filter);
+ }, [grants, search, filter]);
+
+ const paginatedGrants = useMemo(
+ () => filteredGrants.slice(offset, offset + perPage),
+ [filteredGrants, offset, perPage]
+ );
+
+ const openRevokeModal = (grantId: string) => {
+ setGrantToRevoke(grantId);
+ setRevokeModalOpen(true);
+ };
+
+ const closeRevokeModal = () => {
+ setRevokeModalOpen(false);
+ setGrantToRevoke(null);
+ reset();
+ };
+
+ const getGrantedUser = (grantedUserId: string) => {
+ const member = members?.find((m) => m.user.id === grantedUserId);
+ if (member) {
+ return getMemberLabel(member);
+ }
+ return grantedUserId;
+ };
+
+ const handleRevokeGrant = async (data: TRevokeGrantForm) => {
+ if (!grantToRevoke) return;
+
+ try {
+ await revokeGrant({
+ policyType: ApprovalPolicyType.PamAccess,
+ grantId: grantToRevoke,
+ revocationReason: data.revocationReason
+ });
+
+ createNotification({
+ text: "Grant revoked successfully",
+ type: "success"
+ });
+
+ closeRevokeModal();
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to revoke grant",
+ type: "error"
+ });
+ }
+ };
+
+ const isTableFiltered = filter !== ApprovalGrantStatus.Active;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Filter By
+ {
+ evt.preventDefault();
+ setFilter(ApprovalGrantStatus.Active);
+ }}
+ icon={
+ filter === ApprovalGrantStatus.Active &&
+ }
+ iconPos="right"
+ >
+ Active Grants
+
+
+ }
+ >
+ Inactive Grants
+
+
+
+ Filter by Status
+
+ {
+ evt.preventDefault();
+ setFilter(ApprovalGrantStatus.Expired);
+ }}
+ icon={
+ filter === ApprovalGrantStatus.Expired && (
+
+ )
+ }
+ iconPos="right"
+ >
+ Expired
+
+ {
+ evt.preventDefault();
+ setFilter(ApprovalGrantStatus.Revoked);
+ }}
+ icon={
+ filter === ApprovalGrantStatus.Revoked && (
+
+ )
+ }
+ iconPos="right"
+ >
+ Revoked
+
+
+
+
+
+ setSearch(e.target.value)}
+ leftIcon={}
+ placeholder="Search access grants..."
+ />
+
+
+
+
+
+ | User |
+ Account Path |
+ Duration |
+ Status |
+ Granted |
+ Expires |
+ |
+
+
+
+ {isGrantsLoading && }
+ {!isGrantsLoading &&
+ paginatedGrants.map((grant) => {
+ const isActive = grant.status === ApprovalGrantStatus.Active;
+
+ return (
+
+ |
+ {grant.granteeUserId ? getGrantedUser(grant.granteeUserId) : "Unknown"}
+ |
+
+ {grant.attributes.accountPath}
+ |
+
+
+ {grant.attributes.accessDuration}
+
+ |
+
+
+ {isActive ? "Active" : grant.status}
+
+ |
+
+
+ {formatDistance(new Date(grant.createdAt), new Date(), {
+ addSuffix: true
+ })}
+
+ |
+
+ {grant.expiresAt ? (
+
+ {formatDistance(new Date(grant.expiresAt), new Date(), {
+ addSuffix: true
+ })}
+
+ ) : (
+ Never
+ )}
+ |
+ {
+ e.stopPropagation();
+ }}
+ >
+ {isActive && (
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ Request Details
+
+
+ {
+ e.stopPropagation();
+ openRevokeModal(grant.id);
+ }}
+ icon={}
+ >
+ Revoke Grant
+
+
+
+ )}
+ |
+
+ );
+ })}
+
+
+ {Boolean(filteredGrants.length) && (
+
+ )}
+ {!isGrantsLoading && !filteredGrants?.length && (
+
+ )}
+
+
+
!open && closeRevokeModal()}>
+
+
+
+
+
+
+ );
};
diff --git a/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/index.tsx b/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/index.tsx
new file mode 100644
index 000000000..507cf13b7
--- /dev/null
+++ b/frontend/src/pages/pam/ApprovalsPage/components/RequestGrantTab/index.tsx
@@ -0,0 +1 @@
+export { RequestGrantTab } from "./RequestGrantTab";