Merge branch 'main' into fix/cli-jwt-issue

This commit is contained in:
x032205
2025-06-19 19:19:12 -04:00
5 changed files with 457 additions and 461 deletions

View File

@@ -35,7 +35,7 @@ export type Approver = {
id: string;
type: ApproverType;
sequence?: number;
approvals?: number;
approvalsRequired?: number;
};
export type Bypasser = {

View File

@@ -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(

View File

@@ -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"

View File

@@ -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>
);
};

View File

@@ -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 }