diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 70c299564..ad49c4119 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -37,7 +37,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true, caCrl: false, instanceUserManagement: false, diff --git a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx index 450b58aef..bca2bf516 100644 --- a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx +++ b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx @@ -34,18 +34,19 @@ export const FilterableSelect = ({ tabSelectsValue={tabSelectsValue} components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }} classNames={{ - container: () => "w-full font-inter", + container: () => "w-full text-sm font-inter", control: ({ isFocused }) => twMerge( isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400", "border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer" ), - placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5", + placeholder: () => + `${isMulti ? "py-[0.22rem]" : "leading-7"} text-mineshaft-400 text-sm pl-1`, input: () => "pl-1 py-0.5", valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`, singleValue: () => "leading-7 ml-1", - multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5", + multiValue: () => "bg-mineshaft-600 text-sm rounded items-center py-0.5 px-2 gap-1.5", multiValueLabel: () => "leading-6 text-sm", multiValueRemove: () => "hover:text-red text-bunker-400", indicatorsContainer: () => "p-1 gap-1", @@ -53,7 +54,7 @@ export const FilterableSelect = ({ indicatorSeparator: () => "bg-bunker-400", dropdownIndicator: () => "text-bunker-200 p-1", menu: () => - "mt-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md", + "my-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md", groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm", option: ({ isFocused, isSelected }) => twMerge( diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx index 936e6b981..a3e06459e 100644 --- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx @@ -175,7 +175,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { leftIcon={} isDisabled={!isAllowed} > - Create policy + Create Policy )} diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index e96cd3e2d..2f386f53b 100644 --- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -1,18 +1,12 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; -import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, + FilterableSelect, FormControl, Input, Modal, @@ -46,21 +40,34 @@ type Props = { const formSchema = z .object({ - environment: z.string(), + environment: z.object({ slug: z.string(), name: z.string() }), name: z.string().optional(), secretPath: z.string().optional(), approvals: z.number().min(1), - approvers: z - .object({ type: z.nativeEnum(ApproverType), id: z.string() }) + 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() - .min(1) .default([]), policyType: z.nativeEnum(PolicyType), enforcementLevel: z.nativeEnum(EnforcementLevel) }) - .refine((data) => data.approvers, { - path: ["approvers"], - message: "At least one approver should be provided." + .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; @@ -84,8 +91,15 @@ export const AccessPolicyForm = ({ values: editValues ? { ...editValues, - environment: editValues.environment.slug, - approvers: editValues?.approvers || [], + 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 @@ -110,18 +124,27 @@ export const AccessPolicyForm = ({ const approversRequired = watch("approvals") || 1; - const handleCreatePolicy = async (data: TFormSchema) => { + 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 }); } @@ -139,7 +162,12 @@ export const AccessPolicyForm = ({ } }; - const handleUpdatePolicy = async (data: TFormSchema) => { + const handleUpdatePolicy = async ({ + environment, + userApprovers, + groupApprovers, + ...data + }: TFormSchema) => { if (!projectId || !projectSlug) return; if (!editValues?.id) return; @@ -148,12 +176,15 @@ export const AccessPolicyForm = ({ 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 }); } @@ -179,9 +210,35 @@ export const AccessPolicyForm = ({ } }; + const memberOptions = useMemo( + () => + members.map(({ inviteEmail, user: { firstName, lastName, id: userId, username } }) => ({ + id: userId, + type: ApproverType.User, + label: + firstName || lastName + ? `${firstName ?? ""} ${lastName ?? ""}`.trim() + : username || inviteEmail + })), + [members] + ); + + const groupOptions = useMemo( + () => + groups?.map(({ group }) => ({ + id: group.id, + type: ApproverType.Group, + label: group.name + })), + [groups] + ); + return ( - +
( - + onChange={onChange} + placeholder="Select environment..." + options={environments} + getOptionValue={(option) => option.slug} + getOptionLabel={(option) => option.name} + /> )} /> @@ -331,127 +381,60 @@ export const AccessPolicyForm = ({
( - - - e.type === ApproverType.User).length - ? `${value.filter((e) => e.type === ApproverType.User).length} selected` - : "None" - } - className="text-left" - /> - - - - Select members that are allowed to approve requests - - {members.map(({ user }) => { - const { id: userId } = user; - const isChecked = - value?.filter( - (el: { id: string; type: ApproverType }) => - el.id === userId && el.type === ApproverType.User - ).length > 0; - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter( - (el: { id: string; type: ApproverType }) => - el.id !== userId && el.type !== ApproverType.User - ) - : [...(value || []), { id: userId, type: ApproverType.User }] - ); - }} - key={`create-policy-members-${userId}`} - iconPos="right" - icon={isChecked && } - > - {user.username} - - ); - })} - - + option.id} + getOptionLabel={(option) => { + const member = members?.find((m) => m.user.id === option.id); + + if (!member) return option.id; + + const { + inviteEmail, + user: { firstName, lastName, username, email } + } = member; + + return firstName || lastName + ? `${firstName ?? ""} ${lastName ?? ""}`.trim() + : username || email || inviteEmail; + }} + value={value} + onChange={onChange} + /> )} /> ( - - - e.type === ApproverType.Group).length - ? `${ - value?.filter((e) => e.type === ApproverType.Group).length - } selected` - : "None" - } - className="text-left" - /> - - - - Select groups that are allowed to approve requests - - {groups && - groups.map(({ group }) => { - const { id } = group; - const isChecked = - value?.filter( - (el: { id: string; type: ApproverType }) => - el.id === id && el.type === ApproverType.Group - ).length > 0; - - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter( - (el: { id: string; type: ApproverType }) => - el.id !== id && el.type !== ApproverType.Group - ) - : [...(value || []), { id, type: ApproverType.Group }] - ); - }} - key={`create-policy-members-${id}`} - iconPos="right" - icon={isChecked && } - > - {group.name} - - ); - })} - - + option.id} + getOptionLabel={(option) => + groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id + } + value={value} + onChange={onChange} + /> )} />