From c325674da0ccea8490202cfb9f31f4eb06cf7cab Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:21 -0700 Subject: [PATCH] Fix: Move standalone components to individual files --- .../components/AccessPolicyModal.tsx | 266 ++++++++++++++++++ .../components/RequestAccessModal.tsx | 25 ++ .../components/ReviewAccessModal.tsx | 158 +++++++++++ 3 files changed, 449 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx new file mode 100644 index 000000000..6c0ee3fb6 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx @@ -0,0 +1,266 @@ +import { useEffect } 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, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + projectSlug: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + projectSlug, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + + try { + await createAccessApprovalPolicy({ + ...data, + 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 (data: TFormSchema) => { + if (!projectSlug) return; + if (!editValues?.id) return; + + try { + await updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + 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); + } + }; + + return ( + + +
+ ( + + + + )} + /> + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + + + + + Select members that are allowed to approve changes + + {members.map(({ id, user }) => { + const isChecked = value?.includes(id); + return ( + { + evt.preventDefault(); + onChange( + isChecked ? value?.filter((el) => el !== id) : [...(value || []), id] + ); + }} + key={`create-policy-members-${id}`} + iconPos="right" + icon={isChecked && } + > + {user.username} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx new file mode 100644 index 000000000..735ca3b53 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx @@ -0,0 +1,25 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; + +export const RequestAccessModal = ({ + isOpen, + onOpenChange, + policies +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + policies: TAccessApprovalPolicy[]; +}) => { + return ( + + + onOpenChange(false)} policies={policies} /> + + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx new file mode 100644 index 000000000..481cee9c6 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -0,0 +1,158 @@ +import { useCallback, useMemo, useState } from "react"; +import ms from "ms"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Modal, ModalContent } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; +import { ProjectPermissionActions } from "@app/context"; +import { useReviewAccessRequest } from "@app/hooks/api"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; + +export const ReviewAccessRequestModal = ({ + isOpen, + onOpenChange, + request, + projectSlug, + selectedRequester, + selectedEnvSlug +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; + projectSlug: string; + selectedRequester: string | undefined; + selectedEnvSlug: string | undefined; +}) => { + const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); + + const accessDetails = { + env: request.environmentName, + // secret path will be inside $glob operator + secretPath: request.policy.secretPath, + read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), + edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), + create: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + + temporaryAccess: { + isTemporary: request.isTemporary, + temporaryRange: request.temporaryRange + } + }; + + const requestedAccess = useMemo(() => { + const access: string[] = []; + if (accessDetails.read) access.push("Read"); + if (accessDetails.edit) access.push("Edit"); + if (accessDetails.create) access.push("Create"); + if (accessDetails.delete) access.push("Delete"); + + return access.join(", "); + }, [accessDetails]); + + const getAccessLabel = () => { + if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) + return "Permanent"; + + // convert the range to human readable format + ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); + + return ( + + {`Valid for ${ms(ms(accessDetails.temporaryAccess.temporaryRange), { + long: true + })} after approval`} + + ); + }; + + const reviewAccessRequest = useReviewAccessRequest(); + + const handleReview = useCallback(async (status: "approved" | "rejected") => { + setIsLoading(status); + try { + await reviewAccessRequest.mutateAsync({ + requestId: request.id, + status, + projectSlug, + envSlug: selectedEnvSlug, + requestedBy: selectedRequester + }); + } catch (error) { + console.error(error); + setIsLoading(null); + return; + } + + createNotification({ + title: `Request ${status}`, + text: `The request has been ${status}`, + type: status === "approved" ? "success" : "info" + }); + + setIsLoading(null); + onOpenChange(false); + }, []); + + return ( + + +
+ + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + {" "} + is requesting access to the following resource: + + +
+
+ Requested path: + {accessDetails.env + accessDetails.secretPath || ""} +
+ +
+ Permissions: + {requestedAccess} +
+ +
+ Access Type: + {getAccessLabel()} +
+
+ +
+ + +
+
+
+
+ ); +};