mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' into fix/cli-jwt-issue
This commit is contained in:
@@ -35,7 +35,7 @@ export type Approver = {
|
||||
id: string;
|
||||
type: ApproverType;
|
||||
sequence?: number;
|
||||
approvals?: number;
|
||||
approvalsRequired?: number;
|
||||
};
|
||||
|
||||
export type Bypasser = {
|
||||
|
||||
@@ -185,7 +185,7 @@ export const ReviewAccessRequestModal = ({
|
||||
return acc;
|
||||
}
|
||||
|
||||
const approvals = curr.approvals || policy.approvals;
|
||||
const approvals = curr.approvalsRequired || policy.approvals;
|
||||
const sequence = curr.sequence || 1;
|
||||
|
||||
acc.push(
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -264,14 +266,29 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<AccessPolicyForm
|
||||
projectId={currentWorkspace.id}
|
||||
projectSlug={currentWorkspace.slug}
|
||||
<Modal
|
||||
isOpen={popUp.policyForm.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||
members={members}
|
||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||
/>
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||
>
|
||||
<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
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
|
||||
@@ -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 { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
@@ -122,7 +120,6 @@ export const AccessPolicyForm = ({
|
||||
}: Props) => {
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
const modalContainer = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -153,34 +150,34 @@ export const AccessPolicyForm = ({
|
||||
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
|
||||
approvals: editValues?.approvals,
|
||||
allowedSelfApprovals: editValues?.allowedSelfApprovals,
|
||||
sequenceApprovers: editValues.approvers
|
||||
?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0))
|
||||
.reduce(
|
||||
(acc, 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 }
|
||||
);
|
||||
sequenceApprovers: editValues.approvers?.reduce(
|
||||
(acc, curr) => {
|
||||
if (acc.length && acc[acc.length - 1].sequence === curr.sequence) {
|
||||
acc[acc.length - 1][curr.type]?.push(curr);
|
||||
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)
|
||||
: undefined,
|
||||
defaultValues: {
|
||||
sequenceApprovers: [{ approvals: 1 }]
|
||||
}
|
||||
defaultValues: !editValues
|
||||
? {
|
||||
sequenceApprovers: [{ approvals: 1 }]
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const sequenceApproversFieldArray = useFieldArray({
|
||||
control,
|
||||
@@ -204,7 +201,6 @@ export const AccessPolicyForm = ({
|
||||
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
|
||||
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
||||
|
||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
||||
const enforcementLevel = watch("enforcementLevel");
|
||||
|
||||
const formUserBypassers = watch("userBypassers");
|
||||
@@ -391,446 +387,429 @@ export const AccessPolicyForm = ({
|
||||
setDraggedItem(null);
|
||||
setDragOverItem(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
className="max-w-3xl"
|
||||
ref={modalContainer}
|
||||
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
||||
>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="policyType"
|
||||
defaultValue={PolicyType.ChangePolicy}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Policy Type"
|
||||
isRequired
|
||||
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}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val as PolicyType)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{Object.values(PolicyType).map((policyType) => {
|
||||
return (
|
||||
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
|
||||
{policyDetails[policyType].name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="policyType"
|
||||
defaultValue={PolicyType.ChangePolicy}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Policy Type"
|
||||
isRequired
|
||||
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}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val as PolicyType)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{Object.values(PolicyType).map((policyType) => {
|
||||
return (
|
||||
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
|
||||
{policyDetails[policyType].name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isAccessPolicyType && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
name="approvals"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isRequired
|
||||
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
|
||||
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
|
||||
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}
|
||||
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={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: []
|
||||
})
|
||||
<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"
|
||||
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
|
||||
</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);
|
||||
<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;
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
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>
|
||||
)}
|
||||
<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"
|
||||
>
|
||||
<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 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>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,11 +59,11 @@ export const ApprovalPolicyRow = ({
|
||||
const sortedSteps = policy.approvers?.sort((a, b) => (a?.sequence || 0) - (b?.sequence || 0));
|
||||
const entityInSameSequence = sortedSteps?.reduce(
|
||||
(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);
|
||||
return acc;
|
||||
}
|
||||
const approvals = curr.approvals || policy.approvals;
|
||||
const approvals = curr.approvalsRequired || policy.approvals;
|
||||
acc.push(
|
||||
curr.type === ApproverType.User
|
||||
? { user: [curr], group: [], sequence: 1, approvals }
|
||||
|
||||
Reference in New Issue
Block a user