mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 23:00:14 +00:00
feat: renamed old frontend and fixed some minor bugs
This commit is contained in:
+292
@@ -0,0 +1,292 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFileShield,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
TProjectPermission,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
useDeleteSecretApprovalPolicy,
|
||||
useGetSecretApprovalPolicies,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceGroups
|
||||
} from "@app/hooks/api";
|
||||
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
||||
|
||||
import { AccessPolicyForm } from "./components/AccessPolicyModal";
|
||||
import { ApprovalPolicyRow } from "./components/ApprovalPolicyRow";
|
||||
|
||||
interface IProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
||||
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
||||
{
|
||||
projectSlug: currentWorkspace?.slug as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.slug
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data: secretPolicies, isPending: isSecretPoliciesLoading } = useGetSecretApprovalPolicies(
|
||||
{
|
||||
workspaceId: currentWorkspace?.id as string,
|
||||
options: {
|
||||
enabled:
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
|
||||
!!currentWorkspace?.id
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// merge data sorted by updatedAt
|
||||
const policies = [
|
||||
...(accessPolicies?.map((policy) => ({ ...policy, policyType: PolicyType.AccessPolicy })) ||
|
||||
[]),
|
||||
...(secretPolicies?.map((policy) => ({ ...policy, policyType: PolicyType.ChangePolicy })) || [])
|
||||
].sort((a, b) => {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
|
||||
return {
|
||||
policies,
|
||||
isLoading: isAccessPoliciesLoading || isSecretPoliciesLoading
|
||||
};
|
||||
};
|
||||
|
||||
export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
|
||||
"policyForm",
|
||||
"deletePolicy",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const { permission } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: members } = useGetWorkspaceUsers(workspaceId, true);
|
||||
const { data: groups } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
||||
|
||||
const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(
|
||||
permission,
|
||||
currentWorkspace
|
||||
);
|
||||
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
|
||||
const filteredPolicies = useMemo(() => {
|
||||
return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
|
||||
}, [policies, filterType]);
|
||||
|
||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
const { id, policyType } = popUp.deletePolicy.data as TAccessApprovalPolicy;
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
try {
|
||||
if (policyType === PolicyType.ChangePolicy) {
|
||||
await deleteSecretApprovalPolicy({
|
||||
workspaceId,
|
||||
id
|
||||
});
|
||||
} else {
|
||||
await deleteAccessApprovalPolicy({
|
||||
projectSlug: currentWorkspace?.slug,
|
||||
id
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted policy"
|
||||
});
|
||||
handlePopUpClose("deletePolicy");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-end justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Policies</span>
|
||||
<div className="mt-2 text-sm text-bunker-300">
|
||||
Implement granular policies for access requests and secrets management.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SecretApproval}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (subscription && !subscription?.secretApproval) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("policyForm");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<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>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="text-xs font-semibold uppercase text-bunker-300"
|
||||
rightIcon={
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||
}
|
||||
>
|
||||
Type
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Select a type</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(null)}
|
||||
icon={!filterType && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
All
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(PolicyType.AccessPolicy)}
|
||||
icon={
|
||||
filterType === PolicyType.AccessPolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Access Policy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilterType(PolicyType.ChangePolicy)}
|
||||
icon={
|
||||
filterType === PolicyType.ChangePolicy && (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
Change Policy
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPoliciesLoading && (
|
||||
<TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
|
||||
)}
|
||||
{!isPoliciesLoading && !filteredPolicies?.length && (
|
||||
<Tr>
|
||||
<Td colSpan={6}>
|
||||
<EmptyState title="No policies found" icon={faFileShield} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!!currentWorkspace &&
|
||||
filteredPolicies?.map((policy) => (
|
||||
<ApprovalPolicyRow
|
||||
policy={policy}
|
||||
key={policy.id}
|
||||
members={members}
|
||||
groups={groups}
|
||||
onEdit={() => handlePopUpOpen("policyForm", policy)}
|
||||
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<AccessPolicyForm
|
||||
projectId={currentWorkspace.id}
|
||||
projectSlug={currentWorkspace.slug}
|
||||
isOpen={popUp.policyForm.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
||||
members={members}
|
||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this policy?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { policyDetails } from "@app/helpers/policies";
|
||||
import {
|
||||
useCreateSecretApprovalPolicy,
|
||||
useListWorkspaceGroups,
|
||||
useUpdateSecretApprovalPolicy
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
useCreateAccessApprovalPolicy,
|
||||
useUpdateAccessApprovalPolicy
|
||||
} from "@app/hooks/api/accessApproval";
|
||||
import { ApproverType, TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
|
||||
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
members?: TWorkspaceUser[];
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
editValues?: TAccessApprovalPolicy;
|
||||
};
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
environment: z.object({ slug: z.string(), name: z.string() }),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().optional(),
|
||||
approvals: z.number().min(1),
|
||||
userApprovers: z
|
||||
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
groupApprovers: z
|
||||
.object({ type: z.literal(ApproverType.Group), id: z.string() })
|
||||
.array()
|
||||
.default([]),
|
||||
policyType: z.nativeEnum(PolicyType),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
||||
ctx.addIssue({
|
||||
path: ["userApprovers"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one approver should be provided"
|
||||
});
|
||||
ctx.addIssue({
|
||||
path: ["groupApprovers"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one approver should be provided"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const AccessPolicyForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
members = [],
|
||||
projectId,
|
||||
projectSlug,
|
||||
editValues
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: editValues
|
||||
? {
|
||||
...editValues,
|
||||
environment: editValues.environment,
|
||||
userApprovers:
|
||||
editValues?.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map(({ id, type }) => ({ id, type: type as ApproverType.User })) || [],
|
||||
groupApprovers:
|
||||
editValues?.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
|
||||
approvals: editValues?.approvals
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const isEditMode = Boolean(editValues);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !isEditMode) reset({});
|
||||
}, [isOpen, isEditMode]);
|
||||
|
||||
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||
|
||||
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
|
||||
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
||||
|
||||
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
|
||||
|
||||
const approversRequired = watch("approvals") || 1;
|
||||
|
||||
const handleCreatePolicy = async ({
|
||||
environment,
|
||||
groupApprovers,
|
||||
userApprovers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId) return;
|
||||
|
||||
try {
|
||||
if (data.policyType === PolicyType.ChangePolicy) {
|
||||
await createSecretApprovalPolicy({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
environment: environment.slug,
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
} else {
|
||||
await createAccessApprovalPolicy({
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created policy"
|
||||
});
|
||||
onToggle(false);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePolicy = async ({
|
||||
environment,
|
||||
userApprovers,
|
||||
groupApprovers,
|
||||
...data
|
||||
}: TFormSchema) => {
|
||||
if (!projectId || !projectSlug) return;
|
||||
if (!editValues?.id) return;
|
||||
|
||||
try {
|
||||
if (data.policyType === PolicyType.ChangePolicy) {
|
||||
await updateSecretApprovalPolicy({
|
||||
id: editValues?.id,
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
} else {
|
||||
await updateAccessApprovalPolicy({
|
||||
id: editValues?.id,
|
||||
...data,
|
||||
approvers: [...userApprovers, ...groupApprovers],
|
||||
environment: environment.slug,
|
||||
projectSlug
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated policy"
|
||||
});
|
||||
onToggle(false);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "failed to update policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
if (isEditMode) {
|
||||
await handleUpdatePolicy(data);
|
||||
} else {
|
||||
await handleCreatePolicy(data);
|
||||
}
|
||||
};
|
||||
|
||||
const memberOptions = useMemo(
|
||||
() =>
|
||||
members.map((member) => ({
|
||||
id: member.user.id,
|
||||
type: ApproverType.User
|
||||
})),
|
||||
[members]
|
||||
);
|
||||
|
||||
const groupOptions = useMemo(
|
||||
() =>
|
||||
groups?.map(({ group }) => ({
|
||||
id: group.id,
|
||||
type: ApproverType.Group
|
||||
})),
|
||||
[groups]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
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">
|
||||
<Controller
|
||||
control={control}
|
||||
name="policyType"
|
||||
defaultValue={PolicyType.ChangePolicy}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Policy Type"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Policy Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enforcementLevel"
|
||||
defaultValue={EnforcementLevel.Hard}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Enforcement Level"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
<>
|
||||
<p>
|
||||
Determines the level of enforcement for required approvers of a request:
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-bold">Hard</span> enforcement requires at least{" "}
|
||||
<span className="font-bold"> {approversRequired}</span> approver(s) to
|
||||
approve the request.`
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-bold">Soft</span> enforcement At least{" "}
|
||||
<span className="font-bold">{approversRequired}</span> approver(s) must
|
||||
approve the request; however, the requester can bypass approval
|
||||
requirements in emergencies.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(val) => field.onChange(val as EnforcementLevel)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{Object.values(EnforcementLevel).map((level) => {
|
||||
return (
|
||||
<SelectItem value={level} key={`enforcement-level-${level}`}>
|
||||
<span className="capitalize">{level}</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<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}
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userApprovers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User Approvers"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select members that are allowed to approve requests..."
|
||||
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}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isMulti
|
||||
placeholder="Select groups that are allowed to approve requests..."
|
||||
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 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>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { useMemo } from "react";
|
||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { getMemberLabel } from "@app/helpers/members";
|
||||
import { policyDetails } from "@app/helpers/policies";
|
||||
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";
|
||||
import { ApproverType } from "@app/hooks/api/secretApproval/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
|
||||
interface IPolicy {
|
||||
id: string;
|
||||
name: string;
|
||||
environment: WorkspaceEnv;
|
||||
projectId?: string;
|
||||
secretPath?: string;
|
||||
approvals: number;
|
||||
approvers?: Approver[];
|
||||
updatedAt: Date;
|
||||
policyType: PolicyType;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
policy: IPolicy;
|
||||
members?: TWorkspaceUser[];
|
||||
groups?: TGroupMembership[];
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const ApprovalPolicyRow = ({
|
||||
policy,
|
||||
members = [],
|
||||
groups = [],
|
||||
onEdit,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const labels = useMemo(() => {
|
||||
const usersInPolicy = policy.approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id);
|
||||
|
||||
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))
|
||||
.map((member) => getMemberLabel(member))
|
||||
.join(", ")
|
||||
: null;
|
||||
|
||||
const groupLabels = groupsInPolicy?.length
|
||||
? groups
|
||||
.filter(({ group }) => groupsInPolicy?.includes(group.id))
|
||||
.map(({ group }) => group.name)
|
||||
.join(", ")
|
||||
: null;
|
||||
|
||||
return {
|
||||
members: memberLabels,
|
||||
groups: groupLabels
|
||||
};
|
||||
}, [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"
|
||||
)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ApprovalPolicyList } from "./ApprovalPolicyList";
|
||||
Reference in New Issue
Block a user