feat: improved ui for request detail table

This commit is contained in:
=
2025-12-07 14:39:11 +05:30
parent 2a783d3057
commit f4cf0c3545
9 changed files with 573 additions and 83 deletions

View File

@@ -359,6 +359,10 @@ export const ROUTE_PATHS = Object.freeze({
"/organizations/$orgId/projects/pam/$projectId/sessions",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/"
),
ApprovalRequestDetailPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId"
),
PamSessionByIDPage: setRoute(
"/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId"

View File

@@ -14,11 +14,16 @@ export enum ApprovalRequestStepStatus {
Rejected = "rejected"
}
export enum ApprovalRequestApprovalDecision {
Approved = "approved",
Rejected = "rejected"
}
export type ApprovalRequestApproval = {
id: string;
requestStepId: string;
approverId: string;
approverType: ApproverType;
stepId: string;
approverUserId: string;
decision: ApprovalRequestApprovalDecision.Approved;
comment?: string | null;
createdAt: string;
updatedAt: string;

View File

@@ -78,6 +78,7 @@ export type TUserMembership = {
scope: string;
scopeOrgId: string;
actorUserId: string;
actorGroupId: string;
};
export type TProjectMembership = {

View File

@@ -1,3 +1,90 @@
export const ApprovalRequestDetailPage = () => {
return <div>Request detail</div>;
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, useNavigate, useParams } from "@tanstack/react-router";
import { ContentLoader, EmptyState, PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization, useProject } from "@app/context";
import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
import { approvalRequestQuery } from "@app/hooks/api/approvalRequests";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ApprovalStepsSection, RequestActionsSection, RequestDetailsSection } from "./components";
const PageContent = () => {
const { approvalRequestId } = useParams({
from: ROUTE_PATHS.Pam.ApprovalRequestDetailPage.id
});
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const { data: request, isPending } = useQuery(
approvalRequestQuery.getById({
policyType: ApprovalPolicyType.PamAccess,
requestId: approvalRequestId
})
);
if (isPending) {
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader />
</div>
);
}
if (!request) {
return (
<div className="flex h-full w-full items-center justify-center px-20">
<EmptyState
className="max-w-2xl rounded-md text-center"
icon={faBan}
title={`Could not find approval request with ID ${approvalRequestId}`}
/>
</div>
);
}
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/organizations/$orgId/projects/pam/$projectId/approvals"
params={{ orgId: currentOrg.id, projectId: currentProject.id }}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Approvals List
</Link>
<PageHeader
scope={ProjectType.PAM}
title="Approval Request"
description={`Request to access account ${request.requestData.requestData.accountPath} for ${request.requestData.requestData.requestDurationSeconds} by ${request.requesterName || "Unknown"}`}
/>
<div className="flex justify-center gap-4">
<div className="flex w-96 flex-col gap-4">
<RequestDetailsSection request={request} />
<RequestActionsSection request={request} />
</div>
<div className="flex flex-1 flex-col gap-4">
<ApprovalStepsSection request={request} />
</div>
</div>
</div>
</div>
);
};
export const ApprovalRequestDetailPage = () => {
return (
<>
<Helmet>
<title>Approval Request | Infisical</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<PageContent />
</>
);
};

View File

