mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3869 from Infisical/sequence-approval-policy-ui-additions
improvement(access-policies): Revamp approval sequence table display and access request modal
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -7,6 +9,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
|
icon?: IconDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GenericFieldLabel = ({
|
export const GenericFieldLabel = ({
|
||||||
@@ -14,11 +17,15 @@ export const GenericFieldLabel = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
truncate
|
truncate,
|
||||||
|
icon
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("min-w-0", className)}>
|
<div className={twMerge("min-w-0", className)}>
|
||||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
<div className="flex items-center gap-1.5">
|
||||||
|
{icon && <FontAwesomeIcon icon={icon} className="text-mineshaft-400" size="sm" />}
|
||||||
|
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||||
|
</div>
|
||||||
{children ? (
|
{children ? (
|
||||||
<p className={twMerge("text-sm text-mineshaft-100", truncate && "truncate")}>{children}</p>
|
<p className={twMerge("text-sm text-mineshaft-100", truncate && "truncate")}>{children}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+1
-3
@@ -247,9 +247,7 @@ export const AccessApprovalRequest = ({
|
|||||||
};
|
};
|
||||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||||
displayData = {
|
displayData = {
|
||||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
label: "Pending Additional Reviews",
|
||||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
|
||||||
}`,
|
|
||||||
type: "primary",
|
type: "primary",
|
||||||
icon: faClipboardCheck
|
icon: faClipboardCheck
|
||||||
};
|
};
|
||||||
|
|||||||
+201
-174
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faCheckCircle,
|
faBan,
|
||||||
faCircle,
|
faCheck,
|
||||||
faTriangleExclamation,
|
faHourglass,
|
||||||
faUsers,
|
faTriangleExclamation
|
||||||
faXmarkCircle
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
@@ -15,12 +14,10 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
GenericFieldLabel,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@@ -38,10 +35,22 @@ import { groupBy } from "@app/lib/fn/array";
|
|||||||
|
|
||||||
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||||
if (status === ApprovalStatus.APPROVED)
|
if (status === ApprovalStatus.APPROVED)
|
||||||
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
|
return (
|
||||||
|
<Badge variant="success" className="flex h-4 items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faCheck} size="xs" />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
if (status === ApprovalStatus.REJECTED)
|
if (status === ApprovalStatus.REJECTED)
|
||||||
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
|
return (
|
||||||
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
|
<Badge variant="danger" className="flex h-4 items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faBan} size="xs" />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReviewAccessRequestModal = ({
|
export const ReviewAccessRequestModal = ({
|
||||||
@@ -267,139 +276,160 @@ export const ReviewAccessRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="mb-2 mt-4 text-mineshaft-200">
|
<div className="mb-2 mt-4 text-mineshaft-200">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex flex-wrap gap-8">
|
||||||
<div>
|
<GenericFieldLabel label="Environment">{accessDetails.env}</GenericFieldLabel>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Environment</div>
|
<GenericFieldLabel truncate label="Secret Path">
|
||||||
<div>{accessDetails.env || "-"}</div>
|
{accessDetails.secretPath}
|
||||||
</div>
|
</GenericFieldLabel>
|
||||||
<div>
|
<GenericFieldLabel label="Access Type">{getAccessLabel()}</GenericFieldLabel>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Secret Path</div>
|
<GenericFieldLabel label="Permission">{requestedAccess}</GenericFieldLabel>
|
||||||
<div>{accessDetails.secretPath || "-"}</div>
|
{request.note && (
|
||||||
</div>
|
<GenericFieldLabel className="col-span-full" label="Note">
|
||||||
<div>
|
{request.note}
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Access Type</div>
|
</GenericFieldLabel>
|
||||||
<div>{getAccessLabel()}</div>
|
)}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Permission</div>
|
|
||||||
<div>{requestedAccess}</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Note</div>
|
|
||||||
<div>{request.note || "-"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
|
||||||
<div className="thin-scrollbar max-h-64 overflow-y-auto rounded p-2">
|
<div className="mt-4 flex items-center justify-between border-b-2 border-mineshaft-500 py-2">
|
||||||
{approverSequence?.approvers?.map((approver, index) => (
|
<span>Approvers</span>
|
||||||
<div
|
{approverSequence.isMyReviewInThisSequence &&
|
||||||
key={`approval-list-${index + 1}`}
|
request.status === ApprovalStatus.PENDING && (
|
||||||
className={twMerge(
|
<Badge variant="primary" className="h-min">
|
||||||
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
|
Awaiting Your Review
|
||||||
approverSequence?.currentSequence !== approver.sequence &&
|
</Badge>
|
||||||
!hasApproved &&
|
)}
|
||||||
"text-mineshaft-400"
|
</div>
|
||||||
)}
|
<div className="thin-scrollbar max-h-[40vh] overflow-y-auto rounded py-2">
|
||||||
>
|
{approverSequence?.approvers &&
|
||||||
<div>
|
approverSequence.approvers.map((approver, index) => {
|
||||||
|
const isInactive =
|
||||||
|
approverSequence?.currentSequence <
|
||||||
|
(approver.sequence ?? approverSequence.approvers!.length);
|
||||||
|
|
||||||
|
const isPending = approverSequence?.currentSequence === approver.sequence;
|
||||||
|
|
||||||
|
let StepComponent: ReactNode;
|
||||||
|
let BadgeComponent: ReactNode = null;
|
||||||
|
if (approver.hasRejected) {
|
||||||
|
StepComponent = (
|
||||||
|
<Badge
|
||||||
|
variant="danger"
|
||||||
|
className="flex h-6 min-w-6 items-center justify-center"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBan} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
BadgeComponent = <Badge variant="danger">Rejected</Badge>;
|
||||||
|
} else if (approver.hasApproved) {
|
||||||
|
StepComponent = (
|
||||||
|
<Badge
|
||||||
|
variant="success"
|
||||||
|
className="flex h-6 min-w-6 items-center justify-center"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
BadgeComponent = <Badge variant="success">Approved</Badge>;
|
||||||
|
} else if (isPending) {
|
||||||
|
StepComponent = (
|
||||||
|
<Badge
|
||||||
|
variant="primary"
|
||||||
|
className="flex h-6 min-w-6 items-center justify-center"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHourglass} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
BadgeComponent = <Badge variant="primary">Pending</Badge>;
|
||||||
|
} else {
|
||||||
|
StepComponent = (
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
isInactive
|
||||||
|
? "py-auto my-auto flex h-6 min-w-6 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
key={`approval-list-${index + 1}`}
|
||||||
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
|
className={twMerge("flex", isInactive && "opacity-50")}
|
||||||
approver.hasApproved && "border-green-400 text-green-400",
|
>
|
||||||
approver.hasRejected && "border-red-500 text-red-500"
|
{approverSequence.approvers!.length > 1 && (
|
||||||
|
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex-grow border-mineshaft-600",
|
||||||
|
index !== 0 && "border-r"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{StepComponent}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex-grow border-mineshaft-600",
|
||||||
|
index < approverSequence.approvers!.length - 1 && "border-r"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||||
{index + 1}
|
<GenericFieldLabel className="col-span-2" label="Users">
|
||||||
</div>
|
{approver?.user
|
||||||
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
?.map(
|
||||||
<div
|
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||||
className={twMerge(
|
)
|
||||||
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
|
.join(", ")}
|
||||||
approver.hasApproved && "border-green-400",
|
</GenericFieldLabel>
|
||||||
approver.hasRejected && "border-red-500"
|
<GenericFieldLabel className="col-span-2" label="Groups">
|
||||||
)}
|
{approver?.group
|
||||||
/>
|
?.map(
|
||||||
)}
|
(el) =>
|
||||||
{index !== 0 && (
|
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||||
<div
|
)
|
||||||
className={twMerge(
|
.join(", ")}
|
||||||
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
|
</GenericFieldLabel>
|
||||||
approver.hasApproved && "border-green-400",
|
<GenericFieldLabel label="Approvals Required">
|
||||||
approver.hasRejected && "border-red-500"
|
<div className="flex items-center">
|
||||||
)}
|
<span className="mr-2">{approver.approvals}</span>
|
||||||
/>
|
{BadgeComponent && (
|
||||||
)}
|
<Tooltip
|
||||||
</div>
|
className="max-w-lg"
|
||||||
<div className="grid flex-grow grid-cols-3">
|
content={
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||||
<div>
|
<div className="thin-scrollbar flex max-h-64 flex-col divide-y divide-mineshaft-500 overflow-y-auto rounded">
|
||||||
{approver?.user
|
{approver.reviewers.map((el, idx) => (
|
||||||
?.map(
|
<div
|
||||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
key={`reviewer-${idx + 1}`}
|
||||||
)
|
className="flex items-center gap-2 px-2 py-2 text-sm"
|
||||||
.join(",") || "-"}
|
>
|
||||||
</div>
|
<div className="flex-1">{el.username}</div>
|
||||||
</div>
|
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||||
<div>
|
</div>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
))}
|
||||||
<div>
|
</div>
|
||||||
{approver?.group
|
|
||||||
?.map(
|
|
||||||
(el) =>
|
|
||||||
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
|
||||||
)
|
|
||||||
.join(",") || "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
|
||||||
<div>{approver.approvals || "-"}</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-16">
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<FontAwesomeIcon icon={faUsers} />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent hideCloseBtn className="pt-3">
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
|
||||||
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
|
||||||
{approver.reviewers.map((el, idx) => (
|
|
||||||
<div
|
|
||||||
key={`reviewer-${idx + 1}`}
|
|
||||||
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
|
|
||||||
>
|
|
||||||
<div className="flex-grow">{el.username}</div>
|
|
||||||
<Tooltip
|
|
||||||
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
|
||||||
>
|
|
||||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
}
|
||||||
</div>
|
>
|
||||||
</div>
|
<div>{BadgeComponent}</div>
|
||||||
</PopoverContent>
|
</Tooltip>
|
||||||
</Popover>
|
)}
|
||||||
|
</div>
|
||||||
|
</GenericFieldLabel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{approverSequence.isMyReviewInThisSequence &&
|
|
||||||
request.status === ApprovalStatus.PENDING && (
|
|
||||||
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
|
||||||
Awaiting review from you.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{shouldBlockRequestActions ? (
|
{shouldBlockRequestActions ? (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
"mt-2 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||||
isReviewedByMe && "border-l-green-400",
|
isReviewedByMe && "border-l-green-400",
|
||||||
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
||||||
hasRejected && "border-l-red-500"
|
hasRejected && "border-l-red-500"
|
||||||
@@ -409,41 +439,6 @@ export const ReviewAccessRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-x-2">
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading === "approved"}
|
|
||||||
isDisabled={
|
|
||||||
Boolean(isLoading) ||
|
|
||||||
(!(
|
|
||||||
request.isApprover &&
|
|
||||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
|
||||||
) &&
|
|
||||||
!bypassApproval)
|
|
||||||
}
|
|
||||||
onClick={() => handleReview("approved")}
|
|
||||||
className="mt-4"
|
|
||||||
size="sm"
|
|
||||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
|
||||||
>
|
|
||||||
Approve Request
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading === "rejected"}
|
|
||||||
isDisabled={
|
|
||||||
!!isLoading ||
|
|
||||||
(!(
|
|
||||||
request.isApprover &&
|
|
||||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
|
||||||
) &&
|
|
||||||
!bypassApproval)
|
|
||||||
}
|
|
||||||
onClick={() => handleReview("rejected")}
|
|
||||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Reject Request
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isSoftEnforcement &&
|
{isSoftEnforcement &&
|
||||||
request.isRequestedByCurrentUser &&
|
request.isRequestedByCurrentUser &&
|
||||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||||
@@ -453,11 +448,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||||
isChecked={bypassApproval}
|
isChecked={bypassApproval}
|
||||||
id="byPassApproval"
|
id="byPassApproval"
|
||||||
checkIndicatorBg="text-white"
|
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
|
||||||
className={twMerge(
|
|
||||||
"mr-2",
|
|
||||||
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span className="text-xs text-red">
|
<span className="text-xs text-red">
|
||||||
Approve without waiting for requirements to be met (bypass policy
|
Approve without waiting for requirements to be met (bypass policy
|
||||||
@@ -481,6 +472,42 @@ export const ReviewAccessRequestModal = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "approved"}
|
||||||
|
isDisabled={
|
||||||
|
Boolean(isLoading) ||
|
||||||
|
(!(
|
||||||
|
request.isApprover &&
|
||||||
|
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||||
|
) &&
|
||||||
|
!bypassApproval)
|
||||||
|
}
|
||||||
|
onClick={() => handleReview("approved")}
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||||
|
>
|
||||||
|
Approve Request
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "rejected"}
|
||||||
|
isDisabled={
|
||||||
|
!!isLoading ||
|
||||||
|
(!(
|
||||||
|
request.isApprover &&
|
||||||
|
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||||
|
) &&
|
||||||
|
!bypassApproval)
|
||||||
|
}
|
||||||
|
onClick={() => handleReview("rejected")}
|
||||||
|
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Reject Request
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
-19
@@ -1,5 +1,12 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faClipboardCheck,
|
||||||
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
|
faTrash,
|
||||||
|
faUser,
|
||||||
|
faUserGroup
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -179,26 +186,40 @@ export const ApprovalPolicyRow = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
<div className="border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||||
{labels?.map((el, index) => (
|
{labels?.map((el, index) => (
|
||||||
<div
|
<div key={`approval-list-${index + 1}`} className="flex">
|
||||||
key={`approval-list-${index + 1}`}
|
{labels.length > 1 && (
|
||||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||||
>
|
<div
|
||||||
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
className={twMerge(
|
||||||
<div>{index + 1}</div>
|
"flex-grow border-mineshaft-600",
|
||||||
</div>
|
index !== 0 && "border-r"
|
||||||
{index !== labels.length - 1 && (
|
)}
|
||||||
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
/>
|
||||||
|
{labels.length > 1 && (
|
||||||
|
<Badge className="my-auto flex h-5 w-min min-w-5 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200">
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex-grow border-mineshaft-600",
|
||||||
|
index < labels.length - 1 && "border-r"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{index !== 0 && (
|
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||||
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||||
)}
|
{el.userLabels}
|
||||||
|
</GenericFieldLabel>
|
||||||
<div className="grid flex-grow grid-cols-3">
|
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
||||||
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
{el.groupLabels}
|
||||||
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
<GenericFieldLabel icon={faClipboardCheck} label="Approvals Required">
|
||||||
|
{el.approvals}
|
||||||
|
</GenericFieldLabel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user