mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: reorganization of files and more ui changes
This commit is contained in:
@@ -4,7 +4,8 @@ export enum ApprovalRequestStatus {
|
||||
Pending = "pending",
|
||||
Approved = "approved",
|
||||
Rejected = "rejected",
|
||||
Expired = "expired"
|
||||
Expired = "expired",
|
||||
Cancelled = "cancelled"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestStepStatus {
|
||||
|
||||
@@ -16,7 +16,11 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useOrganization, useProject, useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
|
||||
import { approvalRequestQuery, useCancelApprovalRequest } from "@app/hooks/api/approvalRequests";
|
||||
import {
|
||||
approvalRequestQuery,
|
||||
ApprovalRequestStatus,
|
||||
useCancelApprovalRequest
|
||||
} from "@app/hooks/api/approvalRequests";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { ApprovalStepsSection, RequestActionsSection, RequestDetailsSection } from "./components";
|
||||
@@ -102,16 +106,17 @@ const PageContent = () => {
|
||||
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.accessDuration} by ${request.requesterName || "Unknown"}`}
|
||||
>
|
||||
<div>
|
||||
{request.requesterId === currentUser.id && (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("cancelRequest")}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isLoading={cancelApprovalRequest.isPending}
|
||||
>
|
||||
Cancel Request
|
||||
</Button>
|
||||
)}
|
||||
{request.requesterId === currentUser.id &&
|
||||
request.status === ApprovalRequestStatus.Pending && (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("cancelRequest")}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isLoading={cancelApprovalRequest.isPending}
|
||||
>
|
||||
Cancel Request
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="flex justify-center gap-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ApprovalRequestStatus, TApprovalRequest } from "@app/hooks/api/approvalRequests";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,5 +1,358 @@
|
||||
import { RequestsSection } from "./components";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronRight,
|
||||
faExclamationCircle,
|
||||
faFileCircleQuestion,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
DropdownSubMenu,
|
||||
DropdownSubMenuContent,
|
||||
DropdownSubMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useOrganization, useProject, useProjectPermission, useUser } from "@app/context";
|
||||
import { getUserTablePreference, PreferenceKey } from "@app/helpers/userTablePreferences";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import { ApprovalPolicyType, ApproverType } from "@app/hooks/api/approvalPolicies";
|
||||
import {
|
||||
approvalRequestQuery,
|
||||
ApprovalRequestStatus,
|
||||
ApprovalRequestStepStatus,
|
||||
TApprovalRequest
|
||||
} from "@app/hooks/api/approvalRequests";
|
||||
|
||||
const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
|
||||
switch (status) {
|
||||
case ApprovalRequestStatus.Pending:
|
||||
return "project";
|
||||
case ApprovalRequestStatus.Approved:
|
||||
return "success";
|
||||
case ApprovalRequestStatus.Rejected:
|
||||
return "danger";
|
||||
case ApprovalRequestStatus.Expired:
|
||||
return "neutral";
|
||||
default:
|
||||
return "neutral";
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfUserNeedsToApprove = (
|
||||
request: TApprovalRequest,
|
||||
userId: string,
|
||||
userGroups: string[]
|
||||
): boolean => {
|
||||
const currentStep = request.steps.find(
|
||||
(step) => step.status === ApprovalRequestStepStatus.InProgress
|
||||
);
|
||||
|
||||
if (!currentStep) return false;
|
||||
|
||||
const isApprover = currentStep.approvers.some((approver) =>
|
||||
approver.type === ApproverType.User ? approver.id === userId : userGroups.includes(approver.id)
|
||||
);
|
||||
|
||||
if (!isApprover) return false;
|
||||
|
||||
const hasAlreadyApproved = currentStep.approvals.some(
|
||||
(approval) => approval.approverUserId === userId
|
||||
);
|
||||
|
||||
return !hasAlreadyApproved;
|
||||
};
|
||||
|
||||
export const ApprovalRequestTab = () => {
|
||||
return <RequestsSection />;
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentProject } = useProject();
|
||||
const { memberships } = useProjectPermission();
|
||||
const { user } = useUser();
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState(ApprovalRequestStatus.Pending);
|
||||
|
||||
const projectId = currentProject?.id || "";
|
||||
const userId = user?.id || "";
|
||||
const userGroups = memberships.map((el) => el.actorGroupId).filter(Boolean);
|
||||
|
||||
const { data: requests = [], isPending: isRequestsLoading } = useQuery(
|
||||
approvalRequestQuery.list({
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
projectId
|
||||
})
|
||||
);
|
||||
|
||||
const { page, perPage, setPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: getUserTablePreference("PamApprovalRequestTable", PreferenceKey.PerPage, 10)
|
||||
});
|
||||
|
||||
const filteredRequests = useMemo(() => {
|
||||
let filtered = requests;
|
||||
|
||||
// Apply search filter
|
||||
if (search) {
|
||||
filtered = filtered.filter(
|
||||
(request) =>
|
||||
request.requesterName?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.requesterEmail?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.justification?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.requestData.requestData.accountPath.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
return filtered
|
||||
.filter((req) => req.status === filter)
|
||||
.sort(
|
||||
(a, b) => (new Date(b.createdAt)?.getTime() || 0) - (new Date(a.createdAt)?.getTime() || 0)
|
||||
);
|
||||
}, [requests, search, filter]);
|
||||
|
||||
const paginatedRequests = useMemo(
|
||||
() => filteredRequests.slice(offset, offset + perPage),
|
||||
[filteredRequests, offset, perPage]
|
||||
);
|
||||
|
||||
const handleRowClick = (requestId: string) => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id,
|
||||
approvalRequestId: requestId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isTableFiltered = filter !== ApprovalRequestStatus.Pending;
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Approval Requests</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter Requests"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-9.5 w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-0">
|
||||
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Pending);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Pending && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Open Requests
|
||||
</DropdownMenuItem>
|
||||
<DropdownSubMenu>
|
||||
<DropdownSubMenuTrigger
|
||||
iconPos="right"
|
||||
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
|
||||
>
|
||||
Closed Requests
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Filter by Status
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Approved);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Approved && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Approved
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Cancelled);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Cancelled && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Cancelled
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Rejected);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Rejected && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Rejected
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Expired);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Expired && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Expired
|
||||
</DropdownMenuItem>
|
||||
</DropdownSubMenuContent>
|
||||
</DropdownSubMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search approval requests..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Requester</Th>
|
||||
<Th>Account Path</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Requested</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRequestsLoading && <TableSkeleton columns={5} innerKey="approval-requests" />}
|
||||
{!isRequestsLoading &&
|
||||
paginatedRequests.map((request) => {
|
||||
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
|
||||
const { accountPath, accessDuration } = request.requestData.requestData;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={request.id}
|
||||
className={twMerge(
|
||||
"cursor-pointer transition-colors hover:bg-mineshaft-700",
|
||||
needsApproval && "bg-primary/5 hover:bg-primary/10"
|
||||
)}
|
||||
onClick={() => handleRowClick(request.id)}
|
||||
>
|
||||
<Td>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-mineshaft-100">
|
||||
{request.requesterName || "Unknown"}
|
||||
</div>
|
||||
<div className="text-xs text-mineshaft-400">{request.requesterEmail}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div>
|
||||
<div className="text-sm text-mineshaft-200">{accountPath}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-200">{accessDuration}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={getStatusBadgeColor(request.status)}
|
||||
>
|
||||
{request.status.split("-").join(" ")}
|
||||
</Badge>
|
||||
{needsApproval && (
|
||||
<div className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="h-3 w-3" />
|
||||
<span>Approval Required</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
{formatDistance(new Date(request.createdAt), new Date(), {
|
||||
addSuffix: true
|
||||
})}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRequests.length) && (
|
||||
<Pagination
|
||||
count={filteredRequests.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isRequestsLoading && !filteredRequests?.length && (
|
||||
<EmptyState
|
||||
title="No approval requests found"
|
||||
icon={requests.length ? faSearch : faFileCircleQuestion}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { RequestsTable } from "./RequestsTable";
|
||||
|
||||
export const RequestsSection = () => {
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Approval Requests</p>
|
||||
</div>
|
||||
<RequestsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,334 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronRight,
|
||||
faExclamationCircle,
|
||||
faFileCircleQuestion,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
DropdownSubMenu,
|
||||
DropdownSubMenuContent,
|
||||
DropdownSubMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useOrganization, useProject, useProjectPermission, useUser } from "@app/context";
|
||||
import { getUserTablePreference, PreferenceKey } from "@app/helpers/userTablePreferences";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import { ApprovalPolicyType, ApproverType } from "@app/hooks/api/approvalPolicies";
|
||||
import {
|
||||
approvalRequestQuery,
|
||||
ApprovalRequestStatus,
|
||||
ApprovalRequestStepStatus,
|
||||
TApprovalRequest
|
||||
} from "@app/hooks/api/approvalRequests";
|
||||
|
||||
const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
|
||||
switch (status) {
|
||||
case ApprovalRequestStatus.Pending:
|
||||
return "project";
|
||||
case ApprovalRequestStatus.Approved:
|
||||
return "success";
|
||||
case ApprovalRequestStatus.Rejected:
|
||||
return "danger";
|
||||
case ApprovalRequestStatus.Expired:
|
||||
return "neutral";
|
||||
default:
|
||||
return "neutral";
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfUserNeedsToApprove = (
|
||||
request: TApprovalRequest,
|
||||
userId: string,
|
||||
userGroups: string[]
|
||||
): boolean => {
|
||||
const currentStep = request.steps.find(
|
||||
(step) => step.status === ApprovalRequestStepStatus.InProgress
|
||||
);
|
||||
|
||||
if (!currentStep) return false;
|
||||
|
||||
const isApprover = currentStep.approvers.some((approver) =>
|
||||
approver.type === ApproverType.User ? approver.id === userId : userGroups.includes(approver.id)
|
||||
);
|
||||
|
||||
if (!isApprover) return false;
|
||||
|
||||
const hasAlreadyApproved = currentStep.approvals.some(
|
||||
(approval) => approval.approverUserId === userId
|
||||
);
|
||||
|
||||
return !hasAlreadyApproved;
|
||||
};
|
||||
|
||||
export const RequestsTable = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentProject } = useProject();
|
||||
const { memberships } = useProjectPermission();
|
||||
const { user } = useUser();
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState(ApprovalRequestStatus.Pending);
|
||||
|
||||
const projectId = currentProject?.id || "";
|
||||
const userId = user?.id || "";
|
||||
const userGroups = memberships.map((el) => el.actorGroupId).filter(Boolean);
|
||||
|
||||
const { data: requests = [], isPending: isRequestsLoading } = useQuery(
|
||||
approvalRequestQuery.list({
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
projectId
|
||||
})
|
||||
);
|
||||
|
||||
const { page, perPage, setPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: getUserTablePreference("PamApprovalRequestTable", PreferenceKey.PerPage, 10)
|
||||
});
|
||||
|
||||
const filteredRequests = useMemo(() => {
|
||||
let filtered = requests;
|
||||
|
||||
// Apply search filter
|
||||
if (search) {
|
||||
filtered = filtered.filter(
|
||||
(request) =>
|
||||
request.requesterName?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.requesterEmail?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.justification?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
request.requestData.requestData.accountPath.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
return filtered
|
||||
.filter((req) => req.status === filter)
|
||||
.sort(
|
||||
(a, b) => (new Date(b.createdAt)?.getTime() || 0) - (new Date(a.createdAt)?.getTime() || 0)
|
||||
);
|
||||
}, [requests, search, filter]);
|
||||
|
||||
const paginatedRequests = useMemo(
|
||||
() => filteredRequests.slice(offset, offset + perPage),
|
||||
[filteredRequests, offset, perPage]
|
||||
);
|
||||
|
||||
const handleRowClick = (requestId: string) => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id,
|
||||
approvalRequestId: requestId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isTableFiltered = filter !== ApprovalRequestStatus.Pending;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter Requests"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-9.5 w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-0">
|
||||
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Pending);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Pending && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Open Requests
|
||||
</DropdownMenuItem>
|
||||
<DropdownSubMenu>
|
||||
<DropdownSubMenuTrigger
|
||||
iconPos="right"
|
||||
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
|
||||
>
|
||||
Closed Requests
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Filter by Status
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Approved);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Approved && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Approved
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Rejected);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Rejected && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Rejected
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setFilter(ApprovalRequestStatus.Expired);
|
||||
}}
|
||||
icon={
|
||||
filter === ApprovalRequestStatus.Expired && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Expired
|
||||
</DropdownMenuItem>
|
||||
</DropdownSubMenuContent>
|
||||
</DropdownSubMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search approval requests..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Requester</Th>
|
||||
<Th>Account Path</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Requested</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRequestsLoading && <TableSkeleton columns={5} innerKey="approval-requests" />}
|
||||
{!isRequestsLoading &&
|
||||
paginatedRequests.map((request) => {
|
||||
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
|
||||
const { accountPath, accessDuration } = request.requestData.requestData;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={request.id}
|
||||
className={twMerge(
|
||||
"cursor-pointer transition-colors hover:bg-mineshaft-700",
|
||||
needsApproval && "bg-primary/5 hover:bg-primary/10"
|
||||
)}
|
||||
onClick={() => handleRowClick(request.id)}
|
||||
>
|
||||
<Td>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-mineshaft-100">
|
||||
{request.requesterName || "Unknown"}
|
||||
</div>
|
||||
<div className="text-xs text-mineshaft-400">{request.requesterEmail}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div>
|
||||
<div className="text-sm text-mineshaft-200">{accountPath}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-200">{accessDuration}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="capitalize" variant={getStatusBadgeColor(request.status)}>
|
||||
{request.status.split("-").join(" ")}
|
||||
</Badge>
|
||||
{needsApproval && (
|
||||
<div className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="h-3 w-3" />
|
||||
<span>Approval Required</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
{formatDistance(new Date(request.createdAt), new Date(), {
|
||||
addSuffix: true
|
||||
})}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRequests.length) && (
|
||||
<Pagination
|
||||
count={filteredRequests.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isRequestsLoading && !filteredRequests?.length && (
|
||||
<EmptyState
|
||||
title="No approval requests found"
|
||||
icon={requests.length ? faSearch : faFileCircleQuestion}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { RequestsSection } from "./RequestsSection";
|
||||
export { RequestsTable } from "./RequestsTable";
|
||||
@@ -1,5 +1,65 @@
|
||||
import { PolicySection } from "./components";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ApprovalPolicyType, useDeleteApprovalPolicy } from "@app/hooks/api/approvalPolicies";
|
||||
|
||||
import { PoliciesTable } from "./components/PoliciesTable";
|
||||
import { PolicyModal } from "./components/PolicyModal";
|
||||
|
||||
export const PolicyTab = () => {
|
||||
return <PolicySection />;
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { mutateAsync: deleteApprovalPolicy } = useDeleteApprovalPolicy();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"policy",
|
||||
"deletePolicy"
|
||||
] as const);
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
const policyId = (popUp?.deletePolicy?.data as { policyId: string })?.policyId;
|
||||
if (!currentProject?.id) return;
|
||||
if (!policyId) return;
|
||||
|
||||
await deleteApprovalPolicy({
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
policyId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted policy",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpClose("deletePolicy");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Approval Policies</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("policy")}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
</div>
|
||||
<PoliciesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<PolicyModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="delete"
|
||||
title="Are you sure you want to delete this policy?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { User, Users } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { useGetWorkspaceUsers, useListWorkspaceGroups } from "@app/hooks/api";
|
||||
@@ -37,8 +39,6 @@ import {
|
||||
ApproverType
|
||||
} from "@app/hooks/api/approvalPolicies";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { User, Users } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ApprovalPolicyType, useDeleteApprovalPolicy } from "@app/hooks/api/approvalPolicies";
|
||||
|
||||
import { PoliciesTable } from "./PoliciesTable";
|
||||
import { PolicyModal } from "./PolicyModal";
|
||||
|
||||
export const PolicySection = () => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { mutateAsync: deleteApprovalPolicy } = useDeleteApprovalPolicy();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"policy",
|
||||
"deletePolicy"
|
||||
] as const);
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
const policyId = (popUp?.deletePolicy?.data as { policyId: string })?.policyId;
|
||||
if (!currentProject?.id) return;
|
||||
if (!policyId) return;
|
||||
|
||||
await deleteApprovalPolicy({
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
policyId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted policy",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpClose("deletePolicy");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Approval Policies</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("policy")}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
</div>
|
||||
<PoliciesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<PolicyModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="delete"
|
||||
title="Are you sure you want to delete this policy?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,2 @@
|
||||
export { PoliciesTable } from "./PoliciesTable";
|
||||
export { PolicyModal } from "./PolicyModal";
|
||||
export { PolicySection } from "./PolicySection";
|
||||
|
||||
Reference in New Issue
Block a user