mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 20:28:17 +00:00
feat: ui changes for approval to work
This commit is contained in:
-3
@@ -188,9 +188,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
<Th>Name</Th>
|
||||
<Th>Environment</Th>
|
||||
<Th>Secret Path</Th>
|
||||
<Th className="w-[18%]">Eligible Approvers</Th>
|
||||
<Th className="w-[18%]">Eligible Group Approvers</Th>
|
||||
<Th>Approval Required</Th>
|
||||
<Th>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
|
||||
+337
-83
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -8,12 +11,15 @@ import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
Switch,
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
@@ -28,6 +34,7 @@ import {
|
||||
useUpdateAccessApprovalPolicy
|
||||
} from "@app/hooks/api/accessApproval";
|
||||
import {
|
||||
Approver,
|
||||
ApproverType,
|
||||
BypasserType,
|
||||
TAccessApprovalPolicy
|
||||
@@ -68,10 +75,28 @@ const formSchema = z
|
||||
.default([]),
|
||||
policyType: z.nativeEnum(PolicyType),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
sequenceApprovers: z
|
||||
.object({
|
||||
user: z
|
||||
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
group: z
|
||||
.object({ type: z.literal(ApproverType.Group), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
.optional()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
||||
if (
|
||||
data.policyType === PolicyType.ChangePolicy &&
|
||||
!(data.groupApprovers.length || data.userApprovers.length)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
path: ["userApprovers"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -95,6 +120,9 @@ export const AccessPolicyForm = ({
|
||||
projectSlug,
|
||||
editValues
|
||||
}: Props) => {
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
const modalContainer = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -103,6 +131,7 @@ export const AccessPolicyForm = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
// @ts-expect-error due to collision of approver type
|
||||
values: editValues
|
||||
? {
|
||||
...editValues,
|
||||
@@ -124,15 +153,47 @@ export const AccessPolicyForm = ({
|
||||
?.filter((bypasser) => bypasser.type === BypasserType.Group)
|
||||
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
||||
approvals: editValues?.approvals,
|
||||
allowedSelfApprovals: editValues?.allowedSelfApprovals
|
||||
allowedSelfApprovals: editValues?.allowedSelfApprovals,
|
||||
sequenceApprovers: editValues.approvers
|
||||
?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0))
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
if (acc.length > 1 && acc[acc.length - 1].sequence === curr.sequence) {
|
||||
acc[acc.length - 1][curr.type]?.push(curr);
|
||||
return acc;
|
||||
}
|
||||
const approvals = curr.approvals || editValues.approvals;
|
||||
acc.push(
|
||||
curr.type === ApproverType.User
|
||||
? {
|
||||
user: [curr],
|
||||
group: [],
|
||||
sequence: 1,
|
||||
approvals
|
||||
}
|
||||
: { group: [curr], user: [], sequence: 1, approvals }
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[] as { user: Approver[]; group: Approver[]; sequence?: number; approvals: number }[]
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
: undefined,
|
||||
defaultValues: {
|
||||
sequenceApprovers: [{ approvals: 1 }]
|
||||
}
|
||||
});
|
||||
const sequenceApproversFieldArray = useFieldArray({
|
||||
control,
|
||||
name: "sequenceApprovers"
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const isEditMode = Boolean(editValues);
|
||||
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !isEditMode) reset({});
|
||||
@@ -157,6 +218,7 @@ export const AccessPolicyForm = ({
|
||||
userApprovers,
|
||||
groupBypassers,
|
||||
userBypassers,
|
||||
sequenceApprovers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId) return;
|
||||
@@ -175,7 +237,15 @@ export const AccessPolicyForm = ({
|
||||
} else {
|
||||
await createAccessApprovalPolicy({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||
approvers.user
|
||||
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
||||
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||
),
|
||||
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||
stepNumber: index + 1,
|
||||
numberOfApprovals: el.approvals
|
||||
})),
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
@@ -201,6 +271,7 @@ export const AccessPolicyForm = ({
|
||||
groupApprovers,
|
||||
userBypassers,
|
||||
groupBypassers,
|
||||
sequenceApprovers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId || !projectSlug) return;
|
||||
@@ -221,7 +292,15 @@ export const AccessPolicyForm = ({
|
||||
await updateAccessApprovalPolicy({
|
||||
id: editValues?.id,
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
approvers: sequenceApprovers?.flatMap((approvers, index) =>
|
||||
approvers.user
|
||||
.map((el) => ({ ...el, sequence: index + 1 }) as Approver)
|
||||
.concat(approvers.group.map((el) => ({ ...el, sequence: index + 1 })))
|
||||
),
|
||||
approvalsRequired: sequenceApprovers?.map((el, index) => ({
|
||||
stepNumber: index + 1,
|
||||
numberOfApprovals: el.approvals
|
||||
})),
|
||||
bypassers: bypassers.length > 0 ? bypassers : undefined,
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
@@ -285,16 +364,46 @@ export const AccessPolicyForm = ({
|
||||
[groups]
|
||||
);
|
||||
|
||||
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||
setDraggedItem(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
setDragOverItem(index);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (draggedItem === null || dragOverItem === null || draggedItem === dragOverItem) {
|
||||
setDraggedItem(null);
|
||||
setDragOverItem(null);
|
||||
return;
|
||||
}
|
||||
|
||||
sequenceApproversFieldArray.move(draggedItem, dragOverItem);
|
||||
|
||||
setDraggedItem(null);
|
||||
setDragOverItem(null);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedItem(null);
|
||||
setDragOverItem(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
className="max-w-3xl"
|
||||
bodyClassName="overflow-visible"
|
||||
ref={modalContainer}
|
||||
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
||||
>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="grid grid-cols-2 gap-x-3">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="policyType"
|
||||
@@ -306,6 +415,7 @@ export const AccessPolicyForm = ({
|
||||
isError={Boolean(error)}
|
||||
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isEditMode}
|
||||
@@ -324,25 +434,30 @@ export const AccessPolicyForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="approvals"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Minimum Approvals Required"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isAccessPolicyType && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="approvals"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Min. Approvals Required"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
@@ -351,6 +466,7 @@ export const AccessPolicyForm = ({
|
||||
label="Policy Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
@@ -366,6 +482,7 @@ export const AccessPolicyForm = ({
|
||||
label="Secret Path"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
@@ -400,62 +517,199 @@ export const AccessPolicyForm = ({
|
||||
Select members or groups that are allowed to approve requests from this policy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="userApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
options={memberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
{isAccessPolicyType ? (
|
||||
<>
|
||||
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded">
|
||||
{sequenceApproversFieldArray.fields.map((el, index) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0",
|
||||
dragOverItem === index ? "border-2 border-blue-400" : "",
|
||||
draggedItem === index ? "opacity-50" : ""
|
||||
)}
|
||||
key={el.id}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Tag>Step {index + 1}</Tag>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="inline text-xs text-mineshaft-400">Min. Approvals</div>
|
||||
<div className="mr-2 w-20 border-r border-mineshaft-400 pr-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`sequenceApprovers.${index}.approvals` as const}
|
||||
defaultValue={1}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
size="xs"
|
||||
min={1}
|
||||
onChange={(val) => field.onChange(parseInt(val.target.value, 10))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip content="Remove step">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
variant="plain"
|
||||
onClick={() => sequenceApproversFieldArray.remove(index)}
|
||||
className="text-red-500 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Drag to reorder permission">
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="mr-2 cursor-move text-gray-400 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGripVertical} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`sequenceApprovers.${index}.user` as const}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPortalTarget={modalContainer.current}
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
options={memberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
|
||||
if (!member) return option.id;
|
||||
if (!member) return option.id;
|
||||
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`sequenceApprovers.${index}.group` as const}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-grow"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPortalTarget={modalContainer.current}
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
options={groupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||
option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
sequenceApproversFieldArray.append({
|
||||
approvals: 1,
|
||||
user: [],
|
||||
group: []
|
||||
})
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
options={groupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
Add Step
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="userApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members..."
|
||||
options={memberOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => {
|
||||
const member = members?.find((m) => m.user.id === option.id);
|
||||
|
||||
if (!member) return option.id;
|
||||
|
||||
return getMemberLabel(member);
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="groupApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-1/2"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups..."
|
||||
options={groupOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) =>
|
||||
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||
option.id
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedSelfApprovals"
|
||||
|
||||
+141
-100
@@ -10,7 +10,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -18,6 +17,7 @@ import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { policyDetails } from "@app/helpers/policies";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { Approver } from "@app/hooks/api/accessApproval/types";
|
||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
||||
@@ -53,113 +53,154 @@ export const ApprovalPolicyRow = ({
|
||||
onEdit,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle();
|
||||
|
||||
const labels = useMemo(() => {
|
||||
const usersInPolicy = policy.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id);
|
||||
const sortedSteps = policy.approvers?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0));
|
||||
const entityInSameSequence = sortedSteps?.reduce(
|
||||
(acc, curr) => {
|
||||
if (acc.length > 1 && acc[acc.length - 1].sequence === curr.sequence) {
|
||||
acc[acc.length - 1][curr.type]?.push(curr);
|
||||
return acc;
|
||||
}
|
||||
const approvals = curr.approvals || policy.approvals;
|
||||
acc.push(
|
||||
curr.type === ApproverType.User
|
||||
? { user: [curr], group: [], sequence: 1, approvals }
|
||||
: { group: [curr], user: [], sequence: 1, approvals }
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[] as { user: Approver[]; group: Approver[]; sequence?: number; approvals: number }[]
|
||||
);
|
||||
|
||||
const groupsInPolicy = policy.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id);
|
||||
|
||||
const memberLabels = usersInPolicy?.length
|
||||
? members
|
||||
.filter((member) => usersInPolicy?.includes(member.user.id))
|
||||
return entityInSameSequence?.map((el) => {
|
||||
return {
|
||||
sequence: el.sequence || policy.approvals,
|
||||
userLabels: members
|
||||
?.filter((member) => el.user.find((i) => i.id === member.user.id))
|
||||
.map((member) => getMemberLabel(member))
|
||||
.join(", ")
|
||||
: null;
|
||||
|
||||
const groupLabels = groupsInPolicy?.length
|
||||
? groups
|
||||
.filter(({ group }) => groupsInPolicy?.includes(group.id))
|
||||
.join(","),
|
||||
groupLabels: groups
|
||||
?.filter(({ group }) => el.group.find((i) => i.id === group.id))
|
||||
.map(({ group }) => group.name)
|
||||
.join(", ")
|
||||
: null;
|
||||
|
||||
return {
|
||||
members: memberLabels,
|
||||
groups: groupLabels
|
||||
};
|
||||
.join(","),
|
||||
approvals: el.approvals
|
||||
};
|
||||
});
|
||||
}, [policy, members, groups]);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{policy.name}</Td>
|
||||
<Td>{policy.environment.slug}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td className="max-w-0">
|
||||
<Tooltip
|
||||
side="left"
|
||||
content={labels.members ?? "No users are assigned as approvers for this policy"}
|
||||
>
|
||||
<p className="truncate">{labels.members ?? "-"}</p>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<Tooltip
|
||||
side="left"
|
||||
content={labels.groups ?? "No groups are assigned as approvers for this policy"}
|
||||
>
|
||||
<p className="truncate">{labels.groups ?? "-"}</p>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>{policy.approvals}</Td>
|
||||
<Td>
|
||||
<Badge className={policyDetails[policy.policyType].className}>
|
||||
{policyDetails[policy.policyType].name}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
<>
|
||||
<Tr
|
||||
isHoverable
|
||||
isSelectable
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setIsExpanded.toggle();
|
||||
}}
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<Td>{policy.name}</Td>
|
||||
<Td>{policy.environment.slug}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td>
|
||||
<Badge className={policyDetails[policy.policyType].className}>
|
||||
{policyDetails[policy.policyType].name}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Policy
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Policy
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="rounded bg-mineshaft-900">
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||
{labels?.map((el, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
||||
>
|
||||
<div>
|
||||
<div className="mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white">
|
||||
<div className="text-lg">{index + 1}</div>
|
||||
</div>
|
||||
{index !== labels.length - 1 && (
|
||||
<div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Policy
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
{index !== 0 && (
|
||||
<div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Policy
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
</div>
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||
<div>{el.userLabels || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||
<div>{el.groupLabels || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||
<div>{el.approvals || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user