@@ -0,0 +1,187 @@
import { useMemo } from "react";
import { faCheck, faCheckCircle, faClock, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { User, Users } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v3";
import { useProject } from "@app/context";
import { useGetWorkspaceUsers, useListWorkspaceGroups } from "@app/hooks/api";
import { ApproverType } from "@app/hooks/api/approvalPolicies";
import {
ApprovalRequestApproval,
ApprovalRequestStepStatus,
TApprovalRequest
} from "@app/hooks/api/approvalRequests";
import { ApprovalRequestApprovalDecision } from "@app/hooks/api/approvalRequests/types";
type Props = {
request: TApprovalRequest;
};
const getMemberLabel = (member: {
user: { username: string; email: string; firstName: string; lastName: string };
}) => {
const { user } = member;
if (user.firstName || user.lastName) {
return `${user.firstName || ""} ${user.lastName || ""}`.trim();
}
return user.username || user.email;
};
export const ApprovalStepsSection = ({ request }: Props) => {
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data: members = [] } = useGetWorkspaceUsers(projectId, true);
const { data: groups = [] } = useListWorkspaceGroups(projectId);
const getApproverLabel = useMemo(
() => (approverId: string, approverType: ApproverType) => {
if (approverType === ApproverType.User) {
const member = members?.find((m) => m.user.id === approverId);
if (member) return getMemberLabel(member);
} else if (approverType === ApproverType.Group) {
const group = groups?.find(({ group: g }) => g.id === approverId);
if (group) return group.group.name;
}
return approverId;
},
[members, groups]
);
const getApprovalLabel = (approval: ApprovalRequestApproval) => {
const member = members?.find((m) => m.user.id === approval.approverUserId);
if (member) return getMemberLabel(member);
return approval.approverUserId;
};
const getStepStatusIcon = (status: ApprovalRequestStepStatus) => {
switch (status) {
case ApprovalRequestStepStatus.Approved:
return <FontAwesomeIcon icon={faCheckCircle} className="text-green-500" />;
case ApprovalRequestStepStatus.InProgress:
return <FontAwesomeIcon icon={faClock} className="text-yellow-500" />;
case ApprovalRequestStepStatus.Pending:
return <div className="h-4 w-4 rounded-full border-2 border-mineshaft-400" />;
case ApprovalRequestStepStatus.Rejected:
return <FontAwesomeIcon icon={faXmark} className="text-red-500" />;
default:
return null;
}
};
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="mb-2 flex items-center justify-between border-b border-mineshaft-500 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Approval Workflow</h3>
</div>
<div className="space-y-6">
{request.steps.map((step, index) => (
<div key={step.id} className="relative">
{/* Connector line */}
{index < request.steps.length - 1 && (
<div className="absolute top-8 left-2 h-full w-0.5 bg-mineshaft-600" />
)}
<div className="relative flex gap-3">
<div className="relative top-3 z-10 flex h-4 w-4 items-center justify-center bg-mineshaft-900">
{getStepStatusIcon(step.status)}
</div>
<div className="flex-1 pb-4">
<div
className={twMerge(
"rounded-lg border bg-mineshaft-800",
step.status === ApprovalRequestStepStatus.InProgress
? "border-yellow-500/40"
: "border-mineshaft-600"
)}
>
<div
className={twMerge(
"mb-2 flex items-center justify-between p-2",
step.status === ApprovalRequestStepStatus.InProgress
? "bg-yellow-500/20"
: "bg-mineshaft-700"
)}
>
<h4 className="font-medium text-mineshaft-100">
Step {index + 1}
{step.status === ApprovalRequestStepStatus.InProgress && (
<span className="ml-2 text-xs text-yellow-500">(Current Step)</span>
)}
</h4>
<Badge
variant={
step.status === ApprovalRequestStepStatus.InProgress ? "project" : "neutral"
}
className="capitalize"
>
{step.status.split("-").join(" ")}
</Badge>
</div>
<div className="space-y-3 p-2 pb-4 text-bunker-200">
<div className="flex items-center space-x-2 text-sm">
<div>Approvals Required:</div>
<div>{step.requiredApprovals}</div>
</div>
<div className="text-sm">
<div className="mb-2">Approvers</div>
<div className="flex flex-wrap gap-1.5">
{step.approvers.map((approver) => (
<Badge variant="neutral">
{approver.type === ApproverType.Group ? <User /> : <Users />}
{getApproverLabel(approver.id, approver.type)}
</Badge>
))}
</div>
</div>
{step.approvals.length > 0 && (
<div className="mt-3 space-y-2 border-t border-mineshaft-600 pt-3">
<p className="text-xs font-medium text-mineshaft-300">Approvals Given:</p>
{step.approvals.map((approval) => (
<div
key={approval.id}
className="flex items-start justify-between rounded bg-mineshaft-900/50 p-2"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon
icon={
approval.decision === ApprovalRequestApprovalDecision.Approved
? faCheck
: faXmark
}
className={
approval.decision === ApprovalRequestApprovalDecision.Approved
? "text-green-500"
: "text-red-500"
}
/>
<div>
<p className="text-xs font-medium text-mineshaft-100">
{getApprovalLabel(approval)}
</p>
<p className="text-xs text-mineshaft-400">
{format(new Date(approval.createdAt), "MMM dd, yyyy hh:mm aaa")}
</p>
</div>
</div>
{approval.comment && (
<p className="max-w-xs text-xs text-mineshaft-400 italic">
&quot;{approval.comment}&quot;
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,168 @@
import { useState } from "react";
import { faCheck, faMagnifyingGlass, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
FormLabel,
Popover,
PopoverContent,
PopoverTrigger,
TextArea
} from "@app/components/v2";
import { useProjectPermission, useUser } from "@app/context";
import { ApproverType } from "@app/hooks/api/approvalPolicies";
import {
ApprovalRequestStatus,
ApprovalRequestStepStatus,
TApprovalRequest,
useApproveApprovalRequest,
useRejectApprovalRequest
} from "@app/hooks/api/approvalRequests";
type Props = {
request: TApprovalRequest;
};
export const RequestActionsSection = ({ request }: Props) => {
const { user } = useUser();
const { memberships } = useProjectPermission();
const [comment, setComment] = useState("");
const userId = user?.id || "";
const userGroups = memberships.map((el) => el.actorGroupId).filter(Boolean);
const { mutateAsync: approveRequest, isPending: isApproving } = useApproveApprovalRequest();
const { mutateAsync: rejectRequest, isPending: isRejecting } = useRejectApprovalRequest();
// Check if the request is actionable
if (request.status !== ApprovalRequestStatus.Pending) {
return null;
}
// Find the current active step
const currentStep = request.steps.find(
(step) => step.status === ApprovalRequestStepStatus.InProgress
);
if (!currentStep) {
return null;
}
// Check if user is an approver in the current step
const isApprover = currentStep.approvers.some((approver) =>
approver.type === ApproverType.User ? approver.id === userId : userGroups.includes(approver.id)
);
if (!isApprover) {
return null;
}
// Check if user has already approved/rejected
const hasAlreadyActed = currentStep.approvals.some(
(approval) => approval.approverUserId === userId
);
if (hasAlreadyActed) {
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<p className="text-sm text-mineshaft-300">
You have already provided your approval for this request.
</p>
</div>
);
}
const handleApprove = async () => {
try {
await approveRequest({
policyType: request.type,
requestId: request.id,
comment: comment || undefined
});
createNotification({
text: "Request approved successfully",
type: "success"
});
} finally {
setComment("");
}
};
const handleReject = async () => {
try {
await rejectRequest({
policyType: request.type,
requestId: request.id,
comment: comment || undefined
});
createNotification({
text: "Request rejected successfully",
type: "success"
});
} finally {
setComment("");
}
};
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-4 py-3">
<div className="flex items-center justify-between border-b border-yellow-500/30 pb-2">
<h3 className="font-medium text-mineshaft-100">Action Required</h3>
<span className="text-xs text-yellow-500">Awaiting Your Approval</span>
</div>
<div className="space-y-4">
<p className="text-sm text-mineshaft-300">
You are an approver for the current step. Please review the request details and provide
your decision.
</p>
<Popover>
<PopoverTrigger>
<Button
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
className="px-2 py-1"
>
Review
</Button>
</PopoverTrigger>
<PopoverContent align="start" title="Finish your review" className="w-96">
<FormLabel label="Finish your review" />
<TextArea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Add a comment about your decision..."
rows={3}
reSize="vertical"
className="mt-1 mb-4"
/>
<div className="flex gap-3">
<Button
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faCheck} />}
onClick={handleApprove}
isLoading={isApproving}
isDisabled={isRejecting}
className="px-2 py-1"
>
Approve Request
</Button>
<Button
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faXmark} />}
onClick={handleReject}
isLoading={isRejecting}
isDisabled={isApproving}
className="px-2 py-1"
>
Reject Request
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
);
};

View File

@@ -0,0 +1,78 @@
import { format } from "date-fns";
import { Badge } from "@app/components/v3";
import { GenericFieldLabel } from "@app/components/v2";
import { ApprovalRequestStatus, TApprovalRequest } from "@app/hooks/api/approvalRequests";
type Props = {
request: TApprovalRequest;
};
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 formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""}`;
}
return `${minutes}m`;
};
export const RequestDetailsSection = ({ request }: Props) => {
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-500 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Request Details</h3>
</div>
<div>
<div className="space-y-3">
<GenericFieldLabel label="Status">
<Badge className="capitalize" variant={getStatusBadgeColor(request.status)}>
{request.status}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="Requester Name">
{request.requesterName || "Unknown"}
</GenericFieldLabel>
<GenericFieldLabel label="Requester Email">{request.requesterEmail}</GenericFieldLabel>
<GenericFieldLabel label="Account Path">{accountPath}</GenericFieldLabel>
<GenericFieldLabel label="Duration">
{formatDuration(requestDurationSeconds)}
</GenericFieldLabel>
{request.justification && (
<GenericFieldLabel label="Justification">
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{request.justification}
</p>
</GenericFieldLabel>
)}
<GenericFieldLabel label="Requested At">
{format(new Date(request.createdAt), "yyyy-MM-dd, hh:mm aaa")}
</GenericFieldLabel>
{request.expiresAt && (
<GenericFieldLabel label="Expires">
{format(new Date(request.expiresAt), "yyyy-MM-dd, hh:mm aaa")}
</GenericFieldLabel>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export { ApprovalStepsSection } from "./ApprovalStepsSection";
export { RequestActionsSection } from "./RequestActionsSection";
export { RequestDetailsSection } from "./RequestDetailsSection";

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import {
faCheckCircle,
faChevronRight,
@@ -37,7 +37,8 @@ import {
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useOrganization, useProject, useUser } from "@app/context";
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 {
@@ -47,11 +48,6 @@ import {
TApprovalRequest
} from "@app/hooks/api/approvalRequests";
type Filter = {
status: "open" | "closed";
closedStatuses: ApprovalRequestStatus[];
};
const getStatusBadgeColor = (status: ApprovalRequestStatus) => {
switch (status) {
case ApprovalRequestStatus.Pending:
@@ -77,21 +73,25 @@ const formatDuration = (seconds: number) => {
return `${minutes}m`;
};
const checkIfUserNeedsToApprove = (request: TApprovalRequest, userId: string): boolean => {
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
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.approverId === userId
(approval) => approval.approverUserId === userId
);
return !hasAlreadyApproved;
@@ -101,15 +101,14 @@ 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<Filter>({
status: "open",
closedStatuses: []
});
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({
@@ -119,7 +118,7 @@ export const RequestsTable = () => {
);
const { page, perPage, setPage, setPerPage, offset } = usePagination("", {
initPerPage: 10
initPerPage: getUserTablePreference("PamApprovalRequestTable", PreferenceKey.PerPage, 10)
});
const filteredRequests = useMemo(() => {
@@ -137,24 +136,11 @@ export const RequestsTable = () => {
);
}
// Apply open/closed filter
if (filter.status === "open") {
filtered = filtered.filter((req) => req.status === ApprovalRequestStatus.Pending);
} else {
const closedStatuses = [
ApprovalRequestStatus.Approved,
ApprovalRequestStatus.Rejected,
ApprovalRequestStatus.Expired
];
filtered = filtered.filter((req) => closedStatuses.includes(req.status));
// Apply specific closed status filter
if (filter.closedStatuses.length > 0) {
filtered = filtered.filter((req) => filter.closedStatuses.includes(req.status));
}
}
return filtered;
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(
@@ -173,30 +159,7 @@ export const RequestsTable = () => {
});
};
const handleStatusFilterToggle = useCallback(
() =>
setFilter((state) => ({
...state,
status: state.status === "open" ? "closed" : "open",
closedStatuses: []
})),
[]
);
const handleClosedStatusToggle = useCallback(
(status: ApprovalRequestStatus) =>
setFilter((state) => {
const closedStatuses = state.closedStatuses || [];
if (closedStatuses.includes(status)) {
return { ...state, closedStatuses: closedStatuses.filter((s) => s !== status) };
}
return { ...state, closedStatuses: [...closedStatuses, status] };
}),
[]
);
const isTableFiltered = filter.status === "closed" || filter.closedStatuses.length > 0;
const isTableFiltered = filter !== ApprovalRequestStatus.Pending;
return (
<div>
@@ -220,9 +183,11 @@ export const RequestsTable = () => {
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleStatusFilterToggle();
setFilter(ApprovalRequestStatus.Pending);
}}
icon={filter.status === "open" && <FontAwesomeIcon icon={faCheckCircle} />}
icon={
filter === ApprovalRequestStatus.Pending && <FontAwesomeIcon icon={faCheckCircle} />
}
iconPos="right"
>
Open Requests
@@ -241,10 +206,10 @@ export const RequestsTable = () => {
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleClosedStatusToggle(ApprovalRequestStatus.Approved);
setFilter(ApprovalRequestStatus.Approved);
}}
icon={
filter.closedStatuses.includes(ApprovalRequestStatus.Approved) && (
filter === ApprovalRequestStatus.Approved && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
@@ -255,10 +220,10 @@ export const RequestsTable = () => {
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleClosedStatusToggle(ApprovalRequestStatus.Rejected);
setFilter(ApprovalRequestStatus.Rejected);
}}
icon={
filter.closedStatuses.includes(ApprovalRequestStatus.Rejected) && (
filter === ApprovalRequestStatus.Rejected && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
@@ -269,10 +234,10 @@ export const RequestsTable = () => {
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleClosedStatusToggle(ApprovalRequestStatus.Expired);
setFilter(ApprovalRequestStatus.Expired);
}}
icon={
filter.closedStatuses.includes(ApprovalRequestStatus.Expired) && (
filter === ApprovalRequestStatus.Expired && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
@@ -296,7 +261,7 @@ export const RequestsTable = () => {
<THead>
<Tr>
<Th>Requester</Th>
<Th>Resource / Path</Th>
<Th>Account Path</Th>
<Th>Duration</Th>
<Th>Status</Th>
<Th>Requested</Th>
@@ -306,9 +271,8 @@ export const RequestsTable = () => {
{isRequestsLoading && <TableSkeleton columns={5} innerKey="approval-requests" />}
{!isRequestsLoading &&
paginatedRequests.map((request) => {
const needsApproval = checkIfUserNeedsToApprove(request, userId);
const { resourceId, accountPath, requestDurationSeconds } =
request.requestData.requestData;
const needsApproval = checkIfUserNeedsToApprove(request, userId, userGroups);
const { accountPath, requestDurationSeconds } = request.requestData.requestData;
return (
<Tr
@@ -330,9 +294,6 @@ export const RequestsTable = () => {
<Td>
<div>
<div className="text-sm text-mineshaft-200">{accountPath}</div>
<div className="font-mono text-xs text-mineshaft-400">
{resourceId.slice(0, 8)}...
</div>
</div>
</Td>
<Td>
@@ -376,11 +337,7 @@ export const RequestsTable = () => {
)}
{!isRequestsLoading && !filteredRequests?.length && (
<EmptyState
title={
requests.length
? "No approval requests match search..."
: "No approval requests found"
}
title="No approval requests found"
icon={requests.length ? faSearch : faFileCircleQuestion}
/>
)}