mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
Merge pull request #3826 from akhilmhdh/feat/aws-auth-increase-limit
feat: patched up approval sequence ui bugs
This commit is contained in:
@@ -35,7 +35,7 @@ export type Approver = {
|
|||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
sequence?: number;
|
sequence?: number;
|
||||||
approvals?: number;
|
approvalsRequired?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Bypasser = {
|
export type Bypasser = {
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvals = curr.approvals || policy.approvals;
|
const approvals = curr.approvalsRequired || policy.approvals;
|
||||||
const sequence = curr.sequence || 1;
|
const sequence = curr.sequence || 1;
|
||||||
|
|
||||||
acc.push(
|
acc.push(
|
||||||
|
|||||||
+24
-7
@@ -19,6 +19,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -264,14 +266,29 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<AccessPolicyForm
|
<Modal
|
||||||
projectId={currentWorkspace.id}
|
|
||||||
projectSlug={currentWorkspace.slug}
|
|
||||||
isOpen={popUp.policyForm.isOpen}
|
isOpen={popUp.policyForm.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||||
members={members}
|
>
|
||||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
<ModalContent
|
||||||
/>
|
className="max-w-3xl"
|
||||||
|
title={
|
||||||
|
popUp.policyForm.data
|
||||||
|
? `Edit ${popUp?.policyForm?.data?.name || "Policy"}`
|
||||||
|
: "Create Policy"
|
||||||
|
}
|
||||||
|
id="policy-form"
|
||||||
|
>
|
||||||
|
<AccessPolicyForm
|
||||||
|
projectId={currentWorkspace.id}
|
||||||
|
projectSlug={currentWorkspace.slug}
|
||||||
|
isOpen={popUp.policyForm.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||||
|
members={members}
|
||||||
|
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePolicy.isOpen}
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
+429
-450
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -122,7 +120,6 @@ export const AccessPolicyForm = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||||
const modalContainer = useRef<HTMLDivElement>(null);
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -153,34 +150,34 @@ export const AccessPolicyForm = ({
|
|||||||
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
||||||
approvals: editValues?.approvals,
|
approvals: editValues?.approvals,
|
||||||
allowedSelfApprovals: editValues?.allowedSelfApprovals,
|
allowedSelfApprovals: editValues?.allowedSelfApprovals,
|
||||||
sequenceApprovers: editValues.approvers
|
sequenceApprovers: editValues.approvers?.reduce(
|
||||||
?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0))
|
(acc, curr) => {
|
||||||
.reduce(
|
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||||
(acc, curr) => {
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
if (acc.length && 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;
|
return acc;
|
||||||
},
|
}
|
||||||
[] as { user: Approver[]; group: Approver[]; sequence?: number; approvals: number }[]
|
const approvals = curr.approvalsRequired || 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 }[]
|
||||||
|
)
|
||||||
} as TFormSchema)
|
} as TFormSchema)
|
||||||
: undefined,
|
: undefined,
|
||||||
defaultValues: {
|
defaultValues: !editValues
|
||||||
sequenceApprovers: [{ approvals: 1 }]
|
? {
|
||||||
}
|
sequenceApprovers: [{ approvals: 1 }]
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
const sequenceApproversFieldArray = useFieldArray({
|
const sequenceApproversFieldArray = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -204,7 +201,6 @@ export const AccessPolicyForm = ({
|
|||||||
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
|
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
|
||||||
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
||||||
|
|
||||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
|
||||||
const enforcementLevel = watch("enforcementLevel");
|
const enforcementLevel = watch("enforcementLevel");
|
||||||
|
|
||||||
const formUserBypassers = watch("userBypassers");
|
const formUserBypassers = watch("userBypassers");
|
||||||
@@ -391,446 +387,429 @@ export const AccessPolicyForm = ({
|
|||||||
setDraggedItem(null);
|
setDraggedItem(null);
|
||||||
setDragOverItem(null);
|
setDragOverItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<div className="flex flex-col space-y-3">
|
||||||
<ModalContent
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
className="max-w-3xl"
|
<div className="flex items-center gap-x-3">
|
||||||
ref={modalContainer}
|
<Controller
|
||||||
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
control={control}
|
||||||
>
|
name="policyType"
|
||||||
<div className="flex flex-col space-y-3">
|
defaultValue={PolicyType.ChangePolicy}
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<div className="flex items-center gap-x-3">
|
<FormControl
|
||||||
<Controller
|
label="Policy Type"
|
||||||
control={control}
|
isRequired
|
||||||
name="policyType"
|
isError={Boolean(error)}
|
||||||
defaultValue={PolicyType.ChangePolicy}
|
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."
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
errorText={error?.message}
|
||||||
<FormControl
|
className="flex-grow"
|
||||||
label="Policy Type"
|
>
|
||||||
isRequired
|
<Select
|
||||||
isError={Boolean(error)}
|
isDisabled={isEditMode}
|
||||||
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."
|
value={value}
|
||||||
errorText={error?.message}
|
onValueChange={(val) => onChange(val as PolicyType)}
|
||||||
className="flex-grow"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
>
|
||||||
<Select
|
{Object.values(PolicyType).map((policyType) => {
|
||||||
isDisabled={isEditMode}
|
return (
|
||||||
value={value}
|
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
|
||||||
onValueChange={(val) => onChange(val as PolicyType)}
|
{policyDetails[policyType].name}
|
||||||
className="w-full border border-mineshaft-500"
|
</SelectItem>
|
||||||
>
|
);
|
||||||
{Object.values(PolicyType).map((policyType) => {
|
})}
|
||||||
return (
|
</Select>
|
||||||
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
|
</FormControl>
|
||||||
{policyDetails[policyType].name}
|
)}
|
||||||
</SelectItem>
|
/>
|
||||||
);
|
{!isAccessPolicyType && (
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</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"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Policy Name"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="flex-grow"
|
|
||||||
>
|
|
||||||
<Input {...field} value={field.value || ""} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPath"
|
|
||||||
defaultValue="/"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths."
|
|
||||||
label="Secret Path"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="flex-grow"
|
|
||||||
>
|
|
||||||
<Input {...field} value={field.value || ""} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="environment"
|
name="approvals"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Environment"
|
label="Min. Approvals Required"
|
||||||
isRequired
|
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
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"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Policy Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
defaultValue="/"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths."
|
||||||
|
label="Secret Path"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Environment"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isDisabled={isEditMode}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder="Select environment..."
|
||||||
|
options={environments}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mb-2">
|
||||||
|
<p>Approvers</p>
|
||||||
|
<p className="font-inter text-xs text-mineshaft-300 opacity-90">
|
||||||
|
Select members or groups that are allowed to approve requests from this policy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{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={document.getElementById("policy-form")}
|
||||||
|
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={`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={document.getElementById("policy-form")}
|
||||||
|
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: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
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
|
<FilterableSelect
|
||||||
isDisabled={isEditMode}
|
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}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="Select environment..."
|
|
||||||
options={environments}
|
|
||||||
getOptionValue={(option) => option.slug}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="mb-2">
|
<Controller
|
||||||
<p>Approvers</p>
|
control={control}
|
||||||
<p className="font-inter text-xs text-mineshaft-300 opacity-90">
|
name="groupApprovers"
|
||||||
Select members or groups that are allowed to approve requests from this policy.
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
</p>
|
<FormControl
|
||||||
</div>
|
label="Group Approvers"
|
||||||
{isAccessPolicyType ? (
|
isError={Boolean(error)}
|
||||||
<>
|
errorText={error?.message}
|
||||||
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded">
|
className="w-1/2"
|
||||||
{sequenceApproversFieldArray.fields.map((el, index) => (
|
>
|
||||||
<div
|
<FilterableSelect
|
||||||
className={twMerge(
|
menuPlacement="top"
|
||||||
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0",
|
isMulti
|
||||||
dragOverItem === index ? "border-2 border-blue-400" : "",
|
placeholder="Select groups..."
|
||||||
draggedItem === index ? "opacity-50" : ""
|
options={groupOptions}
|
||||||
)}
|
getOptionValue={(option) => option.id}
|
||||||
key={el.id}
|
getOptionLabel={(option) =>
|
||||||
onDragOver={(e) => handleDragOver(e, index)}
|
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||||
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;
|
|
||||||
|
|
||||||
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: []
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedSelfApprovals"
|
||||||
|
defaultValue
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Self Approvals" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
id="self-approvals"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
>
|
||||||
|
Allow approvers to review their own requests
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="enforcementLevel"
|
||||||
|
defaultValue={EnforcementLevel.Hard}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Bypass Approvals"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-3"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="bypass-approvals"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={value === EnforcementLevel.Soft}
|
||||||
|
onCheckedChange={(v) => onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)}
|
||||||
|
>
|
||||||
|
Allow certain users to bypass policy in break-glass situations
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{enforcementLevel === EnforcementLevel.Soft && (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="userBypassers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User Bypassers"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2 w-1/2"
|
||||||
>
|
>
|
||||||
Add Step
|
<FilterableSelect
|
||||||
</Button>
|
menuPlacement="top"
|
||||||
</div>
|
isMulti
|
||||||
</>
|
placeholder="Select members..."
|
||||||
) : (
|
options={bypasserMemberOptions}
|
||||||
<div className="flex gap-2">
|
getOptionValue={(option) => option.id}
|
||||||
<Controller
|
getOptionLabel={(option) => {
|
||||||
control={control}
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
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;
|
if (!member) return option.id;
|
||||||
|
|
||||||
return getMemberLabel(member);
|
return getMemberLabel(member);
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="groupApprovers"
|
name="groupBypassers"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Group Approvers"
|
label="Group Bypassers"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
className="w-1/2"
|
className="mb-2 w-1/2"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select groups..."
|
placeholder="Select groups..."
|
||||||
options={groupOptions}
|
options={bypasserGroupOptions}
|
||||||
getOptionValue={(option) => option.id}
|
getOptionValue={(option) => option.id}
|
||||||
getOptionLabel={(option) =>
|
getOptionLabel={(option) =>
|
||||||
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||||
option.id
|
}
|
||||||
}
|
value={value}
|
||||||
value={value}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
|
{bypasserCount <= 0 && (
|
||||||
|
<div className="mt-1 flex rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm text-bunker-300">
|
||||||
|
Not selecting specific users or groups will allow anyone to bypass this policy.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Controller
|
</>
|
||||||
control={control}
|
)}
|
||||||
name="allowedSelfApprovals"
|
<div className="mt-8 flex items-center space-x-4">
|
||||||
defaultValue
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
Save
|
||||||
<FormControl
|
</Button>
|
||||||
label="Self Approvals"
|
<Button onClick={() => onToggle(false)} variant="outline_bg">
|
||||||
isError={Boolean(error)}
|
Close
|
||||||
errorText={error?.message}
|
</Button>
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
id="self-approvals"
|
|
||||||
thumbClassName="bg-mineshaft-800"
|
|
||||||
isChecked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
>
|
|
||||||
Allow approvers to review their own requests
|
|
||||||
</Switch>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="enforcementLevel"
|
|
||||||
defaultValue={EnforcementLevel.Hard}
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Bypass Approvals"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-3"
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
id="bypass-approvals"
|
|
||||||
thumbClassName="bg-mineshaft-800"
|
|
||||||
isChecked={value === EnforcementLevel.Soft}
|
|
||||||
onCheckedChange={(v) =>
|
|
||||||
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Allow certain users to bypass policy in break-glass situations
|
|
||||||
</Switch>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{enforcementLevel === EnforcementLevel.Soft && (
|
|
||||||
<>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="userBypassers"
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="User Bypassers"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-2 w-1/2"
|
|
||||||
>
|
|
||||||
<FilterableSelect
|
|
||||||
menuPlacement="top"
|
|
||||||
isMulti
|
|
||||||
placeholder="Select members..."
|
|
||||||
options={bypasserMemberOptions}
|
|
||||||
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="groupBypassers"
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Group Bypassers"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-2 w-1/2"
|
|
||||||
>
|
|
||||||
<FilterableSelect
|
|
||||||
menuPlacement="top"
|
|
||||||
isMulti
|
|
||||||
placeholder="Select groups..."
|
|
||||||
options={bypasserGroupOptions}
|
|
||||||
getOptionValue={(option) => option.id}
|
|
||||||
getOptionLabel={(option) =>
|
|
||||||
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
|
||||||
option.id
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{bypasserCount <= 0 && (
|
|
||||||
<div className="mt-1 flex rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm text-bunker-300">
|
|
||||||
Not selecting specific users or groups will allow anyone to bypass this policy.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex items-center space-x-4">
|
|
||||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => onToggle(false)} variant="outline_bg">
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</form>
|
||||||
</Modal>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -59,11 +59,11 @@ export const ApprovalPolicyRow = ({
|
|||||||
const sortedSteps = policy.approvers?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0));
|
const sortedSteps = policy.approvers?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0));
|
||||||
const entityInSameSequence = sortedSteps?.reduce(
|
const entityInSameSequence = sortedSteps?.reduce(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
if (acc.length && acc[acc.length - 1].sequence === (curr.sequence || 1)) {
|
||||||
acc[acc.length - 1][curr.type]?.push(curr);
|
acc[acc.length - 1][curr.type]?.push(curr);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
const approvals = curr.approvals || policy.approvals;
|
const approvals = curr.approvalsRequired || policy.approvals;
|
||||||
acc.push(
|
acc.push(
|
||||||
curr.type === ApproverType.User
|
curr.type === ApproverType.User
|
||||||
? { user: [curr], group: [], sequence: 1, approvals }
|
? { user: [curr], group: [], sequence: 1, approvals }
|
||||||
|
|||||||
Reference in New Issue
Block a user